Files
Hankan/types/lib/blocks.d.ts
MechaCat02 966043ab3f feat(lib): type declarations and golden tests for the exported modules
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>
2026-09-08 19:12:19 +02:00

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;