vector-mirror 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,626 @@
1
+ /**
2
+ * schema.js - Zod Schemas for MCP Tool Input/Output
3
+ * Vector Mirror v2.0 Phase 2
4
+ *
5
+ * Interface module: Defines all tool schemas (transport-agnostic)
6
+ * BAUPLAN ref: Sektion 5 (Tools) + Sektion 7.2 (SDK-Pattern) + Sektion 10.4 (structuredContent)
7
+ * DEPENDS: zod
8
+ */
9
+ import { z } from 'zod';
10
+
11
+ // ── INPUT SCHEMAS ──────────────────────────────────────────
12
+
13
+ export const analyzeInput = {
14
+ svg: z.string().describe('Vollstaendiger SVG-String'),
15
+ constraints: z
16
+ .array(z.string())
17
+ .optional()
18
+ .describe(
19
+ 'Regeln im Format: "#subject TYPE #reference [value]". Typen: CENTERED-IN, NO-OVERLAP, INSIDE, ALIGNED-LEFT, ALIGNED-TOP, LEFT-OF, ABOVE, DISTANCE-FROM, SAME-SIZE, COLOR',
20
+ ),
21
+ previousIssueCount: z
22
+ .number()
23
+ .int()
24
+ .nonnegative()
25
+ .optional()
26
+ .describe(
27
+ 'Anzahl Fehler des vorherigen Aufrufs (fuer Konvergenz-Tracking)',
28
+ ),
29
+ };
30
+
31
+ export const compareInput = {
32
+ svg: z.string().describe('Vollstaendiger SVG-String (neuer Zustand)'),
33
+ constraints: z
34
+ .array(z.string())
35
+ .optional()
36
+ .describe('Optionale Constraints fuer Re-Check'),
37
+ // §1.1 Stateless RPC: analysisId ist Pflichtfeld (Caller-Pflicht).
38
+ // §1.4 Disjunktion: akzeptiert jetzt UUID (grids) ODER Bookmark-Name
39
+ // (bookmarks). z.string().min(1) bleibt permissiv (KEIN ZodEffects →
40
+ // JSON-Schema-clean). Disambiguierung WRITE-seitig: bookmarkInput.name
41
+ // verbietet UUID-Form → Keyspaces disjunkt. §1.1-Invariante (kein
42
+ // .optional()) bleibt — Pflichtfeld.
43
+ analysisId: z
44
+ .string()
45
+ .min(1)
46
+ .describe(
47
+ 'analysisId (UUID aus analyze) ODER Bookmark-Name (aus vector_mirror_bookmark). Server löst UUID→grids, Name→bookmarks auf. Pflichtfeld (§1.1).',
48
+ ),
49
+ };
50
+
51
+ // §1.4 Globale Bookmarks (B-3): Input für vector_mirror_bookmark.
52
+ // name verbietet UUID-Form via Negative-Lookahead — garantiert Keyspace-
53
+ // Disjunktion zum grids-Keyspace (UUIDs). Empirie-belegt: ein UUID v4 mit
54
+ // Hex-Letter-Start (a-f) würde sonst BEIDE Muster treffen (Restambiguität);
55
+ // der Lookahead schließt das strukturell aus (alle 5 Hex-Gruppen 8-4-4-4-12).
56
+ export const bookmarkInput = {
57
+ name: z
58
+ .string()
59
+ .min(1)
60
+ .max(64)
61
+ .regex(
62
+ /^(?![0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$)[A-Za-z][A-Za-z0-9_.-]{0,63}$/,
63
+ 'Name: Buchstabe-Start, [A-Za-z0-9_.-], kein UUID-Format',
64
+ )
65
+ .describe('Bookmark-Name (kein UUID-Format; Keyspace disjunkt von grids).'),
66
+ analysisId: z
67
+ .string()
68
+ .uuid()
69
+ .describe('analysisId aus vector_mirror_analyze'),
70
+ };
71
+
72
+ export const inspectInput = {
73
+ svg: z.string().describe('Vollstaendiger SVG-String'),
74
+ };
75
+
76
+ export const paletteInput = {
77
+ svg: z.string().describe('Vollstaendiger SVG-String'),
78
+ };
79
+
80
+ const arrangeElementSchema = z.object({
81
+ id: z.string(),
82
+ tag: z.string(),
83
+ r: z.number().nonnegative().finite().optional(),
84
+ width: z.number().nonnegative().finite().optional(),
85
+ height: z.number().nonnegative().finite().optional(),
86
+ content: z.string().optional(),
87
+ x: z.number().finite().optional(),
88
+ y: z.number().finite().optional(),
89
+ transform: z.string().optional(),
90
+ });
91
+
92
+ export const arrangeInput = {
93
+ canvas: z
94
+ .object({
95
+ width: z.number().positive().finite(),
96
+ height: z.number().positive().finite(),
97
+ })
98
+ .describe('Canvas-Dimensionen'),
99
+ elements: z
100
+ .array(arrangeElementSchema)
101
+ .refine(
102
+ (elements) =>
103
+ new Set(elements.map((element) => element.id)).size === elements.length,
104
+ {
105
+ message: 'Element-IDs muessen eindeutig sein',
106
+ },
107
+ )
108
+ .describe('Elemente mit ID, Tag und optionalen Dimensionen'),
109
+ constraints: z
110
+ .array(z.string())
111
+ .describe(
112
+ 'Constraints im Format "#subject TYPE #reference". Reihenfolge ist semantisch relevant.',
113
+ ),
114
+ };
115
+
116
+ export const constraintsInput = {};
117
+
118
+ export const statusInput = {};
119
+
120
+ // §1.9 Eichkörper-Selftest: optionaler full-Flag (true → zusätzlich N=10-Mini-
121
+ // Determinismus-Check pro Lauf). Default false (Kalibrierung allein).
122
+ export const selftestInput = {
123
+ full: z
124
+ .boolean()
125
+ .optional()
126
+ .describe(
127
+ 'true → zusätzlich N=10-Mini-Determinismus-Check (langsamer). Default: nur Kalibrierung.',
128
+ ),
129
+ };
130
+
131
+ // ── OUTPUT SCHEMAS (structuredContent) ────────────────────
132
+
133
+ // §6 RELAIS Fehler-Kanal (an internal spec §6, R9a #13/#14): additiver,
134
+ // optionaler Top-Level-Schlüssel `error: {code, hint}` — NUR präsent, wenn
135
+ // die Antwort isError:true trägt (gleiches additive-optional-Muster wie
136
+ // canvas_validity). code = Ursachen-Name (bestehendes Vokabular: Renderer-
137
+ // Error-Codes bzw. NO_BASELINE/ANALYSIS_NOT_FOUND/ARRANGE_FAILED aus der
138
+ // pipeline-Quelle); hint = der Navigations-Satz, wortidentisch in der Prosa
139
+ // (Parity-Pin: tests/relais_red). BEWUSST KEIN severity/level/weight-Feld
140
+ // (maintainer gate §7.2 Severity-Form). Plain zod, kein ZodEffects.
141
+ const errorEnvelopeSchema = z
142
+ .object({
143
+ code: z.string(),
144
+ hint: z.string(),
145
+ })
146
+ .optional();
147
+
148
+ // §1.5 Block E (R-E): fixSchema.attribute ist von z.string() auf z.enum gehärtet.
149
+ // Die Enum-Liste ist gegen die TATSÄCHLICHE buildFix/buildElementFixes-Wertemenge
150
+ // kalibriert (NICHT FIX_PLANs spekulative x2/y2):
151
+ // - whitelist-Pfad (deltaToAttribute map-Werte): cx,cy,r,rx,ry,x,y,x1,y1,width,height
152
+ // - Transform-Fallback (path/polygon/...): transform
153
+ // - tspan native relativer Shift (R-C): dx,dy (NUR für tspan valide)
154
+ // BEWUSST AUSGESCHLOSSEN: dw,dh (werden NIE als attribute emittiert — Größen-Fix
155
+ // auf Non-Whitelist-Tags wird zu reason='SIZE_FIX_UNSUPPORTED_FOR_TAG', C3/C5).
156
+ // Damit ist <path dw=...>/<path dh=...> schemamechanisch unmöglich (C5). dx/dy
157
+ // bleiben enum-zulässig (tspan), aber der Producer (structured.js) emittiert sie
158
+ // für path/polygon NIE — dort läuft alles über 'transform'.
159
+ const FIX_ATTRIBUTE_ENUM = [
160
+ 'cx',
161
+ 'cy',
162
+ 'r',
163
+ 'rx',
164
+ 'ry',
165
+ 'x',
166
+ 'y',
167
+ 'x1',
168
+ 'y1',
169
+ 'width',
170
+ 'height',
171
+ 'transform',
172
+ 'dx',
173
+ 'dy',
174
+ ];
175
+
176
+ const fixSchema = z.object({
177
+ attribute: z.enum(FIX_ATTRIBUTE_ENUM),
178
+ current: z.string(),
179
+ target: z.string(),
180
+ // §1.5: low-effort known-limitation-Hinweis am transform-Fix (praezision_2,
181
+ // CSS-Override). Optional — nur der Transform-Fallback setzt ihn.
182
+ warning: z.string().optional(),
183
+ });
184
+
185
+ const correctionSchema = z.object({
186
+ element: z.string(),
187
+ tag: z.string().optional(),
188
+ constraint: z.string(),
189
+ reference: z.string().nullable(),
190
+ dx: z.number().int().optional(),
191
+ dy: z.number().int().optional(),
192
+ dw: z.number().int().optional(),
193
+ dh: z.number().int().optional(),
194
+ fix: fixSchema.optional(),
195
+ fixes: z.array(fixSchema).optional(),
196
+ // §1.5 (praezision_3): Größen-Fix auf Tags ohne Size-Mapping (path/polygon/...)
197
+ // ist nicht via Attribut darstellbar → reason statt erfundenem scale(). Optional.
198
+ reason: z.enum(['SIZE_FIX_UNSUPPORTED_FOR_TAG']).optional(),
199
+ });
200
+
201
+ // §1.2b L-004 / R-β-2: elementSchema wird exportiert, damit test_schema.js
202
+ // den Schema-Vertrag direkt testen kann (statt nur indirekt via analyzeOutput).
203
+ // API-Erweiterung ist additiv — bestehende Konsumenten (analyzeOutput, inspectOutput,
204
+ // die z.array(elementSchema) bauen) bleiben unverändert.
205
+ export const elementSchema = z.object({
206
+ id: z.string(),
207
+ tag: z.string(),
208
+ cell: z.string(),
209
+ color: z.string(),
210
+ status: z.enum(['ok', 'fail', 'warn']),
211
+ // §1.2 3D-Detection Pre-Gate (FIX_PLAN §1.2 + ADR-026 §3):
212
+ // 'not_measurable' wenn ein Vorfahre matrix3d(...)/perspective(...)/preserve-3d trägt
213
+ // (Spotter darf darauf keine Pixel-Deltas berechnen). 'reliable' bei reiner 2D-Kette.
214
+ // §1.2b L-003 (Sprint-β1, Pflicht-Promotion): Datenkette grid.js (mappedElements)
215
+ // + structured.js (sceneElements) propagieren das Feld vollständig vom Renderer
216
+ // durch — REGEL-3 Spotter-Anti-Lüge ist jetzt auch auf MCP-Caller-Ebene erfüllt.
217
+ // bbox_reliability ist daher Pflicht; warnings bleibt .optional() (nur bei
218
+ // 3D-Treffer populiert).
219
+ // §1.4
220
+ // (ADR-026 §6 Reliability-Trichter + ADR-032 §Entscheidung). 'approximate'
221
+ // signalisiert: Element ist messbar, aber bekannte Naeherung (z.B. 2D-CSS-
222
+ // transform mit Float-Drift, dominant-baseline-Text mit Glyph-Ascent,
223
+ // opacity-Grauzone). Spotter MUSS approximate-Elemente wie reliable
224
+ // behandeln im Pass-Pfad, aber bei fail KEINE Pixel-Korrektur emittieren
225
+ // (superRefine + structured.js Gate adressieren das). Pessimismus-Prinzip:
226
+ // 'not_measurable' > 'approximate' > 'reliable'.
227
+ bbox_reliability: z.enum(['reliable', 'approximate', 'not_measurable']),
228
+ // §1.5 Block E (befund_3): Parent-Kontext für tspan/textPath. Der §1.5-tspan-Fix
229
+ // nutzt natives dx/dy RELATIV zur Eltern-<text>-Position (R-C) — der Caller braucht
230
+ // parent_id/parent_tag, um zu wissen, woran der tspan hängt. .optional(), weil nur
231
+ // verschachtelte Elemente (tspan, textPath) sie tragen; Top-Level-Elemente nicht.
232
+ parent_id: z.string().optional(),
233
+ parent_tag: z.string().optional(),
234
+ // §1.5 Block H / Patch P1 (F-2): Autor-transform-Attribut. Vom Renderer surfaced,
235
+ // damit der Transform-Fallback (buildTransformFix) den Autor-scale/rotate erhält.
236
+ // .optional() — nur Elemente mit transform-Attribut tragen es.
237
+ transform: z.string().optional(),
238
+ warnings: z.array(z.string()).optional(),
239
+ // §E4 Paint-Extent-Ehrlichkeit (F-AT-004, DoD-3): ein gefiltertes Element malt
240
+ // Tinte (Glow/Schatten/Blur) AUSSERHALB seiner geom-bbox. bbox_reliability bleibt
241
+ // 'reliable' (die Geometrie IST exakt) — die Ehrlichkeit trägt diese zwei Felder.
242
+ // has_paint_overflow: true, wenn die W3C-Filter-Region über die geom-bbox
243
+ // hinausragt (url(#id)→<filter>) ODER der Overflow existiert, aber unmessbar
244
+ // ist (CSS-Filter-Funktion ohne Region). .optional() — filterlose Elemente
245
+ // tragen es NICHT (Negativ-Kontrolle: kein Over-Flag).
246
+ has_paint_overflow: z.boolean().optional(),
247
+ // visual_bbox: die Filter-Region als AABB-Hülle im user-space ({x,y,w,h}) ODER
248
+ // das Literal 'not_measurable' (CSS-Filter-Funktion → spec-seitig keine
249
+ // Region). .nullable().optional() — KEIN Enum-Bruch (additiv). Union aus
250
+ // bbox-Form und String-Literal.
251
+ visual_bbox: z
252
+ .union([
253
+ z.object({
254
+ x: z.number().finite(),
255
+ y: z.number().finite(),
256
+ w: z.number().finite(),
257
+ h: z.number().finite(),
258
+ }),
259
+ z.literal('not_measurable'),
260
+ ])
261
+ .nullable()
262
+ .optional(),
263
+ // §HEAL-R6 / T1 Paint-Presence (F-AT-6-01, CRIT, DoD-2): der Sichtbarkeits-Walk
264
+ // prüfte fill-opacity/stroke-opacity NICHT — ein fill-opacity:0-Element (0 Pixel)
265
+ // wurde als reliable+sichtbar emittiert (die Lüge). Drei additive Felder tragen
266
+ // jetzt die Tinten-Wahrheit (additiv → bestehende Konsumenten unverändert):
267
+ // fill_paint_factor / stroke_paint_factor: die effektiven, permanenz-bewussten
268
+ // Kanal-Alpha-Faktoren (composedOpacity-frei; Element-opacity steckt im
269
+ // opacity-Feld). Diagnostik für den Konsumenten.
270
+ // paint_visible: 3-wertiger Tinten-Vertrag (§HEAL-R6 Variante 1, F-AT-6-07).
271
+ // false — KEIN Kanal malt ODER ein RÄUMLICHER Operator löscht die
272
+ // Tinte beweisbar (CTM-det=0 / Vorfahr-Viewport-Clip-leer).
273
+ // 'indeterminate'— ein nicht-aufzählbarer räumlicher Operator (clip-path/mask/
274
+ // pattern/filter, oder nicht-endliche CTM) ist present, aber
275
+ // die Tinten-Präsenz ist raster-frei NICHT entscheidbar →
276
+ // ehrlich unbestimmt statt falsch-reliable (Blind-Trust). Trägt
277
+ // die Warning PAINT_PRESENCE_INDETERMINATE.
278
+ // absent — normal sichtbar (Negativ-Kontrolle: Feld fehlt).
279
+ // bbox_reliability bleibt 'reliable' (die Geometrie IST exakt; NUR die Tinten-
280
+ // Behauptung wird graduiert). Die SDK registriert diese als MCP-outputSchema
281
+ // (tools.js) — ohne die Union verwirft sie 'indeterminate' zur LAUFZEIT (KRIT).
282
+ fill_paint_factor: z.number().optional(),
283
+ stroke_paint_factor: z.number().optional(),
284
+ paint_visible: z
285
+ .union([z.literal(false), z.literal('indeterminate')])
286
+ .optional(),
287
+ // §D5 / R6-STATE Zustands-Abhängigkeit (state_dependent): reines Flag — ein
288
+ // interaktiver Pseudo-Selektor (:hover/:focus/:focus-within/:focus-visible/
289
+ // :active/:target) self-oder-Vorfahr ODER ein SMIL <set/animate begin|end mit
290
+ // Event-Token zielt auf das Element. Die t=0-Geometrie bleibt EXAKT wahr (KEIN
291
+ // bbox_reliability-Degrade) — Alt-Zustände sind ZUSÄTZLICHE Wahrheiten. Plain
292
+ // zod (KEIN ZodEffects) → automatisch in beiden registrierten outputSchemas,
293
+ // kein Laufzeit-Reject. true-only (statische Elemente tragen das Feld NICHT).
294
+ state_dependent: z.boolean().optional(),
295
+ // §F-AT-6-09 / R6-MEDIA + §HEAL-4 Viewport-Abhängigkeit (media_dependent):
296
+ // reines true-only-Flag — das Element ist VIEWPORT-DIVERGENT, erkannt über
297
+ // EINE von ZWEI ODER-verknüpften Quellen (Semantik-Weitung Heal 4, additiv):
298
+ // (a) STATISCH: ein Selektor INNERHALB einer @media-(Conditional-Group-)
299
+ // Regel trifft das Element UND deren Bedingung nennt ein VIEWPORT-
300
+ // Feature (width/height/orientation/aspect-ratio/resolution/device-*),
301
+ // ODER element-lokale vw/vh/vmin/vmax-Geometrie (authored-Scan, I1).
302
+ // Rein statisch @t=0, KEIN matches-Gate — beide Divergenz-Richtungen.
303
+ // (b) GEMESSEN (§HEAL-4, an internal spec): der lean
304
+ // 2-Viewport-Mess-Diff ([1920,400] ∪ px-Breakpoint-Straddles) in
305
+ // analyze()/inspect() belegt reale Divergenz auf einer der 3 Achsen
306
+ // (Geometrie in root-user-units / computed paint / Paint-Server-
307
+ // Closure) — fängt auch @container, %-in-nested-svg, <style>-Regel-vw,
308
+ // transform-vw und font-size-vw, die (a) nicht sieht.
309
+ // Die Verknüpfung ist STRIKT ADDITIV (OR-only): Messung fügt nur hinzu,
310
+ // nimmt nie ein statisches true weg (F-AT-7-14 use-Shadow-Blindfleck —
311
+ // die Statik bleibt load-bearing). Mess-Ausfall ist LAUT (scene-level
312
+ // MEDIA_MEASURE_UNAVAILABLE im Prosa-Kanal), nie still.
313
+ // Die t=0-Geometrie bleibt EXAKT wahr (KEIN bbox_reliability-Degrade).
314
+ // Orthogonal zu state_dependent/Motion (drei stille Achsen). Plain zod
315
+ // (KEIN ZodEffects) → kein Laufzeit-Reject. true-only.
316
+ media_dependent: z.boolean().optional(),
317
+ // §HEAL-5 / F-AT-2-005 Zeit-Achse (motion_dependent): reines true-only-Flag —
318
+ // die Subjekt-Geometrie ist ZEIT-VARIANT (clock-rooted SMIL-GEOMETRIE:
319
+ // animate/set auf einem Geometrie-Attribut · animateTransform · animateMotion,
320
+ // deren begin-ATTRIBUT GANZ FEHLT (Blink-Default 0s) ODER ≥1 validen
321
+ // Offset-/Clock-Token trägt; leere/malformed begin-Werte laufen in Blink NIE
322
+ // — Boden-Wahrheit mgr/malformed_begin_gt.mjs + empty_begin_gt.mjs, Mikro-
323
+ // Patch R1). Die t0-Messung bleibt EXAKT — an anderem t anders
324
+ // (KEIN bbox_reliability-Degrade; T3a/Heal-4-Präzedenz: Alt-Zeitpunkte sind
325
+ // ZUSÄTZLICHE Wahrheiten). Vierte stille Achse, orthogonal zu state_dependent/
326
+ // media_dependent/paint_visible. z.literal(true) kodiert true-only AM VERTRAG
327
+ // (Spec-Tabelle Edit #1; Emission ist ohnehin true-only-Spread). Plain zod
328
+ // (KEIN ZodEffects) → automatisch in beiden registrierten outputSchemas, kein
329
+ // Laufzeit-Reject. EDIT #1 ZUERST (R6-Präzedenz, empirisch erzwungen: zod
330
+ // unknownKeys='strip' strippt unbekannte Felder STILL — Witness S, 4c8a6ed).
331
+ motion_dependent: z.literal(true).optional(),
332
+ // §H10 R11-06 Paint-Zeit-Achse (paint_time_variant): reines true-only-Flag —
333
+ // eine PAINT-/Darstellungs-Eigenschaft des Elements ist zeit-variant
334
+ // (clock-rooted SMIL auf NICHT-Geometrie-Kanal: fill/stroke/opacity/… bzw.
335
+ // animateColor). Die t0-Messung (Farbe/Opacity @t0) bleibt EXAKT — an
336
+ // anderem t anders. Fünfte Achse, orthogonal zu motion_dependent
337
+ // (Geometrie-Zeit ≠ Paint-Zeit). Selbes true-only-Literal-Muster.
338
+ paint_time_variant: z.literal(true).optional(),
339
+ });
340
+
341
+ const diffEntrySchema = z.object({
342
+ type: z.string(),
343
+ id: z.string(),
344
+ from: z.string().optional(),
345
+ to: z.string().optional(),
346
+ });
347
+
348
+ const iterationSchema = z.object({
349
+ sequence: z.number().int(),
350
+ previous_issues: z.number().int(),
351
+ current_issues: z.number().int(),
352
+ total_issues: z.number().int(),
353
+ returned_issues: z.number().int(),
354
+ suppressed: z.number().int(),
355
+ // §H9 K-12: BASELINE = erste Messung mit Issues (sequence=1, keine
356
+ // Historie) — die Trend-Wörter STAGNATING/DIVERGING sind echten
357
+ // Vergleichen (previousIssueCount vorhanden) vorbehalten. Additiv.
358
+ // §H9 P2: .nullable() — die Error-Hülle (analyzeErrorStructured) trägt
359
+ // convergence:null („keine Aussage": im Fehlerfall gibt es keine Messung,
360
+ // das frühere 'SOLVED' widersprach isError:true). Erfolgs-Pfade emittieren
361
+ // weiterhin IMMER einen Enum-Wert (computeConvergence). Additiv-nullable,
362
+ // kein ZodEffects (Muster: breakerStatsSchema/calibrationSchema).
363
+ convergence: z
364
+ .enum(['IMPROVING', 'STAGNATING', 'DIVERGING', 'SOLVED', 'BASELINE'])
365
+ .nullable(),
366
+ // §1.3 Schicht 2: Server-Garantie — auf JEDEM Erfolgs-Pfad emittiert
367
+ // (UUID v4 via crypto.randomUUID). §H9 P2 (Wahrheits-Rekalibrierung der
368
+ // Garantie): die Error-Hülle trägt analysisId:null statt einer ERFUNDENEN
369
+ // frischen UUID, die kein Grid referenziert — das Feld bleibt Pflicht
370
+ // (nullable ≠ optional), null ist das ehrliche „keine Analyse gespeichert".
371
+ analysisId: z.string().uuid().nullable(),
372
+ });
373
+
374
+ const uncheckedEntrySchema = z.object({
375
+ element: z.string().nullable(),
376
+ constraint: z.string(),
377
+ reasonCategory: z.string(),
378
+ reasonCode: z.string(),
379
+ hint: z.string(),
380
+ suggestedCorrection: z.string().optional(),
381
+ });
382
+
383
+ // §1.4
384
+ // Kanal fuer Warnings auf Elementen, die das slice(0,7)-Cap aus BAUPLAN
385
+ // OBL-008 verbergen wuerde. structured.js (Edit F) befuellt das, wenn
386
+ // elements.length > 7 UND warning-tragende Elemente auf Position >7 stehen.
387
+ // Caller sieht damit das Reliability-Signal auch jenseits des Scene-Cap
388
+ // (REGEL-3 Spotter-Anti-Luege ueberstimmt BAUPLAN-Token-Limit).
389
+ const truncatedWarningEntrySchema = z.object({
390
+ element_id: z.string(),
391
+ warnings: z.array(z.string()),
392
+ position: z.number().int().nonnegative(),
393
+ });
394
+
395
+ // §1.4
396
+ // Schluessel im Output. Aktuell traegt er nur truncated_warnings; weitere
397
+ // Hoist-Kanaele (z.B. truncated_corrections) koennten hier additiv ergaenzt
398
+ // werden ohne Schema-Breaking-Change.
399
+ const metaSchema = z
400
+ .object({
401
+ truncated_warnings: z.array(truncatedWarningEntrySchema).optional(),
402
+ })
403
+ .optional();
404
+
405
+ // §H10 R11-01: EINE Schema-Quelle für das Existenz-Register (analyze + inspect).
406
+ // Plain zod, optional-by-default (Feld fehlt, wenn nichts geskippt wurde).
407
+ const hiddenElementsSchema = z
408
+ .array(
409
+ z.object({
410
+ id: z.string().nullable(),
411
+ tag: z.string(),
412
+ axis: z.enum(['display:none', 'visibility:hidden', 'opacity:0']),
413
+ }),
414
+ )
415
+ .optional();
416
+
417
+ export const analyzeOutput = {
418
+ status: z.enum(['PASS', 'FAIL', 'PARTIAL']),
419
+ iteration: iterationSchema,
420
+ scene: z.object({
421
+ width: z.number(),
422
+ height: z.number(),
423
+ grid: z.string(),
424
+ elements: z.array(elementSchema),
425
+ // §HEAL-R6 / T1 "das Kabel" (F-AT-6-08): Canvas-Validität aus dem
426
+ // honesty.js#classifyCanvas-Verdikt. 'lossy' ⇒ DOMPurify hat Semantik
427
+ // entfernt (resolved.sanitize_loss non-empty) → referenzierende Messung
428
+ // kann von der Quelle abweichen. OPTIONAL (kein required → keine
429
+ // Fixture-Brüche); die Garantie sichern Tests, nicht required. Plain zod
430
+ // (KEIN ZodEffects) → automatisch in den registrierten outputSchemas, kein
431
+ // Laufzeit-Reject. Selbes additives Muster wie inspectOutput.scene.suppressed.
432
+ canvas_validity: z
433
+ .enum(['valid', 'default_replaced', 'degenerate', 'lossy'])
434
+ .optional(),
435
+ // §H10 R11-01: Existenz-Register — css-unsichtbar geskippte Elemente
436
+ // (id+Achse), NICHT Teil von scene.elements (Emissions-Menge byte-stabil).
437
+ // Optional-by-default (Muster canvas_validity); id null bei Auto-id-losen.
438
+ hidden_elements: hiddenElementsSchema,
439
+ // §HEAL-7/C (Codex MCP-Wahrheitsgrenze): scene-level Mess-Ausfall-Marker
440
+ // (§HEAL-4 MK3, pipeline.js#withMeasureUnavailable). OHNE Deklaration
441
+ // strippte der zod-Boundary-Parse (SDK mcp.js: safeParseAsync gegen
442
+ // z.object(outputSchema), unknownKeys='strip') das Feld STILL und der
443
+ // tools/list-JSON-Schema-Dump (additionalProperties:false) verwarf es
444
+ // ajv-seitig (-32602-Klasse) — der laute Ausfall war an der MCP-Grenze
445
+ // unsichtbar (nur der Prosa-Kanal trug ihn). z.literal: genau EIN Wert;
446
+ // optional (Normalfall: Feld absent). Plain zod, kein ZodEffects.
447
+ media_measure: z.literal('MEDIA_MEASURE_UNAVAILABLE').optional(),
448
+ }),
449
+ corrections: z.array(correctionSchema),
450
+ unchecked: z.array(uncheckedEntrySchema),
451
+ diff: z.array(diffEntrySchema),
452
+ meta: metaSchema,
453
+ // §6 RELAIS: isError-Pfade von analyze/compare (Render-Fehler ohne Verlust,
454
+ // compare-No-Baseline) tragen error{code,hint} — sonst absent.
455
+ error: errorEnvelopeSchema,
456
+ };
457
+
458
+ export const inspectOutput = {
459
+ scene: z.object({
460
+ width: z.number(),
461
+ height: z.number(),
462
+ grid: z.string(),
463
+ elements: z.array(elementSchema),
464
+ // §E1 D-008 (F-TF-008): ehrlicher ELEMENT-Trunkierungs-Zaehler, required
465
+ // (wie analyze iterationSchema.suppressed). inspect hat KEINEN iteration-
466
+ // Block, daher gehoert der Zaehler in scene (er beschreibt scene.elements,
467
+ // die bei >SCENE_MAX_ELEMENTS via slice(0,7) gekuerzt werden). ≤7 → 0.
468
+ suppressed: z.number().int(),
469
+ // §HEAL-R6 / T1 "das Kabel" (F-AT-6-08): Canvas-Validität (classifyCanvas).
470
+ // Selbes additive optional-Muster wie analyzeOutput.scene.canvas_validity —
471
+ // inspect speist es ebenfalls aus resolved.sanitize_loss (Anti-LECK-3).
472
+ canvas_validity: z
473
+ .enum(['valid', 'default_replaced', 'degenerate', 'lossy'])
474
+ .optional(),
475
+ // §H10 R11-01: Existenz-Register auch im inspect-Pfad (Kanal-Parität).
476
+ hidden_elements: hiddenElementsSchema,
477
+ // §HEAL-7/C: Mess-Ausfall-Marker auch im inspect-Pfad (withMeasureUnavailable
478
+ // läuft in analyze() UND inspect()) — selbes Muster wie analyzeOutput.scene.
479
+ media_measure: z.literal('MEDIA_MEASURE_UNAVAILABLE').optional(),
480
+ }),
481
+ meta: metaSchema,
482
+ // §6 RELAIS: isError-Pfad von inspect (Render-Fehler ohne Verlust).
483
+ error: errorEnvelopeSchema,
484
+ };
485
+
486
+ // §1.4
487
+ // Validator-Variante des analyzeOutput-Shapes mit REGEL-3-Postcondition:
488
+ // Wenn corrections[i].element === scene.elements[j].id (modulo '#'-Praefix)
489
+ // UND scene.elements[j].bbox_reliability ∈ {'not_measurable','approximate'}
490
+ // → corrections[i] DARF KEINE dx/dy enthalten.
491
+ // Wird NICHT als outputSchema im MCP-Server-Pfad verwendet (MCP-SDK's
492
+ // normalizeObjectSchema kann mit ZodEffects nicht umgehen), sondern dient
493
+ // als Drift-Verhinderungs-Gate fuer test_regel3_invariants.js (Edit H) und
494
+ // als Compound-Schema fuer Property-Based-Tests. Der tatsaechliche Defekt-
495
+ // Fix fuer β-002 sitzt in structured.js (Edit F) — dort wird das dx/dy
496
+ // proaktiv unterdrueckt, BEVOR es das Schema erreichen kann. Das hier ist
497
+ // der Mechanismus, der diese Unterdrueckung mechanisch UEBERPRUEFT.
498
+ export const analyzeOutputCompound = z
499
+ .object(analyzeOutput)
500
+ .superRefine((data, ctx) => {
501
+ if (!data.scene || !Array.isArray(data.scene.elements)) return;
502
+ if (!Array.isArray(data.corrections)) return;
503
+ const reliabilityById = new Map();
504
+ for (const el of data.scene.elements) {
505
+ reliabilityById.set(el.id, el.bbox_reliability);
506
+ }
507
+ for (let i = 0; i < data.corrections.length; i++) {
508
+ const c = data.corrections[i];
509
+ if (!c || typeof c.element !== 'string') continue;
510
+ const id = c.element.replace(/^#/, '');
511
+ const reliability = reliabilityById.get(id);
512
+ // Element-Lookup-Fehlschlag (z.B. correction.element zeigt auf nicht-
513
+ // gelistete Scene-Element-ID wegen slice(0,7)-Cap): das ist ein
514
+ // separater REGEL-3-Bruch (β-003), wird in structured.js Edit F
515
+ // angegangen. Hier kein addIssue — sonst doppelte Meldung.
516
+ if (reliability === undefined) continue;
517
+ if (reliability === 'not_measurable' || reliability === 'approximate') {
518
+ const hasDx = c.dx !== undefined;
519
+ const hasDy = c.dy !== undefined;
520
+ if (hasDx || hasDy) {
521
+ ctx.addIssue({
522
+ code: z.ZodIssueCode.custom,
523
+ path: ['corrections', i],
524
+ message:
525
+ `REGEL-3: corrections[${i}].element='${c.element}' verweist auf scene.elements mit ` +
526
+ `bbox_reliability='${reliability}' — dx/dy duerfen nicht emittiert werden (Spotter-Anti-Luege).`,
527
+ });
528
+ }
529
+ }
530
+ }
531
+ });
532
+
533
+ export const paletteOutput = {
534
+ colors: z.array(
535
+ z.object({
536
+ id: z.string(),
537
+ fill: z.string(),
538
+ stroke: z.string().nullable(),
539
+ }),
540
+ ),
541
+ // §6 RELAIS: isError-Pfad von palette (Render-Fehler).
542
+ error: errorEnvelopeSchema,
543
+ };
544
+
545
+ const constraintTypeSchema = z.object({
546
+ type: z.string(),
547
+ syntax: z.string(),
548
+ hasArrange: z.boolean(),
549
+ });
550
+
551
+ export const constraintsOutput = {
552
+ types: z.array(constraintTypeSchema),
553
+ };
554
+
555
+ export const arrangeOutput = {
556
+ attributes: z.record(
557
+ z.string(),
558
+ z.record(z.string(), z.union([z.number(), z.string()])),
559
+ ),
560
+ warnings: z.array(z.string()),
561
+ // §6 RELAIS: isError-Pfad von arrange (Handler-Catch, ARRANGE_FAILED).
562
+ error: errorEnvelopeSchema,
563
+ };
564
+
565
+ // §1.4 Globale Bookmarks (B-3): Output für vector_mirror_bookmark.
566
+ // analysisId bleibt UUID (Quell-ID; KORR-2 — kein Name im Output).
567
+ export const bookmarkOutput = {
568
+ name: z.string(),
569
+ analysisId: z.string().uuid(),
570
+ stored: z.boolean(),
571
+ bookmarkCount: z.number().int().nonnegative(),
572
+ // §6 RELAIS: isError-Pfad von bookmark (unbekannte/verdrängte analysisId).
573
+ error: errorEnvelopeSchema,
574
+ };
575
+
576
+ const breakerStatsSchema = z
577
+ .object({
578
+ name: z.string(),
579
+ state: z.enum(['closed', 'open', 'half-open']),
580
+ fires: z.number().int(),
581
+ successes: z.number().int(),
582
+ failures: z.number().int(),
583
+ timeouts: z.number().int(),
584
+ rejects: z.number().int(),
585
+ fallbacks: z.number().int(),
586
+ semaphoreRejections: z.number().int(),
587
+ latencyMean: z.number(),
588
+ })
589
+ .nullable();
590
+
591
+ // §1.9 Eichkörper-Selftest: Kalibrierungs-Stand im status-Output. nullable +
592
+ // optional → kein Bruch bestehender E2E-Roundtrip-Asserts (Feld fehlt/null bis
593
+ // der erste Selftest lief). PENDING = Auto-Selftest läuft noch (fire-and-forget
594
+ // nach connect). MCP-SDK-tauglich (kein ZodEffects).
595
+ const calibrationSchema = z
596
+ .object({
597
+ status: z.enum(['PASS', 'FAIL', 'PENDING']),
598
+ calibrated: z.number().int(),
599
+ total: z.number().int(),
600
+ timestamp: z.string(),
601
+ })
602
+ .nullable()
603
+ .optional();
604
+
605
+ export const statusOutput = {
606
+ version: z.string(),
607
+ browser: z.enum(['running', 'stopped']),
608
+ lastAnalysis: z.boolean(),
609
+ constraintTypes: z.number().int(),
610
+ breaker: breakerStatsSchema,
611
+ calibration: calibrationSchema,
612
+ };
613
+
614
+ // §1.9 Eichkörper-Selftest: Output-Schema für vector_mirror_selftest. Trägt das
615
+ // Kalibrierungs-Verdikt + die Abweichungs-Liste (anti-zirk Spec-Mismatches).
616
+ export const selftestOutput = {
617
+ status: z.enum(['PASS', 'FAIL']),
618
+ calibrated: z.number().int(),
619
+ total: z.number().int(),
620
+ failures: z.array(
621
+ z.object({
622
+ ek: z.string(),
623
+ reason: z.string(),
624
+ }),
625
+ ),
626
+ };
@@ -0,0 +1,57 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * server.js - MCP Server Entry Point (stdio)
4
+ * Vector Mirror v2.0 Phase 2
5
+ *
6
+ * Interface module: ONLY transport setup. No business logic.
7
+ * BAUPLAN ref: Sektion 7.2 (SDK-Pattern), 7.3 (Architecture Separation), 7.4 (Config)
8
+ * DEPENDS: @modelcontextprotocol/sdk, interface/tools.js, pipeline.js
9
+ */
10
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
11
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
12
+ import { markCalibrationPending, runSelftest, shutdown } from '../pipeline.js';
13
+ import { QUICKSTART } from './claims.js';
14
+ import { tools } from './tools.js';
15
+
16
+ // §RELAIS §4: instructions-Quickstart als additives 2. Konstruktor-Argument
17
+ // (ServerOptions.instructions, SDK-verifiziert). Inhalt ist eine PROJEKTION
18
+ // des Claims-Registers (claims.js — eine Quelle); Auslieferung an den
19
+ // Konsumenten ist client-abhängig (P5-Ehrlichkeit) — die Tool-Descriptions
20
+ // bleiben selbsttragend, der Quickstart ist Beschleuniger, nicht Voraussetzung.
21
+ const server = new McpServer(
22
+ {
23
+ name: 'vector-mirror',
24
+ version: '1.0.0',
25
+ },
26
+ { instructions: QUICKSTART },
27
+ );
28
+
29
+ // Register all tools from tools.js (BAUPLAN 7.2: registerTool, NOT deprecated tool())
30
+ for (const t of tools) {
31
+ server.registerTool(t.name, t.config, t.handler);
32
+ }
33
+
34
+ // Graceful shutdown: close browser on SIGINT/SIGTERM
35
+ process.on('SIGINT', async () => {
36
+ await shutdown();
37
+ process.exit(0);
38
+ });
39
+ process.on('SIGTERM', async () => {
40
+ await shutdown();
41
+ process.exit(0);
42
+ });
43
+
44
+ // Connect transport and start serving (BAUPLAN 7.2: StdioServerTransport)
45
+ const transport = new StdioServerTransport();
46
+ await server.connect(transport);
47
+
48
+ // §1.9 Auto-Selftest: FIRE-AND-FORGET nach connect (R-1, KEIN eager-blocking
49
+ // init). Der Server bleibt sofort verfügbar; markCalibrationPending() setzt
50
+ // status.calibration='PENDING' BIS der Selftest fertig ist (dann PASS/FAIL).
51
+ // Selftest-Fehler sind NICHT fatal für den Start (z.B. fehlendes Chromium) —
52
+ // der Server läuft lazy weiter, status zeigt den Stand. Kein await: connect
53
+ // ist nicht geblockt, Startup-Latenz bleibt minimal.
54
+ markCalibrationPending();
55
+ runSelftest(false).catch(() => {
56
+ /* Selftest-Fehler nicht fatal: status.calibration bleibt PENDING/letzter Stand. */
57
+ });