From d7c7c9772e834cdd108e26df4fc2a68848b6c512 Mon Sep 17 00:00:00 2001 From: Till JS Date: Fri, 8 May 2026 20:48:39 +0200 Subject: [PATCH] =?UTF-8?q?Phase=207a:=20cards.create-Tool=20f=C3=BCr=20Cl?= =?UTF-8?q?oze=20+=20Image-Occlusion=20+=20content=5Fhash?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- apps/api/src/routes/tools.ts | 40 ++++++++++++++++++++++++++++++++---- 1 file changed, 36 insertions(+), 4 deletions(-) diff --git a/apps/api/src/routes/tools.ts b/apps/api/src/routes/tools.ts index 7018c8d..854616d 100644 --- a/apps/api/src/routes/tools.ts +++ b/apps/api/src/routes/tools.ts @@ -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,