diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f601bf9..6c26a43 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,7 +14,7 @@ jobs: # The curriculum gate comes first, before anything else can run. # validate.mjs reads only data/ and lib/ and exits non-zero on a - # blocking failure. Baseline: PASS — 0 blocking, 167 advisory. + # blocking failure. Baseline: PASS — 0 blocking, 0 advisory. - name: Curriculum validation run: node validate.mjs @@ -28,7 +28,35 @@ jobs: - name: Typecheck run: npm run typecheck + # The sync round-trip needs a real Postgres and a running server, so a + # fake would not exercise the change_seq trigger, the last-write-wins + # upsert, or cursor paging — which is where sync actually goes wrong. + - name: Start Postgres + run: | + docker run -d --name hankan-pg \ + -e POSTGRES_PASSWORD=test -e POSTGRES_DB=hankan \ + -p 55432:5432 postgres:16-alpine + for i in $(seq 1 30); do + docker exec hankan-pg pg_isready -U postgres -d hankan && break + sleep 1 + done + + - name: Start the sync server + env: + DATABASE_URL: postgres://postgres:test@localhost:55432/hankan + HANKAN_TOKEN: test-token + HANKAN_TEST_MODE: "1" + PORT: "8788" + run: | + node --experimental-strip-types server/src/main.ts & + for i in $(seq 1 30); do + curl -sf http://localhost:8788/health && break + sleep 1 + done + - name: Tests + env: + HANKAN_TEST_SERVER: http://localhost:8788 run: npm test # Every word in every unit's words[] must resolve in lemma or surface. diff --git a/README.md b/README.md index 7ba0e9f..69e92c0 100644 --- a/README.md +++ b/README.md @@ -18,11 +18,13 @@ types/ TypeScript declarations for lib/ and shar tools/dict/ the dictionary build pipeline app/ Vite + React + TypeScript, and the Capacitor shell src/db/ one storage interface, two SQLite drivers - src/domain/ the gate, the lexicon, SRS, the stub tutor + src/domain/ the gate, the lexicon, SRS, the tutor clients + src/sync/ push/pull against the Pi src/ui/ six tabs, the review overlay, the 한글 keyboard public/dict/ generated dictionary — committed, shipped -test/ golden tests for lib/, driver conformance, domain logic -server/ placeholder; PORT.md steps 4-6 +server/ sync + the tutor endpoint (optional) +tools/icons/ the 한칸 mark, generated +test/ lib goldens, driver conformance, domain, sync, SSE ``` `data/`, `lib/`, `prompt/` and `validate.mjs` are byte-identical to the export @@ -144,11 +146,32 @@ that file. CI runs them in that order, `validate.mjs` first. +## Sync and the tutor — optional, and genuinely optional + +The app is complete without a server: its own SQLite, the shipped dictionary, +and a local stand-in tutor. Pointing it at a Pi adds two things — the real +선생님, and syncing between devices. Everything degrades to the offline +behaviour when the server is unreachable, and a sync failure is recorded in +settings rather than surfaced as an interruption. + +Setup, the Caddy config, and how sync resolves conflicts: [server/README.md](server/README.md). + +Three details worth knowing here: + +- **Seeded rows carry `updated_at = 0`**, so a fresh device can neither push + its empty defaults nor win a conflict with them. The artifact's clobbering + bug is unrepresentable rather than merely avoided, and + `test/sync/roundtrip.test.ts` asserts it against a real Postgres. +- **An allowlist decides what leaves the device.** `meta` mixes preferences + with per-install bookkeeping; `dict.loadedBands` crossing the wire would + tell a phone it holds rows it never downloaded. +- **The system prompt is cached.** It is ~12k characters of gate, identical + for as long as the learner stays in one unit, so every turn after the first + reads it at a fraction of the input price. + ## Not in this pass -No sync layer, no tutor server endpoint, no Play Store packaging — PORT.md steps -4–6. The schema carries `updated_at` on every syncable row and the dictionary is -fetched by URL, so both land without rework. +Android beyond the existing Capacitor scaffold, and Play Store packaging. ## Known limitation