lib/ ships unchanged, so its types live in types/lib/ and are wired up by a
tsconfig path mapping. Adding types costs nothing; reimplementing the logic
would cost the two things that make this port possible.
The tests exist so a later refactor cannot silently drift them:
hangul the nine Composer cases named in the export README —
먹어 · 왔어 · 읽어 · 괜찮아 · 값 · 의사 · 뭐야 and backspace
conjugation all seven irregular classes, and every form in
IRREGULAR_FORMS reachable through haeche()
srs the SM-2 transitions, ease and interval clamps
blocks parse → answerText round-trip for all four task types
101 tests.
One is a pinned defect rather than a guarantee. blocks.parse() never closes
a ::gloss block on its "=" line, so a multi-sentence gloss — which the tutor
prompt explicitly invites — collapses into one run-on line keeping only the
last translation. lib/ ships unchanged, so the test records the real
behaviour and the app works around it at the call site.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
81 lines
2.4 KiB
TypeScript
81 lines
2.4 KiB
TypeScript
/* Declarations for lib/blocks.js — the module itself ships unchanged. */
|
|
|
|
export interface WordEntry {
|
|
ko: string;
|
|
gloss: string;
|
|
note: string;
|
|
}
|
|
|
|
export interface TranslateTask {
|
|
type: "translate";
|
|
items: { q: string }[];
|
|
}
|
|
export interface MatchTask {
|
|
type: "match";
|
|
pairs: { ko: string; gloss: string }[];
|
|
}
|
|
export interface BuildTask {
|
|
type: "build";
|
|
/** chips are given in correct order; the UI shuffles them. */
|
|
items: { en: string; chips: string[] }[];
|
|
}
|
|
export interface ChoiceTask {
|
|
type: "choice";
|
|
items: { q: string; options: string[] }[];
|
|
}
|
|
export type Task = TranslateTask | 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;
|
|
}
|
|
|
|
export interface ParsedMessage {
|
|
body: string;
|
|
words: WordEntry[] | null;
|
|
task: Task | null;
|
|
gloss: GlossBlock[] | null;
|
|
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 MatchState = { pairs: { ko: string; gloss: string }[] };
|
|
export type BuildState = string[][];
|
|
export type ChoiceState = (number | null)[];
|
|
export type TaskState = TranslateState | 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. */
|
|
export function answerText(
|
|
task: TranslateTask,
|
|
state: TranslateState,
|
|
lookups?: string[],
|
|
): string;
|
|
export function answerText(task: MatchTask, state: MatchState, lookups?: string[]): string;
|
|
export function answerText(task: BuildTask, state: BuildState, lookups?: string[]): string;
|
|
export function answerText(task: ChoiceTask, state: ChoiceState, lookups?: string[]): string;
|
|
export function answerText(task: Task, state: TaskState, lookups?: string[]): string;
|