utils/leetcode_coach
Krzysztof kuhy Rudnicki 46ab26fa15
Some checks are pending
dependency freshness / dependencies on newest stable (push) Waiting to run
file length / file-length (push) Waiting to run
leetcode-coach pre-commit / pre-commit (push) Waiting to run
leetcode-coach tests / test (push) Waiting to run
md-naming / markdown naming convention (push) Waiting to run
palette drift / palette (push) Waiting to run
repo-contract / repo contract (push) Waiting to run
shared gate tests / gate unit tests (push) Waiting to run
pylint --fail-under=10 everywhere, and clear what…
Todo: fda71ed1
2026-11-08 20:55:09 +01:00
..
leetcode_coach pylint --fail-under=10 everywhere, and clear what… 2026-11-08 20:55:09 +01:00
.pre-commit-config.yaml pylint --fail-under=10 everywhere, and clear what… 2026-11-08 20:55:09 +01:00
install.sh leetcode_coach: shared package, SQL shape gate 2026-11-02 15:02:38 +01:00
pyproject.toml req_check: accept (system, prompt, schema) asks 2026-11-02 15:17:43 +01:00
README.md req_check: accept (system, prompt, schema) asks 2026-11-02 15:17:43 +01:00
requirements.txt pylint --fail-under=10 everywhere, and clear what… 2026-11-08 20:55:09 +01:00

leetcode_coach

The /leetcode coach as a library: the Socratic method gates, the session log, the grader and card builder, and the relay that serves them. Moved here from leetcode-guard (with its git history) so the tutor server and leetcode-guard share ONE coach. Python 3.14, import leetcode_coach.

Specs stay in ~/src/leetcode-guard/DOCS-coach-{spec,ui-spec,relay}.md.

Install

Who How
leetcode-guard pinned: leetcode-coach @ git+https://github.com/kuhyx/utils@leetcode-coach-v0.1.0#subdirectory=leetcode_coach (its install.sh pip-installs it)
tutor server / development ./install.sh (editable, system python)

Tests: scripts/run_subproject_tests.sh leetcode_coach (100% branch coverage). Gate: pre-commit run --config leetcode_coach/.pre-commit-config.yaml --all-files.

Layers

Stdlib only unless noted. Import only what you need.

Layer Modules
Gates phase, shape, gate_notes, req_check, leak, toolbox, stop_gate, gate_detector
Log + progress session_log, progress, report
Content fetch_problem, toolbox.json, answer_key, clusters.json
Cards toolbox_cards, card_rules, make_cards (needs genanki), anki_connect
Models claude_cli (haiku/sonnet via claude -p), local_backend (Ollama)
Relay relay.*: state machine, grading, run-code, HTTP + RTDB (needs requests, crdt-sync)
Clients leetcode_repl (leetcode command), cluster_cli, local_session, how_to

Library API for the tutor server

Everything keys off the session log: a JSONL file per problem and day, state is derived from it (never held in memory), so any caller and a restart agree.

from leetcode_coach import fetch_problem, phase, session_log, toolbox, leak

Statement. fetch_problem.fetch(slug) -> dict | None (None / isPaidOnly means paste it), fetch_problem.render(q, lang) -> str (markdown with Tags: and Languages available: lines), slug_from(url_or_slug).

Phase gate (req-before-code). gate = phase.PhaseGate(log, toolbox.load_safe(), source=lambda: statement, ask=None)

  • gate.screen(block, *, is_code=None) -> (sent | None, note): None means refuse and show note; otherwise forward sent to the coach and show note if non-empty. Logs req, req_check, code, reveal.
  • ask (gate, req_check.check, looks_like_code, graders) is CLI-style (args, prompt, **kw) (default: haiku, then Ollama) or a structured (system, prompt, schema) -> dict model function such as the tutor's ask_model (answers are labelled via: "model"; any raise falls through to Ollama, then fails open to unverified).
  • gate.refusal_note(), gate.code_seen(), gate.events(), gate.sql(), gate.leaks(reply, user_texts) -> [names] (bounce and regenerate), gate.after_reply(text) -> text without the rung marker (logs rung).
  • phase.req_state(events) -> ReqState(accepted, forced, tries, .open), phase.step2_attempts(events), phase.regex_verdict(block) -> bool | None.
  • 3 incomplete lists force the gate open (req_incomplete: true on code).

Q5, SQL problems. If the statement has the Database tag or offers a SQL dialect (shape.is_sql), req: is skipped: the gate asks for an OUTPUT SHAPE, a message starting shape: that says the columns, the rows (what one row stands for) and the grouping. Judged mechanically (shape.check -> req_check.Check(verdict, missing, "shape"), no model, never fails open), logged as the same req / req_check events with via: "shape", forced after 3 tries like req:. A req: message on a SQL problem counts as the shape. An unfetched statement (just a slug) is not SQL: the stricter req: gate applies. Algorithm problems are unchanged. The relay state's session.kind is "sql" or "algorithm".

Leaks and code. leak.leaked_names(reply, allowed, box) (toolbox names too early), stop_gate.verdict(raw, reply), gate_detector.offending_snippets.

Log. session_log.log_path(problem), append(path, event, **fields), read(path), current(events), has_code(path). Events: session_start, req, req_check, code, rung, reveal, grade, chat. The field set is a FIXED contract read by leetcode_guard._coach_log: add a field only together with that reader.

Progress. progress.history(slug), due_retests() (cold re-tests at +3 and +10 days), cluster_progress(), next_new(...), cluster_of(slug).

Toolbox + cards. toolbox.load_safe() -> Toolbox (.entries, .properties), toolbox_cards.cards_for(names) -> [Card] (property list -> operation), toolbox_cards.append_entry(...) (new entry, after kuhy confirms), make_cards.deliver(...) (Anki deck "LeetCode", .apkg fallback).

Relay (the five-step coach). relay.coach.Coach(Deps(...)): handle(Request) -> dict for the seven kinds in relay.protocol.KINDS (session_start, session_end, chat, grade_step, hint, reveal, run_code), state(), chat_after(id), tick() (Accepted probe). relay.app.Relay(Deps()) adds loopback HTTP (start(), port) and the RTDB phone link (connect_phone(make_rtdb)). Every dependency is a Deps field: ask, local_chat, fetch, probe, run_code, answers, box, state_dir, cards_dry_run, clock_ms. The wire is DOCS-coach-relay.md.

What it needs from leetcode-guard

Three read-only calls, resolved on demand in guard_bridge (a consumer without leetcode-guard imports everything else; these raise GuardUnavailableError, never a silent "nothing solved"):

Call Used by
solved_slugs(slugs) cluster_cli (skip already-Accepted problems)
recent_accepted() relay step 4 probe (AcceptedProbe, at most 1/min)
phone_id_token(app, timeout_seconds=) relay.__main__ RTDB sign-in

No coach module imports leetcode_guard statically and no route can reach the gate's ledger, credits or unlocks (tests/test_guard_boundary.py reads the bridge's strings against an allow-list). Credit stays leetcode-guard's job.

Environment

Variable Meaning
LEETCODE_COACH_SESSIONS_DIR session logs (default ~/.local/share/leetcode-coach/sessions)
LEETCODE_COACH_LOG log the REPL exported for the Stop hook
LEETCODE_COACH_CLUSTER cluster tag for new logs
LEETCODE_CLAUDE_BIN the claude binary (tests: a fake)
LEETCODE_COACH_RELAY_DIR / _PORT / _RTDB_URL relay state dir, port (default 8777), RTDB override
LEETCODE_COACH_CARDS_DRY_RUN 1: build cards, push nothing

Commands (the live units use these paths)

  • python3 -m leetcode_coach.relay: the relay (leetcode-coach-relay.service)
  • python3 -P -m leetcode_coach.leetcode_repl <url> (the leetcode command)
  • python3 -m leetcode_coach.stop_gate: the Stop hook (shim in hook_shims/)
  • python3 -m leetcode_coach.toolbox_cards --help, make_cards, report, cluster_cli, fetch_problem: the skill's scripts
  • The skill body is leetcode_coach/SKILL.md; the skill dir ~/.claude/skills/leetcode points at leetcode_coach/leetcode_coach/.