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>
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
/* The four exercise types, rendered as real interface.
|
||||
/* The five exercise types, rendered as real interface.
|
||||
|
||||
State lives HERE, in the component, keyed by the turn it belongs to. The
|
||||
artifact rebuilt taskState inside its full-page re-render, so anything
|
||||
@@ -14,6 +14,7 @@ import type {
|
||||
BuildTask,
|
||||
ChoiceTask,
|
||||
MatchTask,
|
||||
RecallTask,
|
||||
Task,
|
||||
TranslateTask,
|
||||
} from "@lib/blocks.js";
|
||||
@@ -35,6 +36,7 @@ function shuffle<T>(items: T[], seed: number): T[] {
|
||||
|
||||
const LABEL: Record<Task["type"], string> = {
|
||||
translate: "type what each line means",
|
||||
recall: "write it in 한글",
|
||||
match: "tap a Korean word, then its meaning",
|
||||
build: "tap or drag the words into order",
|
||||
choice: "one pick per line",
|
||||
@@ -92,6 +94,53 @@ function Translate({ task, answers, setAnswers, disabled, onEnter }: {
|
||||
);
|
||||
}
|
||||
|
||||
/* ── recall ──────────────────────────────────────────────────────── */
|
||||
|
||||
/* English prompt; he writes the 한글. The keyboard wiring and the
|
||||
letter-level check arrive with the answer mode — this renders the task
|
||||
so it can be answered at all. */
|
||||
function Recall({ task, answers, setAnswers, disabled, onEnter }: {
|
||||
task: RecallTask;
|
||||
answers: string[];
|
||||
setAnswers: (a: string[]) => void;
|
||||
disabled: boolean;
|
||||
onEnter: () => void;
|
||||
}) {
|
||||
return (
|
||||
<div className="ti">
|
||||
{task.items.map((it, i) => (
|
||||
<div className="ti-row recall" key={i}>
|
||||
<span className="q">
|
||||
{it.q}
|
||||
{it.hint && <span className="rc-hint"> · {it.hint}</span>}
|
||||
</span>
|
||||
<input
|
||||
type="text"
|
||||
className="ko"
|
||||
value={answers[i] ?? ""}
|
||||
disabled={disabled}
|
||||
placeholder="한국어로…"
|
||||
autoComplete="off"
|
||||
spellCheck={false}
|
||||
aria-label={`Write ${it.q} in Korean`}
|
||||
onChange={(e) => {
|
||||
const next = [...answers];
|
||||
next[i] = e.target.value;
|
||||
setAnswers(next);
|
||||
}}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault();
|
||||
onEnter();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ── match ───────────────────────────────────────────────────────── */
|
||||
|
||||
interface Pair {
|
||||
@@ -310,6 +359,7 @@ export function TaskHost({ task, turnId, lookups, onSubmit, onSkip, disabled = f
|
||||
const filled = (() => {
|
||||
switch (task.type) {
|
||||
case "translate":
|
||||
case "recall":
|
||||
return { n: answers.filter((a) => a?.trim()).length, of: task.items.length };
|
||||
case "match":
|
||||
return { n: done.length, of: task.pairs.length };
|
||||
@@ -326,6 +376,8 @@ export function TaskHost({ task, turnId, lookups, onSubmit, onSkip, disabled = f
|
||||
switch (task.type) {
|
||||
case "translate":
|
||||
return answerText(task, answers, lookups);
|
||||
case "recall":
|
||||
return answerText(task, answers, lookups);
|
||||
case "match":
|
||||
return answerText(task, { pairs: done }, lookups);
|
||||
case "build":
|
||||
@@ -354,6 +406,15 @@ export function TaskHost({ task, turnId, lookups, onSubmit, onSkip, disabled = f
|
||||
onEnter={submit}
|
||||
/>
|
||||
)}
|
||||
{task.type === "recall" && (
|
||||
<Recall
|
||||
task={task}
|
||||
answers={answers}
|
||||
setAnswers={setAnswers}
|
||||
disabled={disabled}
|
||||
onEnter={submit}
|
||||
/>
|
||||
)}
|
||||
{task.type === "match" && (
|
||||
<Match
|
||||
task={task}
|
||||
|
||||
@@ -64,6 +64,21 @@
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* recall: the prompt is English and the answer is 한글, so the weights swap */
|
||||
.ti-row.recall .q {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.ti-row.recall input {
|
||||
font-size: 19px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.rc-hint {
|
||||
font-size: 12px;
|
||||
color: var(--ink3);
|
||||
}
|
||||
|
||||
/* ── match ───────────────────────────────────────────────────────── */
|
||||
|
||||
.mt-cols {
|
||||
|
||||
Reference in New Issue
Block a user