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>
129 lines
3.9 KiB
TypeScript
129 lines
3.9 KiB
TypeScript
/* Declarations for lib/gate.js — the module itself ships unchanged.
|
|
|
|
The gate is what stops material being taught out of order. buildGate()
|
|
computes it from curriculum + progress; renderGate() turns it into the
|
|
{{GATE}} section of prompt/tutor-system.md. The second half —
|
|
scanTask(), proseIsKorean(), rejectionNote() — checks whether the tutor
|
|
actually listened, and its scope was measured against real messages
|
|
(audit-gate.mjs), not reasoned out. */
|
|
|
|
import type { ParsedMessage, Task } from "./blocks.js";
|
|
|
|
export interface Unit {
|
|
id: string;
|
|
ko: string;
|
|
name: string;
|
|
goal: string;
|
|
vocabUnit: boolean;
|
|
/** A 다지기 phase review: introduces nothing, confirms the whole phase. */
|
|
review?: boolean;
|
|
teaches: string[];
|
|
avoid?: string[];
|
|
/** Strictly NEW vocabulary. Deliberate repeats live in revisits[]. */
|
|
words: string[];
|
|
revisits?: { word: string; from: string }[];
|
|
}
|
|
|
|
/** A Unit with its phase stamped on, as flatten() returns it. */
|
|
export interface FlatUnit extends Unit {
|
|
phase: number;
|
|
phaseKo: string;
|
|
phaseName: string;
|
|
}
|
|
|
|
export interface Phase {
|
|
phase: number;
|
|
ko: string;
|
|
name: string;
|
|
units: Unit[];
|
|
}
|
|
|
|
export interface Curriculum {
|
|
version: number;
|
|
note?: string;
|
|
phases: Phase[];
|
|
}
|
|
|
|
export interface ProgressState {
|
|
current: string;
|
|
done: Record<string, boolean>;
|
|
confidence?: Record<string, number>;
|
|
}
|
|
|
|
/**
|
|
* Replaces the hand-listed word set with a dictionary query. Returning a
|
|
* frequency band here is what turns 371 hand-typed words into something
|
|
* that scales.
|
|
*/
|
|
export type VocabQuery = (unit: FlatUnit, done: FlatUnit[]) => string[];
|
|
|
|
export interface GateOptions {
|
|
vocabQuery?: VocabQuery;
|
|
}
|
|
|
|
export interface Gate {
|
|
unit: FlatUnit;
|
|
phase: { n: number; ko: string; name: string };
|
|
taught: string[];
|
|
forbidden: { near: string[]; tailUnit: FlatUnit | null; count: number };
|
|
vocabulary: string[];
|
|
newWords: string[];
|
|
/** Words met earlier that this unit should deliberately bring back. */
|
|
revisits: string[];
|
|
confidence: number | null;
|
|
next: FlatUnit | null;
|
|
finished: string[];
|
|
}
|
|
|
|
export function flatten(curriculum: Curriculum): FlatUnit[];
|
|
|
|
export function buildGate(
|
|
curriculum: Curriculum,
|
|
progress: ProgressState,
|
|
opts?: GateOptions,
|
|
): Gate;
|
|
|
|
/** Render the gate into the system prompt section. Keep the headings. */
|
|
export function renderGate(g: Gate): string;
|
|
|
|
/* ── enforcing the gate ─────────────────────────────────────────────── */
|
|
|
|
/** Unit and phase names plus the course's metalanguage (받침, 비음화, …):
|
|
Hangul a teacher may write without it being taught vocabulary. */
|
|
export function buildScaffold(curriculum: Curriculum): Set<string>;
|
|
|
|
/** Only the side of an exercise the student must decode. Recall prompts
|
|
are English, so a recall task contributes nothing. */
|
|
export function taskMaterial(task: Task | null | undefined): string[];
|
|
|
|
export interface GateFinding {
|
|
word: string;
|
|
/** The unit that introduces it, or "" when no unit does. */
|
|
unit: string;
|
|
/** false: no gloss exists anywhere — a typo or an invented word. */
|
|
known: boolean;
|
|
}
|
|
|
|
export interface ScanContext {
|
|
allowed: Set<string>;
|
|
scaffold: Set<string>;
|
|
/** Every dictionary word this surface could be, best first. */
|
|
heads: (token: string) => string[];
|
|
/** The unit that introduces a word, or "". */
|
|
unitOf: (word: string) => string;
|
|
}
|
|
|
|
export function scanTask(
|
|
parsed: Pick<ParsedMessage, "task" | "words"> | null | undefined,
|
|
ctx: ScanContext,
|
|
): GateFinding[];
|
|
|
|
/** ≥40 Hangul syllables in the prose outside blocks, and ≥50% of letters. */
|
|
export function proseIsKorean(text: string): boolean;
|
|
|
|
/** What to tell the tutor when a message is sent back. */
|
|
export function rejectionNote(findings: GateFinding[]): string;
|
|
|
|
/** Retries before a message is shown anyway, with its words flagged. */
|
|
export const GATE_TRIES: number;
|