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>
146 lines
4.3 KiB
TypeScript
146 lines
4.3 KiB
TypeScript
/* Declarations for lib/blocks.js — the module itself ships unchanged. */
|
|
|
|
export interface WordEntry {
|
|
ko: string;
|
|
gloss: string;
|
|
note: string;
|
|
}
|
|
|
|
/** What parse() stamps onto whichever task it returns. */
|
|
interface TaskMeta {
|
|
/** How many earlier ::task blocks in the message were withdrawn. The
|
|
LAST block wins; a non-zero count means the tutor retracted a draft. */
|
|
retracted?: number;
|
|
/** The block's raw rows, for the gate to read — see gate.js taskMaterial(). */
|
|
rows?: string[];
|
|
}
|
|
|
|
export interface TranslateTask extends TaskMeta {
|
|
type: "translate";
|
|
items: { q: string }[];
|
|
}
|
|
/** English prompt; the student WRITES the 한글. The one type that proves
|
|
recall rather than recognition, and the one that needs letterCheck(). */
|
|
export interface RecallTask extends TaskMeta {
|
|
type: "recall";
|
|
items: { q: string; hint: string }[];
|
|
}
|
|
export interface MatchTask extends TaskMeta {
|
|
type: "match";
|
|
pairs: { ko: string; gloss: string }[];
|
|
}
|
|
export interface BuildTask extends TaskMeta {
|
|
type: "build";
|
|
/** chips are given in correct order; the UI shuffles them. */
|
|
items: { en: string; chips: string[] }[];
|
|
}
|
|
export interface ChoiceTask extends TaskMeta {
|
|
type: "choice";
|
|
items: { q: string; options: string[] }[];
|
|
}
|
|
export type Task = TranslateTask | RecallTask | MatchTask | BuildTask | ChoiceTask;
|
|
export type TaskType = Task["type"];
|
|
|
|
/** One uppercase letter; see ROLES. */
|
|
export type GlossRole = "S" | "T" | "O" | "V" | "P" | "C" | "Q" | "M" | "N";
|
|
|
|
export interface GlossPart {
|
|
ko: string;
|
|
role: GlossRole;
|
|
gloss: string;
|
|
/** The meaningful piece INSIDE the word — particle, tense marker, ending. */
|
|
highlight: string;
|
|
}
|
|
export interface GlossBlock {
|
|
parts: GlossPart[];
|
|
en: string;
|
|
}
|
|
|
|
export interface Progress {
|
|
score: number;
|
|
note: string;
|
|
}
|
|
|
|
/** One marked item from a ::result block. */
|
|
export interface ResultRow {
|
|
item: string;
|
|
/** Only the literal "ok" (any case) counts as correct. */
|
|
ok: boolean;
|
|
/** On a wrong answer: what the tutor thinks the student mistook it for. */
|
|
mistakenFor: string;
|
|
}
|
|
|
|
export interface ParsedMessage {
|
|
body: string;
|
|
/** Every ::words entry in the message; the first gloss of a term wins. */
|
|
words: WordEntry[] | null;
|
|
/** The LAST ::task block. */
|
|
task: Task | null;
|
|
/** Every ::gloss block, accumulated; each closes at its "=" line. */
|
|
gloss: GlossBlock[] | null;
|
|
/** The last ::result block. */
|
|
results: ResultRow[] | null;
|
|
/** The last ::confirmed block, first column of each row. May carry a
|
|
leading "-" meaning "put this back". */
|
|
confirmed: string[] | null;
|
|
/** The last ::progress line. */
|
|
progress: Progress | null;
|
|
}
|
|
|
|
export function parse(text: string): ParsedMessage;
|
|
|
|
/** Roles a gloss part can carry, and what the UI should do with each. */
|
|
export const ROLES: Record<GlossRole, string>;
|
|
|
|
/** State shapes accepted by answerText(), per task type. */
|
|
export type TranslateState = string[];
|
|
export type RecallState = string[];
|
|
export type MatchState = { pairs: { ko: string; gloss: string }[] };
|
|
export type BuildState = string[][];
|
|
export type ChoiceState = (number | null)[];
|
|
export type TaskState = TranslateState | RecallState | MatchState | BuildState | ChoiceState;
|
|
|
|
/** Turn a completed task back into the message the student sends.
|
|
The state shape follows the task type, so these are overloads rather
|
|
than one signature over a union.
|
|
|
|
`letterBlock` is hangul.js letterCheck() output for a recall task — pass
|
|
it, always. The tutor cannot see inside a syllable and will invent a
|
|
diagnosis otherwise. Other task types ignore it. */
|
|
export function answerText(
|
|
task: TranslateTask,
|
|
state: TranslateState,
|
|
lookups?: string[],
|
|
letterBlock?: string,
|
|
): string;
|
|
export function answerText(
|
|
task: RecallTask,
|
|
state: RecallState,
|
|
lookups?: string[],
|
|
letterBlock?: string,
|
|
): string;
|
|
export function answerText(
|
|
task: MatchTask,
|
|
state: MatchState,
|
|
lookups?: string[],
|
|
letterBlock?: string,
|
|
): string;
|
|
export function answerText(
|
|
task: BuildTask,
|
|
state: BuildState,
|
|
lookups?: string[],
|
|
letterBlock?: string,
|
|
): string;
|
|
export function answerText(
|
|
task: ChoiceTask,
|
|
state: ChoiceState,
|
|
lookups?: string[],
|
|
letterBlock?: string,
|
|
): string;
|
|
export function answerText(
|
|
task: Task,
|
|
state: TaskState,
|
|
lookups?: string[],
|
|
letterBlock?: string,
|
|
): string;
|