Three unit-1.1 lessons with gpt-oss-20b through LM Studio and the server —
the first real model on the reworked turn. The letter-level check went out
right, and the +25 clamp held: a reported 80 on the first answer was stored
as 25. What failed was how the model wrote its blocks, a different way each
session. All three transcripts are in test/fixtures/, verbatim, and each
failure below is a test against them.
Marks lost. The prompt asks for `여덟 | wrong | 여덜`. The first session wrote
`we | wrong | 우라 → 우리`, English prompt first; the second wrote no ::result
at all and marked only in prose, `✗ 나 | I (humble) → 저`. evidence.ts keys on
the first field of a ::result row, so nothing was ever recorded — no
evidence, no schedule, no confusions, and a 다지기 review that could never
close. The artifact would have lost them the same way. domain/marking.ts
attaches each mark to its word only where that is unambiguous: one Korean
word first, or through a prompt of the exercise he answered, read via that
exercise's ::words as the letter check reads it. With no ::result block the
✓/✗ lines are read on the same terms, so a mark can never name a word the
exercise did not ask for; a mark on a whole sentence is still dropped. What
he mistook a word for is taken from what he actually wrote whenever the mark
itself gives no other word — the third session put the right answer there.
Its third session, marked through all of this: 20 evidence rows, 20 cards.
Progress on requests. The prompt allows marks, ::confirmed and ::progress
only in reply to an answer. The model wrote ::progress on every message, and
three requests for a new exercise took the unit from 50% to 80% with nothing
answered. A reply to anything but an answer now changes none of them.
Feedback swallowed. The model closed no blocks, so lib read what followed
each one as rows: "your score is about 5%" became a result row the student
never saw, and a "---" became a recall item he was asked to write in 한글.
Another session fenced every block in ```. gloss.ts now decides every
block's extent from the raw text — at "::", the next block, a rule or fence
line, a blank line with no row after it, or for the piped blocks the first
line without a "|" — and hands lib the blocks properly closed. The gate
audit, now run through the parser the lesson uses, still flags 7 and 2.
Answers given away. Translate rows came with their meanings ("나 | I") and
recall hints were the answers ("two | 이"). A translate row keeps only its
Korean line, and a recall hint that is the expected word, or any word the
message declares, is dropped.
Also: the spelling a recall prompt expects now keeps its qualifiers. With 나
"I, me (casual)" and 저 "I, me (humble)" in one list, "I (humble)" matched
나: no letter check was sent, and the mark for 저 was filed under 나.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
"The client enforces; the prompt only explains." Every rule the artifact's
tutor was merely asked to follow, it broke: it certified words on one
correct answer, scored a unit before anything was answered, used a word
from three phases ahead, answered in Korean, and invented spelling
diagnoses. The reworked app fixed each by making the client refuse. This
ports those refusals; domain/turn.ts holds the turn, testable without React.
The gate. A reply is scanned before he sees it — the side of the exercise
he must decode, through the one resolver, and its prose for Korean. A
refused draft is never stored, shown or applied: the tutor is asked again
and told exactly why. After two retries the reply is shown with its words
flagged, and the next turn names them. (The artifact's follow-up told the
tutor it could declare such a word in ::words; that contradicts the gate
and is left out.)
Marking. ::result feeds recall evidence per word. lib/srs.js is looser
than PORT.md, so the call site tightens it: one outcome per word per round,
and "learned" also needs five rounds between the first and last CORRECT
answer — lib alone counted a wrong answer as the start of the span. A
lookup is never recall. What he mistook a word for is kept. The schedule
takes at most one good grade a day from marking; in the artifact five good
rounds in one afternoon made a word "secure" by interval alone.
Phase reviews. The client holds the 다지기 checklist — each unit's rule and
every word the phase introduced, 132 items for Phase 1 — worked in batches
of ten. ::confirmed ticks a rule on the tutor's word but a word only on
evidence; "-item" puts one back; anything off the list is ignored.
Progress is earned: ignored until the unit has an answer, +25 at most per
message, a fall honoured in full, and the next unit only at 85% with three
answers — plus, in a review, nothing open. advanceUnit() enforces it too,
not only the banner.
The prompt gains a per-round tail after the shipped prompt — the practice
set (scored on the evidence, round-robin by word class, each word with the
words one letter away), the checklist, retry notes — sent as a second,
uncached system block so the stable prefix still caches.
Also: recall answers carry the letter-level jamo comparison (kept out of
his own bubble, since it is written to the model); match chips are keyed by
pair index, the bug PORT.md names; and the stand-in tutor exercises every
path offline — recall, ::result, ::confirmed, progress only after answers.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>