4 Commits

Author SHA1 Message Date
MechaCat02
57fd6e5087 feat(learn): the reference screens as the reworked artifact has them
읽기 연습. Each mode opens with what the round is and a Start button; the
round is timed, and a finished one reports accuracy and time, keeps the
best per mode and says when it was beaten. In sound mode the written
spelling is among the options — reading it as written is the habit the
drill exists to break, so it is the distractor that matters. Answers stay
marked 450ms when right, 900ms when a rule is shown, 1400ms when wrong.
The best rounds sync (trainer.drill), merged mode by mode: the higher
accuracy, then the faster time.

문법. An All chip, a count on every category, 반말 selected first — it is
what manhwa speech is made of — and each row names its category, so the
list still reads under All.

활용 연습. Words come in random order, not the pool's; the running score
comes back after a reload; a wrong answer shows what was typed beside what
it should have been, then the rule. The pool adds the roadmap's own verbs
and adjectives to the deck's. A form lib/conjugation.js cannot build no
longer leaves the Check button stuck.

문장. The artifact's lesson on the ending word and the 서술어 legend, a count
on each level, and every sentence's place in review — as far along as its
least-known chunk, since the chunks are the cards.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-16 22:12:05 +02:00
MechaCat02
9ab5aba6b3 feat(sync): protocol 2 — hydration, a counter not a clock, no silent shrinking
The reworked bundle's PORT.md makes row-level last-write-wins conditional
on three gates, each learned by losing real data. The port's sync broke
all three, and had four more ways to lose or stall work. Both ends change,
so this is one protocol version, refused by the other side if mismatched.

Gate 1, hydration. A device now pulls every page the server holds before
it may push anything; it used to push first. The 14 Sep laptop — a
week-old copy re-stamped at boot and pushed over a week of phone work —
is now a test, and the phone's week survives it. Boot writes nothing
syncable either: the lesson opens with the app's own words (the artifact's
seeded turn) and waits for Start, instead of stamping a reply and a
progress edit before a server can even be configured.

Gate 2, a counter. Every row remembers the change_seq it last agreed with
(base_seq). The server applies a write only if that still matches —
compare-and-swap under an advisory lock — and otherwise returns its copy
as a conflict. No clock is compared anywhere: a device an hour fast used
to win every conflict for an hour, and a slow one's newer edit was
silently dropped with HTTP 200. dirty and rev replace the timestamp
watermark, which lost edits whenever a clock moved backwards.

Gate 3, no silent shrinking. A conflict is settled by what each copy
holds (sync/resolve.ts): more reviews, more evidence, a finished unit, the
further roadmap position, the union of learned grammar. A deliberate
shrink is explicit: a reset or a cleared lesson raises a marker every
device obeys, including its own unsynced edits, so a reset is not undone
by a device that had not heard of it. Trimming the transcript is local
and tombstones nothing — it used to delete the other device's turns.

Also fixed on the way:
  · keys travel as JSON arrays — a space in 몇 명 used to stop every
    device's pull at that row, permanently;
  · pulls take a shared lock against pushes, so a change_seq committed
    out of order can no longer be skipped;
  · study_log and peek are per device and summed, so two devices' reviews
    of one day both count;
  · each user's data has an epoch; a server that lost it is detected,
    and the device re-hydrates and offers its data back;
  · a protocol-1 client is refused with 426 rather than half-understood.

Migration 9 adds the columns, re-keys the counters and tombstones; the
server drops protocol-1 rows once (none were deployed). Verified: 14
two-device scenarios against a real Postgres, and two browser profiles
syncing a lesson through the UI.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-16 20:28:35 +02:00
MechaCat02
bf9b5950da feat(db): a roadmap without 'now' rows, chat ids two devices can share, the learner-model tables
Three storage changes the reworked app needs, as migration 8.

Progress. "Which unit is current" was a 'now' state on each unit's row. Two
devices that advanced could leave two of them, and leaving a finished unit
through the roadmap panel wrote it back to 'todo' — goToUnit un-finished
work. Where he is now lives in one place, meta road.unit; a unit's row
records only what is true of that unit: done, confidence, and room for the
answer count and the tutor's note that earned progress needs. The migration
carries the most recently written 'now' row across with its own stamp.

Chat. Turn ids were INTEGER PRIMARY KEY — max+1 on whichever device wrote
them, restarting at 1 after a clear — so two devices continuing a lesson
both wrote turn 201 and sync treated two different turns as one row. Ids
are UUIDv7 now, and the transcript is ordered by (created_at, id). Existing
turns become legacy:<device>:<n>, zero-padded so turns sharing a timestamp
keep the order they were written in; their tombstones are renamed with them.

The learner model gets its tables: evidence (lib/srs.js's record, plus the
rounds of the first and last CORRECT answer, which PORT.md measures and lib
does not), confusion, and phase_ledger for the 다지기 checklist, with a
confirmed flag so putting an item back is an edit rather than a delete.

A roadmap reset now clears the ledger and road.*; a full wipe also clears
evidence, confusions and learner.* — the artifact's wipe left its round
counter and confusion list behind. What a learner knows about words
survives a roadmap-only reset, as it should.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-16 20:10:58 +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