Commit Graph

4 Commits

Author SHA1 Message Date
MechaCat02
e72b77d6c2 chore: take in the 16 Sep bundle — lib, curriculum v5, prompt, gate audit
The artifact was reworked after real incidents: a week of lost data, a
student taught out of order, and spelling diagnoses the model invented.
This takes the new export in verbatim; the port catches up in the
commits that follow.

Copied byte-identical from the bundle:
  lib/        lexicon.js and sync.js are new; gate.js gains enforcement,
              hangul.js letter-level marking, srs.js recall evidence,
              conjugation.js deconjugate(); blocks.js now takes the last
              block, closes gloss at "=", and parses recall, ::result and
              ::confirmed
  data/       curriculum.json v5 — six 다지기 phase reviews; the 371
              roadmap words are unchanged and no band moves
  prompt/     English-only rule, recall, LETTER-LEVEL CHECK, marking
  audit-gate.mjs, run-checks.sh, fixtures/  — the word gate measured
              against 54 real tutor messages

CI runs run-checks.sh in place of validate.mjs alone, and `npm run check`
gains the audit. Baselines: validate PASS 0/0; audit 7 of 41 and 2 of 13.

types/lib/ declares the new API, and test/lib/ pins it: letterCheck on
the prompt's own 짧다/빫다 case, deconjugation, the roadmap-first order
that keeps 마셔 out of Phase 1, sync's three gates, and recall evidence —
including the two ways lib's evidence is looser than PORT.md, pinned as
they are so the call site that tightens them is visibly needed.

TaskHost gains a plain recall renderer so the tree typechecks against the
wider Task union.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-16 19:49:52 +02:00
MechaCat02
f447f881c0 feat(sync): tombstones, the wire protocol, and the client loop
Row-level last-write-wins on updated_at, cursor-based on a server-assigned
change_seq. The schema was built for this in step 1, so the work here is
the three things it did not yet have.

Tombstones (migration 4). Row-level sync cannot express a delete: with the
row gone there is nothing to compare timestamps against, so the other
device pushes its still-live copy back and the row silently returns. Every
delete path now writes a tombstone inside the same transaction.

The wire format lives in shared/sync-protocol.mjs and is imported by both
sides, so there is one definition rather than two that drift. It carries
the syncable-meta allowlist, which is the load-bearing part: meta mixes the
learner's preferences with bookkeeping that describes one install, and
replicating dict.loadedBands would tell a phone that had loaded bands 0-2
it holds every row the desktop has — the word rail would then fail to find
words it believes are present.

The sync loop pushes first, then pages the pull. Two details it would be
easy to get wrong, both commented at their site:

- The pull cursor advances only as rows are applied, never from the push
  response. The server's newest change_seq includes rows this device has
  not seen; adopting it skips them permanently, and nothing ever asks for
  that range again.
- Pulled rows advance sync.pushedAt too, bounded by the instant the sync
  started. Otherwise they look like local edits and get pushed straight
  back, and an edit made during the sync is not swept up with them.

Seeded rows carry updated_at = 0, so a fresh device is never dirty and can
never win a conflict — the artifact's clobbering bug stays unrepresentable.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 19:57:32 +02:00
MechaCat02
7bd8507909 feat(dict): build pipeline, grammar lexicon, and the shipped band files
Closes REVIEW.md §2. 167 of 371 roadmap words had no lexicon entry, so the
word rail silently showed nothing. Now:

  Roadmap words: 371/371 resolve
  Spiral targets: 35/35 resolve
  Deck words: 386/386 resolve

and npm run dict:assert makes it a blocking build failure, not a silent
empty rail.

No runtime morphological analyser ships. lib/conjugation.js surfaceForms()
runs at BUILD time over every verb and adjective, so looking up a conjugated
form is an index hit on the surface table.

The frequency join had to be inverted. A subtitle frequency list holds
surface forms; a dictionary holds lemmas whose -다 citation form barely
occurs in running text, so joining on headword gives verbs a frequency of
roughly zero. Expanding each lemma into the forms it plausibly takes and
summing recovers 하다 from 118 to 89,041. Forms claimed by more than one
lemma are dropped rather than split, so homographs don't inherit each
other's mass. Those expansions score frequency only — the surface table
itself stays strictly surfaceForms() output plus the headword.

Bands are one per curriculum phase. Phase 1 admits no frequency band at
all: during the writing-system phase every word must be phonologically
legal for the unit reached, and a rank ceiling would hand the learner a
겹받침 during unit 1.4. shared/phonology.mjs lifts validate.mjs's own
feature ladder to enforce that; it agrees with the validator on all 371
words.

Sources are chosen automatically — KRDICT when vendored, otherwise the
kaikki.org extract. KRDICT's download is a JS form behind anti-bot
protection, so it cannot be fetched by CI; the derived band files are
committed instead, which the app needs offline regardless. Attribution and
the share-alike terms are in NOTICE.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 19:13:15 +02:00
MechaCat02
966043ab3f feat(lib): type declarations and golden tests for the exported modules
lib/ ships unchanged, so its types live in types/lib/ and are wired up by a
tsconfig path mapping. Adding types costs nothing; reimplementing the logic
would cost the two things that make this port possible.

The tests exist so a later refactor cannot silently drift them:

  hangul       the nine Composer cases named in the export README —
               먹어 · 왔어 · 읽어 · 괜찮아 · 값 · 의사 · 뭐야 and backspace
  conjugation  all seven irregular classes, and every form in
               IRREGULAR_FORMS reachable through haeche()
  srs          the SM-2 transitions, ease and interval clamps
  blocks       parse → answerText round-trip for all four task types

101 tests.

One is a pinned defect rather than a guarantee. blocks.parse() never closes
a ::gloss block on its "=" line, so a multi-sentence gloss — which the tutor
prompt explicitly invites — collapses into one run-on line keeping only the
last translation. lib/ ships unchanged, so the test records the real
behaviour and the app works around it at the call site.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 19:12:19 +02:00