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>
This commit is contained in:
80
types/lib/blocks.d.ts
vendored
Normal file
80
types/lib/blocks.d.ts
vendored
Normal file
@@ -0,0 +1,80 @@
|
||||
/* 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;
|
||||
33
types/lib/conjugation.d.ts
vendored
Normal file
33
types/lib/conjugation.d.ts
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
/* Declarations for lib/conjugation.js — the module itself ships unchanged. */
|
||||
|
||||
/** Forms that do not fall out of the rules and are simply known. */
|
||||
export const IRREGULAR_FORMS: Record<string, string>;
|
||||
|
||||
/** Dictionary form → 반말 present (해체). Returns null for non-verbs. */
|
||||
export function haeche(dict: string): string | null;
|
||||
|
||||
/** 반말 present → 반말 past. 먹어 → 먹었어. */
|
||||
export function past(present: string | null): string | null;
|
||||
|
||||
export function polite(present: string | null): string | null;
|
||||
|
||||
export type IrregularClass =
|
||||
| "ㅡ" | "ㅂ" | "ㄷ" | "르" | "ㅅ" | "ㅎ" | "special" | "regular";
|
||||
|
||||
/** Which class a dictionary form belongs to — drives the "why" in feedback. */
|
||||
export function irregularClass(dict: string): IrregularClass;
|
||||
|
||||
/** Human explanation of the rule applied — shown when an answer is wrong. */
|
||||
export function explain(dict: string): string;
|
||||
|
||||
export interface SurfaceForm {
|
||||
form: string;
|
||||
gloss: string;
|
||||
note: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build-time: every surface form a learner will meet, mapped back to its lemma.
|
||||
* This is what replaces a runtime morphological analyser.
|
||||
*/
|
||||
export function surfaceForms(dict: string, gloss: string): SurfaceForm[];
|
||||
80
types/lib/gate.d.ts
vendored
Normal file
80
types/lib/gate.d.ts
vendored
Normal file
@@ -0,0 +1,80 @@
|
||||
/* 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. */
|
||||
|
||||
export interface Unit {
|
||||
id: string;
|
||||
ko: string;
|
||||
name: string;
|
||||
goal: string;
|
||||
vocabUnit: 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;
|
||||
38
types/lib/hangul.d.ts
vendored
Normal file
38
types/lib/hangul.d.ts
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
/* Declarations for lib/hangul.js — the module itself ships unchanged. */
|
||||
|
||||
export const CHO: string;
|
||||
export const JUNG: string;
|
||||
export const JONG: string;
|
||||
|
||||
/** [initialIndex, medialIndex, finalIndex], or null if not a syllable block. */
|
||||
export function decompose(ch: string): [number, number, number] | null;
|
||||
export function compose(i: number, m: number, f?: number): string;
|
||||
|
||||
export const isJamo: {
|
||||
initial(c: string): boolean;
|
||||
medial(c: string): boolean;
|
||||
final(c: string): boolean;
|
||||
};
|
||||
|
||||
/**
|
||||
* A 두벌식 IME. Hold one per input: feed jamo with key(), literal text with
|
||||
* text(), Backspace with back(). Each call returns the full new value.
|
||||
*/
|
||||
export class Composer {
|
||||
cho: string | null;
|
||||
jung: string | null;
|
||||
jong: string | null;
|
||||
reset(): void;
|
||||
readonly empty: boolean;
|
||||
/** The syllable currently being assembled, as text. */
|
||||
render(): string;
|
||||
key(value: string, jamo: string): string;
|
||||
back(value: string): string;
|
||||
/** Commit the buffer and append literal text (space, punctuation). */
|
||||
text(value: string, t: string): string;
|
||||
}
|
||||
|
||||
export const KEYBOARD: {
|
||||
rows: string[][];
|
||||
shift: Record<string, string>;
|
||||
};
|
||||
38
types/lib/srs.d.ts
vendored
Normal file
38
types/lib/srs.d.ts
vendored
Normal file
@@ -0,0 +1,38 @@
|
||||
/* Declarations for lib/srs.js — the module itself ships unchanged. */
|
||||
|
||||
export const AGAIN: 0;
|
||||
export const HARD: 1;
|
||||
export const GOOD: 2;
|
||||
export const EASY: 3;
|
||||
export type Grade = 0 | 1 | 2 | 3;
|
||||
|
||||
export const NEW: 0;
|
||||
export const LEARNING: 1;
|
||||
export const REVIEW: 2;
|
||||
export type CardState = 0 | 1 | 2;
|
||||
|
||||
/** Days at which a card counts as known. */
|
||||
export const SECURE_INTERVAL: number;
|
||||
|
||||
export interface Card {
|
||||
state: CardState;
|
||||
interval: number;
|
||||
ease: number;
|
||||
/** Day number, not a timestamp — see dayNumber(). */
|
||||
due: number;
|
||||
reps: number;
|
||||
lapses: number;
|
||||
}
|
||||
|
||||
export function newCard(): Card;
|
||||
export function grade(card: Card, g: Grade, today: number): Card;
|
||||
export function markKnown(today: number): Card;
|
||||
|
||||
export type CardStatus = "new" | "learning" | "review" | "secure";
|
||||
export function statusOf(card: Card | null | undefined): CardStatus;
|
||||
|
||||
/** Label for the interval a grade would produce — shown on the buttons. */
|
||||
export function preview(card: Card | null | undefined, g: Grade, today: number): string;
|
||||
|
||||
/** Local day number, DST-safe. */
|
||||
export function dayNumber(d?: Date): number;
|
||||
Reference in New Issue
Block a user