mirror of
https://github.com/kuhyx/utils.git
synced 2026-11-11 18:34:01 +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
|
withhold a reply; the TUI has already streamed it). `leetcode --local` runs
|
||||||
offline on Ollama; everything above `## Model and client` is its system
|
offline on Ollama; everything above `## Model and client` is its system
|
||||||
prompt. Clients, gates and the session log: `DOCS-clients.md` here.
|
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 os
|
||||||
import sys
|
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._live_solved import check_solved
|
||||||
from leetcode_guard._settings import build_client
|
from leetcode_guard._settings import build_client
|
||||||
|
|
||||||
@ -66,9 +66,10 @@ def _due() -> int:
|
|||||||
print("no re-tests due")
|
print("no re-tests due")
|
||||||
return 0
|
return 0
|
||||||
for row in rows:
|
for row in rows:
|
||||||
|
command = how_to.retest_command(row["slug"])
|
||||||
print(
|
print(
|
||||||
f"{row['due']} {row['slug']} re-test {row['stage']}/{STAGES} "
|
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
|
return 0
|
||||||
|
|
||||||
|
|||||||
@ -1,40 +1,40 @@
|
|||||||
{
|
{
|
||||||
"clusters": {
|
"clusters": {
|
||||||
"bit-cancellation": [
|
"bit-cancellation": [
|
||||||
{"id": 268, "slug": "missing-number"},
|
{"id": 268, "slug": "missing-number", "title": "Missing Number"},
|
||||||
{"id": 260, "slug": "single-number-iii"},
|
{"id": 260, "slug": "single-number-iii", "title": "Single Number III"},
|
||||||
{"id": 169, "slug": "majority-element"},
|
{"id": 169, "slug": "majority-element", "title": "Majority Element"},
|
||||||
{"id": 137, "slug": "single-number-ii"}
|
{"id": 137, "slug": "single-number-ii", "title": "Single Number II"}
|
||||||
],
|
],
|
||||||
"two-pointers": [
|
"two-pointers": [
|
||||||
{"id": 167, "slug": "two-sum-ii-input-array-is-sorted"},
|
{"id": 167, "slug": "two-sum-ii-input-array-is-sorted", "title": "Two Sum II - Input Array Is Sorted"},
|
||||||
{"id": 15, "slug": "3sum"},
|
{"id": 15, "slug": "3sum", "title": "3Sum"},
|
||||||
{"id": 11, "slug": "container-with-most-water"},
|
{"id": 11, "slug": "container-with-most-water", "title": "Container With Most Water"},
|
||||||
{"id": 42, "slug": "trapping-rain-water"}
|
{"id": 42, "slug": "trapping-rain-water", "title": "Trapping Rain Water"}
|
||||||
],
|
],
|
||||||
"sliding-window": [
|
"sliding-window": [
|
||||||
{"id": 3, "slug": "longest-substring-without-repeating-characters"},
|
{"id": 3, "slug": "longest-substring-without-repeating-characters", "title": "Longest Substring Without Repeating Characters"},
|
||||||
{"id": 424, "slug": "longest-repeating-character-replacement"},
|
{"id": 424, "slug": "longest-repeating-character-replacement", "title": "Longest Repeating Character Replacement"},
|
||||||
{"id": 567, "slug": "permutation-in-string"},
|
{"id": 567, "slug": "permutation-in-string", "title": "Permutation in String"},
|
||||||
{"id": 76, "slug": "minimum-window-substring"}
|
{"id": 76, "slug": "minimum-window-substring", "title": "Minimum Window Substring"}
|
||||||
],
|
],
|
||||||
"prefix-sum-hash": [
|
"prefix-sum-hash": [
|
||||||
{"id": 560, "slug": "subarray-sum-equals-k"},
|
{"id": 560, "slug": "subarray-sum-equals-k", "title": "Subarray Sum Equals K"},
|
||||||
{"id": 523, "slug": "continuous-subarray-sum"},
|
{"id": 523, "slug": "continuous-subarray-sum", "title": "Continuous Subarray Sum"},
|
||||||
{"id": 974, "slug": "subarray-sums-divisible-by-k"},
|
{"id": 974, "slug": "subarray-sums-divisible-by-k", "title": "Subarray Sums Divisible by K"},
|
||||||
{"id": 525, "slug": "contiguous-array"}
|
{"id": 525, "slug": "contiguous-array", "title": "Contiguous Array"}
|
||||||
],
|
],
|
||||||
"monotonic-stack": [
|
"monotonic-stack": [
|
||||||
{"id": 739, "slug": "daily-temperatures"},
|
{"id": 739, "slug": "daily-temperatures", "title": "Daily Temperatures"},
|
||||||
{"id": 496, "slug": "next-greater-element-i"},
|
{"id": 496, "slug": "next-greater-element-i", "title": "Next Greater Element I"},
|
||||||
{"id": 503, "slug": "next-greater-element-ii"},
|
{"id": 503, "slug": "next-greater-element-ii", "title": "Next Greater Element II"},
|
||||||
{"id": 84, "slug": "largest-rectangle-in-histogram"}
|
{"id": 84, "slug": "largest-rectangle-in-histogram", "title": "Largest Rectangle in Histogram"}
|
||||||
],
|
],
|
||||||
"intervals": [
|
"intervals": [
|
||||||
{"id": 56, "slug": "merge-intervals"},
|
{"id": 56, "slug": "merge-intervals", "title": "Merge Intervals"},
|
||||||
{"id": 57, "slug": "insert-interval"},
|
{"id": 57, "slug": "insert-interval", "title": "Insert Interval"},
|
||||||
{"id": 435, "slug": "non-overlapping-intervals"},
|
{"id": 435, "slug": "non-overlapping-intervals", "title": "Non-overlapping Intervals"},
|
||||||
{"id": 452, "slug": "minimum-number-of-arrows-to-burst-balloons"}
|
{"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