Read AGENTS.md (conventions, commands) first, then content/AUTHORING.md before touching lessons.
- Curriculum complete: 18 units, 266 lessons (u01-u18), strict
npm run contentpasses,check-contenthas 0 errors / 0 warnings,verify-contentruns every solution and invalid sample through the real checker with 0 problems. - App: lessons workspace (bento: lesson / CodeMirror editor / terminal), multi-file projects with file tabs, review sessions, playground, mastery map, achievements, settings, XP / levels / streaks, persistence with backups, mobile tabs, light and dark themes.
- Quality gates:
npm run typecheck,npm run lint,npm test(55 Vitest tests) all pass. Playwright:lesson-flow,features, andwalkthrough(plays lessons with their reference solutions; multi-file aware). - Walkthroughs (final build, after the last app change): every lesson of all 18 units was played through the real UI (Chromium, JSPI available) with its reference solution and passed: u01-u09 134/134, u10-u11 + u17-u18 47/47, u12 8/8, u13-u16 77/77 (all 266). The
lesson-flowandfeaturesspecs (10 tests) pass too.
| Area | Path |
|---|---|
| Python runner, checker, AST features, error shaping (single implementation) | python/hatch/ |
Worker that hosts Pyodide (module worker, Atomics-blocking input()) |
public/hatch-worker.js, src/runtime/ |
| Curriculum schema, graph, validators, execution verifier | src/curriculum/ |
| Curriculum data | content/ (spec: content/AUTHORING.md) |
| Pure engine: XP ledger, mastery, review, achievements | src/engine/ (tested in tests/) |
Store and persistence (hatch.v1, hatch.drafts.v1) |
src/store/ |
| Lesson/review orchestrator | src/components/lesson/workspace.tsx, step-views.tsx |
- Blue-accent, larger, sturdier UI (light = sky tint with white cards, dark = deep navy). Tokens live at the top of
src/app/globals.css:--accent(solid blue for buttons and text, 5.4:1 on white),--accent-bright(#1cb0f6, fills only: progress bars, active step),--xp-bright(gold fills), green only for success. Nunito (rounded) replaced Geist/Fraunces; JetBrains Mono for code. - Root font size is 106.25% (mobile) and 112.5% (>= 768px); Tailwind
text-xs/sm/basewere enlarged in@themeand the oldtext-[0.6-0.7rem]micro sizes were bumped to >= 0.8rem. Buttons are chunky (2px border, 4px bottom edge, 3rem tall), panels have 2px borders and a flat ledge shadow, progress bars are 0.75-1rem thick. Default editor font size is now 17 (existing saved preferences keep their value). - Contrast for the small coloured labels was checked numerically (all >= 4.5:1 in light, >= 5.8:1 in dark). The Duolingo-style reference tokens were adapted, not copied: white text on the bright #1cb0f6 would fail contrast, so solid buttons use the darker #0a6eb4.
- Checks are behavioural. AST rules apply to the entry file only; for multi-file projects the u11/u18 lessons use hidden fixtures (
probe.py,mutants.py,h.py) that run the learner's own tests against broken implementations or parse the modules withast. - Async lessons (u17) check the stdout of the learner's complete program. Pages in the fetcher project are simulated (
SITEdict +asyncio.sleep): the lesson says so. Delays differ by at least 30 ms to keep the output order deterministic. - "Check" button = run the hidden check only; "Run" = run in the terminal, then check (also when the program errors, so steps that expect an error can pass).
- Preferences: the old
soundtoggle and the "Full motion" option were removed because they did nothing. Old saved values are migrated by the zod schema (persistence.ts, covered by a test). - Lint ignores the vendored
public/pyodide/**andsrc/generated/**.
- Saved progress is a JSON document (
hatch-progress,schemaVersioninsrc/engine/progress.ts). Reading goes through zod insrc/store/persistence.ts; to change the shape, bumpSCHEMA_VERSIONand add a case tomigrate(). A document from a newer app is never overwritten (the learner sees a notice), and a damaged one is copied to a backup key before anything is reset. - Every lesson has an integer
version(YAML header, default 1) and progress records the version a learner completed. Bump it when a lesson changes in a way that should not silently count as the old one;course.versionincontent/course.yamlis the course-level counterpart. - Drafts (
hatch.drafts.v1) are per step, pruned oldest first when over budget.
- Browsers without JSPI (non-Chromium): live runs work through a rewrite of module-level
asyncio.run, but hidden checks that callasyncio.runare not supported there. u17 only relies on stdout checks, but this has not been exercised in Firefox/Safari. - Walkthrough runs:
HATCH_E2E_UNITS=u10,u11 npx playwright test walkthrough(about 3 minutes per 25 lessons). Make sure no stalenext devis on :2034 first (a hung one makes every test time out inpage.goto). A failing lesson usually means a solution that passes the Node checker but behaves differently in Pyodide. Two real examples found this way: JSPI was never enabled becausecan_run_sync()was probed outsidecallPromising, and under JSPIasyncio.runturned4.0into4(fixed by boxing the result inbridge.fix_asyncio_run); and checklist ticks leaked between checklists of one lesson because they shared the scratchpad key. - Curriculum review: content was validated mechanically (prerequisite leaks, terminology, feature ownership, XP). A human read-through for tone, pacing and difficulty spikes, especially units 12-17, is still the best next investment.
- Reasonable future features, none started: export/import of progress as a file, per-unit "skip test" placement quiz (deliberately absent: the app picks the order), keyboard shortcut cheat-sheet.
test-results/andplaywright-report/are generated and safe to delete.- The repository has a single "Initial commit" and nothing committed since; no remote is configured.