Phase 7a: cards.create-Tool für Cloze + Image-Occlusion + content_hash
Some checks are pending
CI / validate (push) Waiting to run
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:
parent
5b6d096f56
commit
d7c7c9772e
1 changed files with 36 additions and 4 deletions
|
|
@ -4,8 +4,11 @@ import { Hono } from 'hono';
|
||||||
import {
|
import {
|
||||||
CardsCreateInputSchema,
|
CardsCreateInputSchema,
|
||||||
CardsSearchInputSchema,
|
CardsSearchInputSchema,
|
||||||
|
cardContentHash,
|
||||||
|
maskRegionCount,
|
||||||
newReview,
|
newReview,
|
||||||
subIndexCount,
|
subIndexCount,
|
||||||
|
subIndexCountForCloze,
|
||||||
} from '@cards/domain';
|
} from '@cards/domain';
|
||||||
|
|
||||||
import { getDb, type CardsDb } from '../db/connection.ts';
|
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) return c.json({ error: 'deck_not_found' }, 404);
|
||||||
if (deck.userId !== userId) return c.json({ error: 'deck_not_owned' }, 403);
|
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 cardId = ulid();
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
|
const contentHash = await cardContentHash({
|
||||||
|
type: parsed.data.type,
|
||||||
|
fields: parsed.data.fields,
|
||||||
|
});
|
||||||
const [row] = await dbOf().transaction(async (tx) => {
|
const [row] = await dbOf().transaction(async (tx) => {
|
||||||
const [card] = await tx
|
const [card] = await tx
|
||||||
.insert(cards)
|
.insert(cards)
|
||||||
|
|
@ -67,14 +101,12 @@ export function toolsRouter(deps: ToolsDeps = {}): Hono<{ Variables: AuthVars }>
|
||||||
type: parsed.data.type,
|
type: parsed.data.type,
|
||||||
fields: parsed.data.fields,
|
fields: parsed.data.fields,
|
||||||
mediaRefs: parsed.data.media_refs ?? [],
|
mediaRefs: parsed.data.media_refs ?? [],
|
||||||
|
contentHash,
|
||||||
createdAt: now,
|
createdAt: now,
|
||||||
updatedAt: now,
|
updatedAt: now,
|
||||||
})
|
})
|
||||||
.returning();
|
.returning();
|
||||||
const initial = Array.from(
|
const initial = Array.from({ length: count }, (_, i) => i).map((subIndex) => {
|
||||||
{ length: subIndexCount(parsed.data.type) },
|
|
||||||
(_, i) => i
|
|
||||||
).map((subIndex) => {
|
|
||||||
const r = newReview({ userId, cardId, subIndex, now });
|
const r = newReview({ userId, cardId, subIndex, now });
|
||||||
return {
|
return {
|
||||||
cardId: r.card_id,
|
cardId: r.card_id,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue