mirror of
https://github.com/kuhyx/utils.git
synced 2026-11-10 19:47:38 +01:00
Coach tab: no sessions yet is a known state with how to start
The exporter used to publish coach: null before the first session, so the tab printed "unknown" everywhere with no way forward. Now a missing or empty sessions dir exports empty problems/re-tests, every cluster at zero with its problems (slug, number, title, URL) and next, and a how_to block (commands, the 4-step method, first command). null stays for a real failure. how_to lives once in leetcode_coach/how_to.py; DOCS-how-to.md quotes it and a test fails on drift. The app's Coach tab shows a How to start card (monospace command box with copy), one card per cluster with tappable problems, re-tests with their command, and "unknown" + why only for a null section. Also fixes the done/solved key mismatch (the app read a key no exporter wrote). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PjvF5DZG9JduSBYKxqyysi
This commit is contained in:
parent
2d2d8a41f8
commit
e06df74caf
20
leetcode_coach/leetcode_coach/DOCS-how-to.md
Normal file
20
leetcode_coach/leetcode_coach/DOCS-how-to.md
Normal file
@ -0,0 +1,20 @@
|
||||
# The commands and the method, as the status app's Coach tab shows them
|
||||
|
||||
Quoted verbatim from `how_to.py`; `tests/test_how_to.py` fails if they differ.
|
||||
SKILL.md section 3 is the full method; this is its one-line-per-step form.
|
||||
|
||||
Commands:
|
||||
|
||||
- `leetcode --cluster <name>` - Serve the next problem of a concept cluster (a due re-test comes first).
|
||||
- `leetcode <url>` - Coach one problem, from its LeetCode URL or slug.
|
||||
- `leetcode due` - List the cold re-tests that are due.
|
||||
- `leetcode report` - One row per session: requirements before code, hint rung, reveal.
|
||||
|
||||
The command skips problems LeetCode already shows as Accepted, so it may serve a later one than listed here.
|
||||
|
||||
The method:
|
||||
|
||||
1. **Constraint** - Turn the hard constraint into a rule for the design.
|
||||
2. **Requirements** - List what the operation must do, as properties, never by name.
|
||||
3. **Toolbox** - Search the toolbox for an operation with those properties.
|
||||
4. **Post-mortem** - Say which hint gave which requirement, then make the cards.
|
||||
@ -244,3 +244,5 @@ The right client is `leetcode <url>` (print mode: the Stop hook can still
|
||||
withhold a reply; the TUI has already streamed it). `leetcode --local` runs
|
||||
offline on Ollama; everything above `## Model and client` is its system
|
||||
prompt. Clients, gates and the session log: `DOCS-clients.md` here.
|
||||
|
||||
The commands and the method as the status app shows them: `DOCS-how-to.md`.
|
||||
|
||||
@ -27,7 +27,7 @@ import logging
|
||||
import os
|
||||
import sys
|
||||
|
||||
from leetcode_coach import progress
|
||||
from leetcode_coach import how_to, progress
|
||||
from leetcode_guard._live_solved import check_solved
|
||||
from leetcode_guard._settings import build_client
|
||||
|
||||
@ -66,9 +66,10 @@ def _due() -> int:
|
||||
print("no re-tests due")
|
||||
return 0
|
||||
for row in rows:
|
||||
command = how_to.retest_command(row["slug"])
|
||||
print(
|
||||
f"{row['due']} {row['slug']} re-test {row['stage']}/{STAGES} "
|
||||
f"({row['cluster'] or 'no cluster'}) -> leetcode --retest {row['slug']}"
|
||||
f"({row['cluster'] or 'no cluster'}) -> {command}"
|
||||
)
|
||||
return 0
|
||||
|
||||
|
||||
@ -1,40 +1,40 @@
|
||||
{
|
||||
"clusters": {
|
||||
"bit-cancellation": [
|
||||
{"id": 268, "slug": "missing-number"},
|
||||
{"id": 260, "slug": "single-number-iii"},
|
||||
{"id": 169, "slug": "majority-element"},
|
||||
{"id": 137, "slug": "single-number-ii"}
|
||||
{"id": 268, "slug": "missing-number", "title": "Missing Number"},
|
||||
{"id": 260, "slug": "single-number-iii", "title": "Single Number III"},
|
||||
{"id": 169, "slug": "majority-element", "title": "Majority Element"},
|
||||
{"id": 137, "slug": "single-number-ii", "title": "Single Number II"}
|
||||
],
|
||||
"two-pointers": [
|
||||
{"id": 167, "slug": "two-sum-ii-input-array-is-sorted"},
|
||||
{"id": 15, "slug": "3sum"},
|
||||
{"id": 11, "slug": "container-with-most-water"},
|
||||
{"id": 42, "slug": "trapping-rain-water"}
|
||||
{"id": 167, "slug": "two-sum-ii-input-array-is-sorted", "title": "Two Sum II - Input Array Is Sorted"},
|
||||
{"id": 15, "slug": "3sum", "title": "3Sum"},
|
||||
{"id": 11, "slug": "container-with-most-water", "title": "Container With Most Water"},
|
||||
{"id": 42, "slug": "trapping-rain-water", "title": "Trapping Rain Water"}
|
||||
],
|
||||
"sliding-window": [
|
||||
{"id": 3, "slug": "longest-substring-without-repeating-characters"},
|
||||
{"id": 424, "slug": "longest-repeating-character-replacement"},
|
||||
{"id": 567, "slug": "permutation-in-string"},
|
||||
{"id": 76, "slug": "minimum-window-substring"}
|
||||
{"id": 3, "slug": "longest-substring-without-repeating-characters", "title": "Longest Substring Without Repeating Characters"},
|
||||
{"id": 424, "slug": "longest-repeating-character-replacement", "title": "Longest Repeating Character Replacement"},
|
||||
{"id": 567, "slug": "permutation-in-string", "title": "Permutation in String"},
|
||||
{"id": 76, "slug": "minimum-window-substring", "title": "Minimum Window Substring"}
|
||||
],
|
||||
"prefix-sum-hash": [
|
||||
{"id": 560, "slug": "subarray-sum-equals-k"},
|
||||
{"id": 523, "slug": "continuous-subarray-sum"},
|
||||
{"id": 974, "slug": "subarray-sums-divisible-by-k"},
|
||||
{"id": 525, "slug": "contiguous-array"}
|
||||
{"id": 560, "slug": "subarray-sum-equals-k", "title": "Subarray Sum Equals K"},
|
||||
{"id": 523, "slug": "continuous-subarray-sum", "title": "Continuous Subarray Sum"},
|
||||
{"id": 974, "slug": "subarray-sums-divisible-by-k", "title": "Subarray Sums Divisible by K"},
|
||||
{"id": 525, "slug": "contiguous-array", "title": "Contiguous Array"}
|
||||
],
|
||||
"monotonic-stack": [
|
||||
{"id": 739, "slug": "daily-temperatures"},
|
||||
{"id": 496, "slug": "next-greater-element-i"},
|
||||
{"id": 503, "slug": "next-greater-element-ii"},
|
||||
{"id": 84, "slug": "largest-rectangle-in-histogram"}
|
||||
{"id": 739, "slug": "daily-temperatures", "title": "Daily Temperatures"},
|
||||
{"id": 496, "slug": "next-greater-element-i", "title": "Next Greater Element I"},
|
||||
{"id": 503, "slug": "next-greater-element-ii", "title": "Next Greater Element II"},
|
||||
{"id": 84, "slug": "largest-rectangle-in-histogram", "title": "Largest Rectangle in Histogram"}
|
||||
],
|
||||
"intervals": [
|
||||
{"id": 56, "slug": "merge-intervals"},
|
||||
{"id": 57, "slug": "insert-interval"},
|
||||
{"id": 435, "slug": "non-overlapping-intervals"},
|
||||
{"id": 452, "slug": "minimum-number-of-arrows-to-burst-balloons"}
|
||||
{"id": 56, "slug": "merge-intervals", "title": "Merge Intervals"},
|
||||
{"id": 57, "slug": "insert-interval", "title": "Insert Interval"},
|
||||
{"id": 435, "slug": "non-overlapping-intervals", "title": "Non-overlapping Intervals"},
|
||||
{"id": 452, "slug": "minimum-number-of-arrows-to-burst-balloons", "title": "Minimum Number of Arrows to Burst Balloons"}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
78
leetcode_coach/leetcode_coach/how_to.py
Normal file
78
leetcode_coach/leetcode_coach/how_to.py
Normal file
@ -0,0 +1,78 @@
|
||||
"""The commands and the method, written once.
|
||||
|
||||
The status app's Coach tab shows these next to an empty session list, and
|
||||
``DOCS-how-to.md`` (which SKILL.md points at) quotes the same lines verbatim;
|
||||
a test fails when the two differ, which is the point of having one module. Nothing
|
||||
here names a toolbox operation: the Stop hook bounces that before kuhy's
|
||||
first code, and the same words on a screen would teach skipping the search.
|
||||
|
||||
Stdlib only, like ``progress``: the status snapshot imports this every
|
||||
minute on the system python.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, NamedTuple
|
||||
|
||||
|
||||
class Command(NamedTuple):
|
||||
"""One thing kuhy can type, and what it does."""
|
||||
|
||||
command: str
|
||||
meaning: str
|
||||
|
||||
|
||||
class Step(NamedTuple):
|
||||
"""One step of the method."""
|
||||
|
||||
name: str
|
||||
text: str
|
||||
|
||||
|
||||
COMMANDS = (
|
||||
Command(
|
||||
"leetcode --cluster <name>",
|
||||
"Serve the next problem of a concept cluster (a due re-test comes first).",
|
||||
),
|
||||
Command("leetcode <url>", "Coach one problem, from its LeetCode URL or slug."),
|
||||
Command("leetcode due", "List the cold re-tests that are due."),
|
||||
Command(
|
||||
"leetcode report",
|
||||
"One row per session: requirements before code, hint rung, reveal.",
|
||||
),
|
||||
)
|
||||
STEPS = (
|
||||
Step("Constraint", "Turn the hard constraint into a rule for the design."),
|
||||
Step(
|
||||
"Requirements",
|
||||
"List what the operation must do, as properties, never by name.",
|
||||
),
|
||||
Step("Toolbox", "Search the toolbox for an operation with those properties."),
|
||||
Step("Post-mortem", "Say which hint gave which requirement, then make the cards."),
|
||||
)
|
||||
NEXT_NOTE = (
|
||||
"The command skips problems LeetCode already shows as Accepted, "
|
||||
"so it may serve a later one than listed here."
|
||||
)
|
||||
|
||||
|
||||
def cluster_command(name: str) -> str:
|
||||
"""The command that starts cluster ``name``."""
|
||||
return f"leetcode --cluster {name}"
|
||||
|
||||
|
||||
def retest_command(slug: str) -> str:
|
||||
"""The command that starts the cold re-test of ``slug``."""
|
||||
return f"leetcode --retest {slug}"
|
||||
|
||||
|
||||
def block(first_cluster: str | None) -> dict[str, Any]:
|
||||
"""The snapshot's ``coach.how_to``; ``first_cluster`` names the first command."""
|
||||
return {
|
||||
"commands": [c._asdict() for c in COMMANDS],
|
||||
"steps": [s._asdict() for s in STEPS],
|
||||
"next_note": NEXT_NOTE,
|
||||
"first_command": (
|
||||
None if first_cluster is None else cluster_command(first_cluster)
|
||||
),
|
||||
}
|
||||
64
leetcode_coach/leetcode_coach/tests/test_how_to.py
Normal file
64
leetcode_coach/leetcode_coach/tests/test_how_to.py
Normal file
@ -0,0 +1,64 @@
|
||||
"""`how_to` is the one source of the commands and method the status app shows.
|
||||
|
||||
SKILL.md quotes it, the Coach tab renders it, and these tests are what stop
|
||||
the two copies from drifting apart (the reason it is one module).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
import re
|
||||
|
||||
from leetcode_coach import cluster_cli, how_to, report
|
||||
|
||||
DOC = Path(__file__).parents[1] / "DOCS-how-to.md"
|
||||
SKILL = Path(__file__).parents[1] / "SKILL.md"
|
||||
|
||||
|
||||
def test_block_is_commands_steps_note_and_the_first_command() -> None:
|
||||
block = how_to.block("two-pointers")
|
||||
|
||||
assert block["first_command"] == "leetcode --cluster two-pointers"
|
||||
assert len(block["commands"]) == 4
|
||||
assert len(block["steps"]) == 4
|
||||
assert block["next_note"] == how_to.NEXT_NOTE
|
||||
assert all(c["command"] and c["meaning"] for c in block["commands"])
|
||||
assert all(s["name"] and s["text"] for s in block["steps"])
|
||||
|
||||
|
||||
def test_no_first_cluster_means_no_first_command() -> None:
|
||||
assert how_to.block(None)["first_command"] is None
|
||||
|
||||
|
||||
def test_command_templates() -> None:
|
||||
assert how_to.cluster_command("intervals") == "leetcode --cluster intervals"
|
||||
assert how_to.retest_command("3sum") == "leetcode --retest 3sum"
|
||||
|
||||
|
||||
def test_every_listed_command_is_one_the_launcher_dispatches() -> None:
|
||||
known = {*cluster_cli.COMMANDS, report.COMMAND}
|
||||
for entry in how_to.COMMANDS:
|
||||
word = entry.command.split()[1]
|
||||
assert word in known or word == "<url>", entry.command
|
||||
|
||||
|
||||
def test_the_doc_quotes_every_command_and_step_verbatim() -> None:
|
||||
skill = DOC.read_text(encoding="utf-8")
|
||||
|
||||
for entry in how_to.COMMANDS:
|
||||
assert f"`{entry.command}` - {entry.meaning}" in skill
|
||||
for number, step in enumerate(how_to.STEPS, start=1):
|
||||
assert f"{number}. **{step.name}** - {step.text}" in skill
|
||||
assert how_to.NEXT_NOTE in skill
|
||||
|
||||
|
||||
def test_skill_md_points_at_the_doc() -> None:
|
||||
assert "DOCS-how-to.md" in SKILL.read_text(encoding="utf-8")
|
||||
|
||||
|
||||
def test_the_method_never_names_a_toolbox_operation() -> None:
|
||||
# The Stop hook bounces a reply that names one before kuhy's first code;
|
||||
# the same words on the Coach tab would teach kuhy to skip the search.
|
||||
text = " ".join(s.text.casefold() for s in how_to.STEPS)
|
||||
|
||||
assert re.search(r"\b(xor|hash map|sort|stack|two pointers)\b", text) is None
|
||||
Loading…
Reference in New Issue
Block a user