Phase 7a: cards.create-Tool für Cloze + Image-Occlusion + content_hash
Some checks are pending
CI / validate (push) Waiting to run

Tool-Pfad in /api/v1/tools/cards.create war nicht konvergent zum
REST-Pfad in /api/v1/cards POST:
  - subIndexCount(type) crashte bei type='cloze' und 'image-occlusion'
    (beide werfen seit Sprint 8a/9l, weil Sub-Index-Anzahl text-abhängig)
  - content_hash wurde nicht geschrieben (war seit Sprint 9j auf REST-Pfad)

Fix: identische Branching-Logik wie cards.ts POST. Cloze ohne {{cN::…}}
und image-occlusion ohne valides mask_regions-JSON liefern 422.
content_hash wird mit cardContentHash beim Insert geschrieben.

Damit ist der Tool-Pfad voll-konvergent — mana-mcp und Persona-Runner
können jeden Card-Type via cards.create anlegen, sobald die Plattform-
Services (mana-share + mana-mcp) deployed sind.

Phase-7-Plumbing:
  ✓ Cards-Tools (cards.create + cards.search) sind konvergent zum
    REST-Pfad und end-to-end via Bearer-JWT verifiziert
  ✓ App-Manifest deklariert beide Tools (input_schema + output_schema)
  ✓ Service-Key in mana-auth registriert (Phase 2)
  ✗ mana-mcp + mana-share Container sind auf Mac Mini NICHT deployed
    → Tool-Discovery + Routing aus Claude Desktop / Persona-Runner
    bleiben offen, bis die Plattform-Services hochgezogen werden.
    Das ist Plattform-Scope, nicht Cards-Scope.

56 API-Tests grün, type-check sauber.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-05-08 20:48:39 +02:00
parent 5b6d096f56
commit d7c7c9772e

View file

@ -4,8 +4,11 @@ import { Hono } from 'hono';
import {
CardsCreateInputSchema,
CardsSearchInputSchema,
cardContentHash,
maskRegionCount,
newReview,
subIndexCount,
subIndexCountForCloze,
} from '@cards/domain';
import { getDb, type CardsDb } from '../db/connection.ts';
@ -55,8 +58,39 @@ export function toolsRouter(deps: ToolsDeps = {}): Hono<{ Variables: AuthVars }>
if (!deck) return c.json({ error: 'deck_not_found' }, 404);
if (deck.userId !== userId) return c.json({ error: 'deck_not_owned' }, 403);
// Text-abhängige Sub-Index-Counts identisch zum REST-Pfad
// (cards.ts POST). Cloze ohne Cluster + Image-Occlusion
// ohne Mask-Regions werden 422.
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 if (parsed.data.type === 'image-occlusion') {
count = maskRegionCount(parsed.data.fields.mask_regions ?? '');
if (count === 0) {
return c.json(
{
error: 'invalid_input',
issues: ['image-occlusion.mask_regions must be JSON array with >=1 region'],
},
422
);
}
} else {
count = subIndexCount(parsed.data.type);
}
const cardId = ulid();
const now = new Date();
const contentHash = await cardContentHash({
type: parsed.data.type,
fields: parsed.data.fields,
});
const [row] = await dbOf().transaction(async (tx) => {
const [card] = await tx
.insert(cards)
@ -67,14 +101,12 @@ export function toolsRouter(deps: ToolsDeps = {}): Hono<{ Variables: AuthVars }>
type: parsed.data.type,
fields: parsed.data.fields,
mediaRefs: parsed.data.media_refs ?? [],
contentHash,
createdAt: now,
updatedAt: now,
})
.returning();
const initial = Array.from(
{ length: subIndexCount(parsed.data.type) },
(_, i) => i
).map((subIndex) => {
const initial = Array.from({ length: count }, (_, i) => i).map((subIndex) => {
const r = newReview({ userId, cardId, subIndex, now });
return {
cardId: r.card_id,