Phase 8a: Cloze als MVP-Card-Type, Cluster-Counter

CardTypeSchema öffnet 'cloze' als drittes MVP-Set-Mitglied. Domain-Modul
@cards/domain/src/cloze.ts kapselt die Cluster-Logik (extractClusterIds,
subIndexCountForCloze, clusterIdForSubIndex, renderClozePrompt/Answer)
— Hint-Markup wird MVP-stumm gedroppt.

subIndexCount('cloze') wirft jetzt explizit, statt still auf 1 zu fallen,
weil die Cluster-Anzahl text-abhängig ist und ein silent-default falsch
dimensionierte Review-Tabellen produzieren würde. Card-POST-Handler holt
für Cloze die Anzahl aus subIndexCountForCloze und lehnt 422 ab, wenn
kein {{cN::…}}-Markup vorhanden ist.

12 neue Cloze-Tests, alle Domain- und API-Tests grün (41 + 46).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-05-08 17:35:39 +02:00
parent 2bed28212d
commit 553a78d73b
9 changed files with 249 additions and 14 deletions

View file

@ -1,7 +1,13 @@
import { and, eq } from 'drizzle-orm';
import { Hono } from 'hono';
import { CardCreateSchema, CardUpdateSchema, newReview, subIndexCount } from '@cards/domain';
import {
CardCreateSchema,
CardUpdateSchema,
newReview,
subIndexCount,
subIndexCountForCloze,
} from '@cards/domain';
import { getDb, type CardsDb } from '../db/connection.ts';
import { cards, decks, reviews } from '../db/schema/index.ts';
@ -33,6 +39,22 @@ export function cardsRouter(deps: CardsDeps = {}): Hono<{ Variables: AuthVars }>
}
const userId = c.get('userId');
// Cloze: Sub-Index-Anzahl hängt vom Cluster-Markup im Text ab.
// Eine Cloze-Karte ohne `{{cN::…}}` ist sinnlos — vor dem Deck-Lookup
// ablehnen, damit Validation-Errors konsistent 422 statt 404 sind.
let count: number;
if (parsed.data.type === 'cloze') {
count = subIndexCountForCloze(parsed.data.fields.text ?? '');
if (count === 0) {
return c.json(
{ error: 'invalid_input', issues: ['cloze.text contains no {{cN::…}} clusters'] },
422
);
}
} else {
count = subIndexCount(parsed.data.type);
}
const [deck] = await dbOf()
.select({ id: decks.id, userId: decks.userId })
.from(decks)
@ -43,7 +65,7 @@ export function cardsRouter(deps: CardsDeps = {}): Hono<{ Variables: AuthVars }>
const cardId = ulid();
const now = new Date();
const subIndices = Array.from({ length: subIndexCount(parsed.data.type) }, (_, i) => i);
const subIndices = Array.from({ length: count }, (_, i) => i);
const [cardRow] = await dbOf().transaction(async (tx) => {
const [card] = await tx