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.
- package/LICENSE +21 -0
- package/README.md +249 -0
- package/package.json +65 -0
- package/src/adapters/emitter/prose.js +689 -0
- package/src/adapters/emitter/structured.js +649 -0
- package/src/adapters/renderer/playwright.js +7345 -0
- package/src/core/arbitrate.js +266 -0
- package/src/core/constraints/_schema.js +89 -0
- package/src/core/constraints/aligned.js +42 -0
- package/src/core/constraints/centered-in.js +29 -0
- package/src/core/constraints/color.js +63 -0
- package/src/core/constraints/distance.js +233 -0
- package/src/core/constraints/fill.js +22 -0
- package/src/core/constraints/inside.js +52 -0
- package/src/core/constraints/loader.js +65 -0
- package/src/core/constraints/no-overlap.js +50 -0
- package/src/core/constraints/positional.js +46 -0
- package/src/core/constraints/registry.js +98 -0
- package/src/core/constraints/same-size.js +35 -0
- package/src/core/diff.js +118 -0
- package/src/core/element_vocabulary.js +241 -0
- package/src/core/grid.js +240 -0
- package/src/core/honesty.js +214 -0
- package/src/core/sanitizer/auto_ids.js +104 -0
- package/src/core/tolerance.js +22 -0
- package/src/core/use_graph.js +541 -0
- package/src/interface/claims.js +439 -0
- package/src/interface/schema.js +626 -0
- package/src/interface/server.js +57 -0
- package/src/interface/tools.js +437 -0
- package/src/lib/bbox.js +17 -0
- package/src/lib/breaker.js +240 -0
- package/src/lib/geom.js +144 -0
- package/src/lib/palette.js +236 -0
- package/src/lib/transforms.js +111 -0
- package/src/pipeline.js +1983 -0
|
@@ -0,0 +1,689 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* prose.js — Spotter Report Formatter (Text)
|
|
3
|
+
* Vector Mirror v2.5 (P1-04 migrated)
|
|
4
|
+
*
|
|
5
|
+
* Adapter module: presentation-layer formatting.
|
|
6
|
+
* P1-04 changes:
|
|
7
|
+
* - Consumes tri-state shape: `arbitrated.{failing, unchecked, diff, totals}`.
|
|
8
|
+
* §E1: `arbitrated.failing` ist die EINE (gegatete) Wahrheitsquelle — der
|
|
9
|
+
* Caller (pipeline.js) reicht die honesty.js#gateCorrections-Liste dort
|
|
10
|
+
* rein (symmetrisch zu formatStructured). Kein separater 3.-Arg-Kanal mehr;
|
|
11
|
+
* so leakt KEIN Pixel-Delta (Objekt-Feld NOCH detail-STRING) und der
|
|
12
|
+
* fail-closed-Assert (REGEL-8) bewacht die konsumierte Liste lueckenlos.
|
|
13
|
+
* - Caps applied HERE (top-3 failing, top-3 unchecked, top-2 diff). Total
|
|
14
|
+
* suppressed-count printed; full set lives in structured output.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const FAILING_CAP = 3;
|
|
18
|
+
const UNCHECKED_CAP = 3;
|
|
19
|
+
const DIFF_CAP = 2;
|
|
20
|
+
const SANITIZE_LOSS_CAP = 3;
|
|
21
|
+
// §H9 P1: Längen-Deckel für das Wert-Echo in der Verlust-Zeile (Fremdtext).
|
|
22
|
+
const SANITIZE_LOSS_VALUE_CAP = 40;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* §H9 P1 Echo-Hygiene: der gestrippte Attribut-WERT ist FREMDTEXT (Autor-/
|
|
26
|
+
* Angreifer-kontrolliert) und darf die Report-Grammatik nicht fälschen. EIN
|
|
27
|
+
* Schritt, EIN Prinzip — das Echo bleibt ein Zitat, nie Struktur:
|
|
28
|
+
* Whitespace (inkl. \n\r\t) zu einzelnen Spaces kollabieren (keine geforgte
|
|
29
|
+
* ✗-/STATUS-Zeile), doppelte Anführungszeichen neutralisieren (die
|
|
30
|
+
* `="…"`-Echo-Grammatik bleibt eindeutig), Länge kappen (keine 2000-Zeichen-
|
|
31
|
+
* Bombe). Wahrheit bleibt: ein gekürztes Echo ist ehrlich markiert (…).
|
|
32
|
+
*/
|
|
33
|
+
function sanitizeValueEcho(value) {
|
|
34
|
+
const flat = String(value).replace(/\s+/g, ' ').replace(/"/g, "'");
|
|
35
|
+
return flat.length > SANITIZE_LOSS_VALUE_CAP
|
|
36
|
+
? `${flat.slice(0, SANITIZE_LOSS_VALUE_CAP)}…`
|
|
37
|
+
: flat;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* §H9 K-03/K-24/K-05: EINE Wahrheits-Quelle für die Verlust-Warnzeile
|
|
42
|
+
* (formatReport + formatErrorWithLoss — der frühere Wortlaut war physisch
|
|
43
|
+
* dupliziert UND behauptete hartkodiert "(id/name)" als Ursache, die der
|
|
44
|
+
* Emitter gar nicht kennen konnte). Die Zeile nennt die ECHTEN, vom Caller
|
|
45
|
+
* R9-stabilisierten Ursachen aus sanitizeLoss (tag + reason; bei gestripptem
|
|
46
|
+
* Attribut den konkreten Wert mit, z.B. die verlorene Autor-id). Cap klein
|
|
47
|
+
* (Top 3 + Rest-Zähler). Fehlt die Liste, wird KEINE Ursache geraten —
|
|
48
|
+
* lieber ehrlich generisch als plausibel lügen.
|
|
49
|
+
*
|
|
50
|
+
* @param {Array<{tag:string, reason:string, value?:string}>} [sanitizeLoss]
|
|
51
|
+
* @returns {string} die ⚠-Warnzeile.
|
|
52
|
+
*/
|
|
53
|
+
function sanitizeLossLine(sanitizeLoss) {
|
|
54
|
+
const loss = Array.isArray(sanitizeLoss) ? sanitizeLoss : [];
|
|
55
|
+
const tokens = loss
|
|
56
|
+
.slice(0, SANITIZE_LOSS_CAP)
|
|
57
|
+
.map((l) =>
|
|
58
|
+
l.value !== undefined
|
|
59
|
+
? `${l.tag} (${l.reason}="${sanitizeValueEcho(l.value)}")`
|
|
60
|
+
: `${l.tag} (${l.reason})`,
|
|
61
|
+
);
|
|
62
|
+
const rest = loss.length - tokens.length;
|
|
63
|
+
const head =
|
|
64
|
+
tokens.length > 0
|
|
65
|
+
? `Sanitizer hat entfernt: ${tokens.join(' · ')}${rest > 0 ? ` · +${rest} weitere` : ''}`
|
|
66
|
+
: 'Sanitizer hat Inhalt entfernt';
|
|
67
|
+
return (
|
|
68
|
+
`⚠ Hinweis: ${head} — ` +
|
|
69
|
+
'Messung referenzierender Elemente kann von der Quelle abweichen (canvas_validity=lossy)'
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function formatReport(gridMap, arbitrated, opts = {}) {
|
|
74
|
+
const lines = [];
|
|
75
|
+
|
|
76
|
+
// §HEAL-R6 / T1 "das Kabel" (F-AT-6-08): die Canvas-Validität aus dem Caller
|
|
77
|
+
// (pipeline.js, aus resolved.sanitize_loss DIREKT via classifyCanvas —
|
|
78
|
+
// Anti-LECK-3). 'lossy' ⇒ DOMPurify hat Inhalt entfernt; die
|
|
79
|
+
// Messung referenzierender Elemente kann von der Quelle abweichen. Wie
|
|
80
|
+
// paintDead/overflow ist das ein HINWEIS, der NIE unter "✓ Alles korrekt"
|
|
81
|
+
// verschwinden darf — reine Durchreichung des Caller-Verdikts, KEINE Mess-Logik.
|
|
82
|
+
// §H10 R11-13: das Verdikt ist VIERWERTIG (valid|default_replaced|degenerate|
|
|
83
|
+
// lossy) — JEDER nicht-valide Wert zählt als Hinweis (Kanal-Parität zu
|
|
84
|
+
// structured.scene.canvas_validity), nicht nur lossy.
|
|
85
|
+
const canvasValidity = opts.canvasValidity;
|
|
86
|
+
const canvasLossy = canvasValidity === 'lossy';
|
|
87
|
+
|
|
88
|
+
// §E1 R3 (Symmetrie + REGEL-8 dicht): prose hat — wie formatStructured — GENAU
|
|
89
|
+
// EINE failing-Wahrheitsquelle: `arbitrated.failing`. Der Caller (pipeline.js)
|
|
90
|
+
// reicht dort die GEGATETE Liste (honesty.js#gateCorrections) rein; es gibt
|
|
91
|
+
// keinen zweiten (ungegateten) Kanal und keinen klobigen 3.-Arg-Vertrag mehr.
|
|
92
|
+
// Der fail-closed-Assert (REGEL-8) bewacht damit die NATUERLICHE Aufruf-Form:
|
|
93
|
+
// jedes konsumierte failing-issue MUSS _gated tragen, sonst Wurf — eine
|
|
94
|
+
// ungegatete Emission ist mechanisch unmoeglich (kein `|| []`-Kollaps mehr,
|
|
95
|
+
// der den Assert umgeht). Gegatete Derivate sind um dx/dy/dw/dh UND um die
|
|
96
|
+
// Korrektur-Vorschreibung im detail-String bereinigt → kein Leak in beiden
|
|
97
|
+
// Kanaelen (FAILING-Top-Liste + Element-Baum).
|
|
98
|
+
const failing = arbitrated.failing || [];
|
|
99
|
+
for (const issue of failing) {
|
|
100
|
+
if (issue._gated === undefined) {
|
|
101
|
+
throw new Error(
|
|
102
|
+
'formatReport: ungegatetes failing-issue (kein _gated) — ' +
|
|
103
|
+
'gateCorrections am Emissions-Rand fehlt (REGEL-8 fail-closed).',
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
const unchecked = arbitrated.unchecked || [];
|
|
108
|
+
const diff = arbitrated.diff || [];
|
|
109
|
+
|
|
110
|
+
const failingCount = failing.length;
|
|
111
|
+
const uncheckedCount = unchecked.length;
|
|
112
|
+
const diffCount = diff.length;
|
|
113
|
+
|
|
114
|
+
// \u00A7HEAL-R6 / T1 PROSA-EHRLICHKEIT (F-AT-6-01, DoD-2): die Tinten-tot-Existenz
|
|
115
|
+
// (paint_visible:false / PAINT_NOT_VISIBLE) ist im structured-Kanal bereits
|
|
116
|
+
// ehrlich gemeldet \u2014 der PROSA-Kanal l\u00FCgt aber weiter ("red \u2713 / Alles korrekt").
|
|
117
|
+
// KEINE neue Mess-Logik: das Signal kommt fertig aus gridMap.elements (Renderer
|
|
118
|
+
// \u2192 grid.js-Durchreichung). KEIN erfundener Constraint-Fehler \u2014 nur die
|
|
119
|
+
// Existenz unsichtbarer Tinte sichtbar machen, konsistent zur Hinweis-Mechanik.
|
|
120
|
+
const { canvas, elements } = gridMap;
|
|
121
|
+
const paintDeadCount = elements.filter(isPaintDead).length;
|
|
122
|
+
// §HEAL-R6 Variante 1 PROSA-EHRLICHKEIT (F-AT-6-07, DoD-2-Schwanz): ein
|
|
123
|
+
// paint_visible:'indeterminate'-Element (räumlicher Operator present, Tinten-
|
|
124
|
+
// Präsenz raster-frei NICHT entscheidbar) darf NIE unter "✓ Alles korrekt"
|
|
125
|
+
// verschwinden — sonst lügt die Prosa weiter. Symmetrisch zu paintDeadCount,
|
|
126
|
+
// reine Durchreichung (gridMap.elements). Disjunkt zu isPaintDead.
|
|
127
|
+
const paintIndeterminateCount = elements.filter(isPaintIndeterminate).length;
|
|
128
|
+
// \u00A7HEAL-R6 / T2 PROSA-EHRLICHKEIT (F-AT-6-02/03, DoD-3): has_paint_overflow ist
|
|
129
|
+
// im structured-Kanal ehrlich (Tinte \u2014 Filter/stroke/Marker \u2014 ragt \u00FCber die als
|
|
130
|
+
// reliable gemeldete geom-bbox hinaus), der PROSA-Kanal verschwieg es aber
|
|
131
|
+
// ("\u2713 Alles korrekt"). KEINE neue Mess-Logik: das Signal kommt fertig aus
|
|
132
|
+
// gridMap.elements (Renderer \u2192 grid.js). Symmetrisch zur paintDead-Mechanik: ein
|
|
133
|
+
// Overflow z\u00E4hlt als Hinweis und darf NIE unter "\u2713 Alles korrekt" verschwinden.
|
|
134
|
+
const paintOverflowCount = elements.filter(isPaintOverflow).length;
|
|
135
|
+
// \u00A7D5 / R6-STATE PROSA-EHRLICHKEIT: ein zustands-abh\u00E4ngiges Element (interaktiver
|
|
136
|
+
// Alt-Zustand existiert) darf NIE unter \u201E\u2713 Alles korrekt" verschwinden \u2014 sonst
|
|
137
|
+
// l\u00FCgt die Prosa weiter. Symmetrisch zu paintOverflowCount, reine Durchreichung.
|
|
138
|
+
const stateDependentCount = elements.filter(isStateDependent).length;
|
|
139
|
+
// §F-AT-6-09 / R6-MEDIA PROSA-EHRLICHKEIT: ein viewport-abhängiges Element (ein
|
|
140
|
+
// anderer Viewport rendert es anders) darf NIE unter „✓ Alles korrekt" verschwinden
|
|
141
|
+
// — sonst lügt die Prosa weiter. Symmetrisch zu stateDependentCount, reine Durchreichung.
|
|
142
|
+
const mediaDependentCount = elements.filter(isMediaDependent).length;
|
|
143
|
+
// §HEAL-5 / Zeit-Achse PROSA-EHRLICHKEIT: ein zeit-variantes Element (die
|
|
144
|
+
// Geometrie ist an anderem t anders) darf NIE unter „✓ Alles korrekt"
|
|
145
|
+
// verschwinden — sonst lügt die Prosa weiter. Symmetrisch zu
|
|
146
|
+
// mediaDependentCount, reine Durchreichung.
|
|
147
|
+
const motionDependentCount = elements.filter(isMotionDependent).length;
|
|
148
|
+
// §H10 R11-06 / Paint-Zeit-Achse PROSA-EHRLICHKEIT: ein paint-zeit-variantes
|
|
149
|
+
// Element (Farbe/Opacity an anderem t anders) darf NIE unter „✓ Alles
|
|
150
|
+
// korrekt" verschwinden. Symmetrisch zu motionDependentCount, reine
|
|
151
|
+
// Durchreichung.
|
|
152
|
+
const paintTimeVariantCount = elements.filter(isPaintTimeVariant).length;
|
|
153
|
+
// \u00A7F-AT-7-02 PROSA-EHRLICHKEIT (SK5): ein stroke-Farb-Element (sichtbare Farbe aus
|
|
154
|
+
// dem stroke) bzw. ein Mehrfach-Quellen-Element darf NIE unter \u201E\u2713 Alles korrekt"
|
|
155
|
+
// verschwinden \u2014 sonst l\u00FCgt die Prosa weiter (\u201Etransparent \u2713"). Symmetrisch zu
|
|
156
|
+
// stateDependentCount/mediaDependentCount, reine Durchreichung (gridMap.elements).
|
|
157
|
+
const colorFromStrokeCount = elements.filter(isColorFromStroke).length;
|
|
158
|
+
const multiplePaintSourcesCount =
|
|
159
|
+
elements.filter(isMultiplePaintSources).length;
|
|
160
|
+
|
|
161
|
+
// 1. STATUS \u2014 paintDead + paintOverflow z\u00E4hlen als Hinweis (d\u00FCrfen NIE unter
|
|
162
|
+
// "\u2713 Alles korrekt" verschwinden). Folgt der bestehenden Hinweis-Logik (wie diffCount).
|
|
163
|
+
// \u00A7HEAL-R6 / T1: ein lossy-Canvas z\u00E4hlt als zus\u00E4tzlicher Hinweis (1) und darf
|
|
164
|
+
// \u2014 wie paintDead/overflow \u2014 NIE unter "\u2713 Alles korrekt" oder die reine
|
|
165
|
+
// "ungepr\u00FCft"-Zeile verschwinden. \u00A7H10 R11-13: gleiche Mechanik f\u00FCr ALLE
|
|
166
|
+
// nicht-validen canvas_validity-Werte (degenerate/default_replaced) \u2014 was
|
|
167
|
+
// structured flaggt, flaggt die Prosa (Parit\u00E4t, kein Schwere-Urteil).
|
|
168
|
+
// canvasNoteCount ist 0|1.
|
|
169
|
+
const canvasNoteCount =
|
|
170
|
+
canvasValidity !== undefined && canvasValidity !== 'valid' ? 1 : 0;
|
|
171
|
+
if (
|
|
172
|
+
failingCount === 0 &&
|
|
173
|
+
uncheckedCount === 0 &&
|
|
174
|
+
diffCount === 0 &&
|
|
175
|
+
paintDeadCount === 0 &&
|
|
176
|
+
paintIndeterminateCount === 0 &&
|
|
177
|
+
paintOverflowCount === 0 &&
|
|
178
|
+
stateDependentCount === 0 &&
|
|
179
|
+
mediaDependentCount === 0 &&
|
|
180
|
+
motionDependentCount === 0 &&
|
|
181
|
+
paintTimeVariantCount === 0 &&
|
|
182
|
+
colorFromStrokeCount === 0 &&
|
|
183
|
+
multiplePaintSourcesCount === 0 &&
|
|
184
|
+
canvasNoteCount === 0
|
|
185
|
+
) {
|
|
186
|
+
lines.push('STATUS: \u2713 Alles korrekt');
|
|
187
|
+
} else if (
|
|
188
|
+
failingCount === 0 &&
|
|
189
|
+
uncheckedCount > 0 &&
|
|
190
|
+
diffCount === 0 &&
|
|
191
|
+
paintDeadCount === 0 &&
|
|
192
|
+
paintIndeterminateCount === 0 &&
|
|
193
|
+
paintOverflowCount === 0 &&
|
|
194
|
+
stateDependentCount === 0 &&
|
|
195
|
+
mediaDependentCount === 0 &&
|
|
196
|
+
motionDependentCount === 0 &&
|
|
197
|
+
paintTimeVariantCount === 0 &&
|
|
198
|
+
colorFromStrokeCount === 0 &&
|
|
199
|
+
multiplePaintSourcesCount === 0 &&
|
|
200
|
+
canvasNoteCount === 0
|
|
201
|
+
) {
|
|
202
|
+
lines.push(
|
|
203
|
+
`STATUS: ${uncheckedCount} ungepr\u00FCft (Spotter blind, siehe structured)`,
|
|
204
|
+
);
|
|
205
|
+
} else {
|
|
206
|
+
const parts = [];
|
|
207
|
+
if (failingCount > 0) parts.push(`${failingCount} Fehler`);
|
|
208
|
+
if (uncheckedCount > 0) parts.push(`${uncheckedCount} ungepr\u00FCft`);
|
|
209
|
+
// Tinten-tot + Overflow + Diff + Sanitize-Verlust sind alle "Hinweise" (keine
|
|
210
|
+
// Spotter-Korrektur) \u2192 in EINEN Hinweis-Z\u00E4hler falten, damit die Zeile nicht
|
|
211
|
+
// mehrere Hinweis-Begriffe tr\u00E4gt. paintDead/paintOverflow/lossy bekommen
|
|
212
|
+
// zus\u00E4tzlich einen sprechenden Suffix-Vermerk.
|
|
213
|
+
const hinweisCount =
|
|
214
|
+
diffCount +
|
|
215
|
+
paintDeadCount +
|
|
216
|
+
paintIndeterminateCount +
|
|
217
|
+
paintOverflowCount +
|
|
218
|
+
stateDependentCount +
|
|
219
|
+
mediaDependentCount +
|
|
220
|
+
motionDependentCount +
|
|
221
|
+
paintTimeVariantCount +
|
|
222
|
+
colorFromStrokeCount +
|
|
223
|
+
multiplePaintSourcesCount +
|
|
224
|
+
canvasNoteCount;
|
|
225
|
+
if (hinweisCount > 0)
|
|
226
|
+
parts.push(`${hinweisCount} Hinweis${hinweisCount > 1 ? 'e' : ''}`);
|
|
227
|
+
const noteParts = [];
|
|
228
|
+
if (paintDeadCount > 0) noteParts.push(`${paintDeadCount} unsichtbar`);
|
|
229
|
+
if (paintIndeterminateCount > 0)
|
|
230
|
+
noteParts.push(`${paintIndeterminateCount} Sichtbarkeit unbestimmt`);
|
|
231
|
+
if (paintOverflowCount > 0)
|
|
232
|
+
noteParts.push(`${paintOverflowCount} Tinten-\u00DCberlauf`);
|
|
233
|
+
if (stateDependentCount > 0)
|
|
234
|
+
noteParts.push(`${stateDependentCount} zustands-abh\u00E4ngig`);
|
|
235
|
+
if (mediaDependentCount > 0)
|
|
236
|
+
noteParts.push(`${mediaDependentCount} viewport-abh\u00E4ngig`);
|
|
237
|
+
if (motionDependentCount > 0)
|
|
238
|
+
noteParts.push(`${motionDependentCount} zeit-variant`);
|
|
239
|
+
if (paintTimeVariantCount > 0)
|
|
240
|
+
noteParts.push(`${paintTimeVariantCount} paint-zeit-variant`);
|
|
241
|
+
if (colorFromStrokeCount > 0)
|
|
242
|
+
noteParts.push(`${colorFromStrokeCount} Farbe aus Rand`);
|
|
243
|
+
if (multiplePaintSourcesCount > 0)
|
|
244
|
+
noteParts.push(`${multiplePaintSourcesCount} mehrere Farbquellen`);
|
|
245
|
+
// §H10 R11-13: je nicht-valider canvas_validity-Wert ein sprechendes Token
|
|
246
|
+
// (Kanal-Parität: was structured flaggt, flaggt die Prosa). Unbekannte
|
|
247
|
+
// künftige Enum-Werte passieren NIE still (generisches Echo).
|
|
248
|
+
if (canvasNoteCount > 0) {
|
|
249
|
+
if (canvasLossy) noteParts.push('Sanitize-Verlust');
|
|
250
|
+
else if (canvasValidity === 'degenerate')
|
|
251
|
+
noteParts.push('Canvas degeneriert');
|
|
252
|
+
else if (canvasValidity === 'default_replaced')
|
|
253
|
+
noteParts.push(`Canvas-Default ${canvas.width}×${canvas.height}`);
|
|
254
|
+
else noteParts.push(`Canvas ${canvasValidity}`);
|
|
255
|
+
}
|
|
256
|
+
const suffix =
|
|
257
|
+
failingCount > 0 || uncheckedCount > 0
|
|
258
|
+
? ' (Spotter-Korrektur aktiv)'
|
|
259
|
+
: noteParts.length > 0
|
|
260
|
+
? ` (${noteParts.join(', ')}, siehe structured)`
|
|
261
|
+
: '';
|
|
262
|
+
lines.push(`STATUS: ${parts.join(', ')}${suffix}`);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
// \u00A7HEAL-R6 / T1: die laute Verlust-Zeile. Direkt nach STATUS, damit sie der
|
|
266
|
+
// LLM nie \u00FCbersieht. \u00A7H9 K-03/K-24/K-05: sie nennt die ECHTEN Ursachen aus
|
|
267
|
+
// opts.sanitizeLoss (eine Wahrheits-Quelle: sanitizeLossLine) statt der
|
|
268
|
+
// fr\u00FCheren hartkodierten Behauptung "(id/name)". Erscheint NUR bei lossy
|
|
269
|
+
// (Negativ-Kontrolle: valid \u2192 keine Zeile).
|
|
270
|
+
// §H10 R11-13: degenerate/default_replaced tragen ihre eigene Wahrheits-Zeile
|
|
271
|
+
// (gemessener Sachverhalt + canvas_validity-Zeiger auf structured) — gleiche
|
|
272
|
+
// Mechanik, Hinweis statt Tadel (CSS-Default ist spec-konform).
|
|
273
|
+
if (canvasLossy) {
|
|
274
|
+
lines.push(sanitizeLossLine(opts.sanitizeLoss));
|
|
275
|
+
} else if (canvasValidity === 'degenerate') {
|
|
276
|
+
lines.push(
|
|
277
|
+
'⚠ Hinweis: viewBox degeneriert (nicht parsebar oder Breite/Höhe ≤0) — ' +
|
|
278
|
+
'gemeldete Koordinaten beziehen sich auf einen Renderer-Fallback (canvas_validity=degenerate)',
|
|
279
|
+
);
|
|
280
|
+
} else if (canvasValidity === 'default_replaced') {
|
|
281
|
+
lines.push(
|
|
282
|
+
`⚠ Hinweis: SVG ohne width/height/viewBox — CSS-Default ${canvas.width}×${canvas.height} ` +
|
|
283
|
+
'ersetzt die fehlende Deklaration (canvas_validity=default_replaced)',
|
|
284
|
+
);
|
|
285
|
+
} else if (canvasNoteCount > 0) {
|
|
286
|
+
lines.push(
|
|
287
|
+
`⚠ Hinweis: Canvas nicht valide (canvas_validity=${canvasValidity}, siehe structured)`,
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
// 2a. FAILING (Top 3)
|
|
292
|
+
const shownFailing = failing.slice(0, FAILING_CAP);
|
|
293
|
+
for (const issue of shownFailing) {
|
|
294
|
+
lines.push(
|
|
295
|
+
`\u2717 ${issue.detail || `${issue.constraintType || 'CONSTRAINT'} #${issue.id || '?'}`}`,
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// 2b. UNCHECKED (Top 3) — visible reason, optional suggestion
|
|
300
|
+
const shownUnchecked = unchecked.slice(0, UNCHECKED_CAP);
|
|
301
|
+
for (const u of shownUnchecked) {
|
|
302
|
+
const tail = u.suggestedCorrection
|
|
303
|
+
? ` (vielleicht: ${u.suggestedCorrection})`
|
|
304
|
+
: '';
|
|
305
|
+
const ref = u.id !== undefined ? `#${u.id}: ` : '';
|
|
306
|
+
lines.push(`? ${ref}${u.hint}${tail}`);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// 2c. DIFF (Top 2) — szene-aenderungen
|
|
310
|
+
const shownDiff = diff.slice(0, DIFF_CAP);
|
|
311
|
+
for (const d of shownDiff) {
|
|
312
|
+
switch (d.type) {
|
|
313
|
+
case 'VERSCHOBEN':
|
|
314
|
+
lines.push(`\u25B3 #${d.id}: ${d.from} \u2192 ${d.to}`);
|
|
315
|
+
break;
|
|
316
|
+
case 'FARB\u00C4NDERUNG':
|
|
317
|
+
lines.push(`\u25B3 #${d.id}: Farbe ${d.from} \u2192 ${d.to}`);
|
|
318
|
+
break;
|
|
319
|
+
case 'FORM\u00C4NDERUNG':
|
|
320
|
+
lines.push(`\u25B3 #${d.id}: ${d.from} \u2192 ${d.to} (Form)`);
|
|
321
|
+
break;
|
|
322
|
+
case 'NEU':
|
|
323
|
+
lines.push(`+ #${d.id}: neu in ${d.cell} (${d.color})`);
|
|
324
|
+
break;
|
|
325
|
+
case 'ENTFERNT':
|
|
326
|
+
lines.push(`- #${d.id}: entfernt (war in ${d.cell})`);
|
|
327
|
+
break;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// 3. Suppression-Hinweis: nur wenn wir oben gekuerzt haben
|
|
332
|
+
const suppressed =
|
|
333
|
+
failingCount -
|
|
334
|
+
shownFailing.length +
|
|
335
|
+
(uncheckedCount - shownUnchecked.length) +
|
|
336
|
+
(diffCount - shownDiff.length);
|
|
337
|
+
if (suppressed > 0) lines.push(` (${suppressed} weitere siehe structured)`);
|
|
338
|
+
|
|
339
|
+
// 4. SZENE
|
|
340
|
+
lines.push('');
|
|
341
|
+
const vbInfo = canvas.viewBox ? ` (viewBox: ${canvas.viewBox})` : '';
|
|
342
|
+
lines.push(
|
|
343
|
+
`SZENE: ${canvas.width}\u00D7${canvas.height}, ${elements.length} Elemente${vbInfo}`,
|
|
344
|
+
);
|
|
345
|
+
|
|
346
|
+
// \u00A7H10 R11-01: Existenz-Register \u2014 css-unsichtbare Elemente sind nicht Teil
|
|
347
|
+
// der Szene (Emissions-Menge byte-stabil), d\u00FCrfen aber nicht verschwiegen
|
|
348
|
+
// werden (Kanal-Parit\u00E4t zu scene.hidden_elements; Stil der suppressed-Zeile).
|
|
349
|
+
const hiddenCount = Array.isArray(gridMap.hidden) ? gridMap.hidden.length : 0;
|
|
350
|
+
if (hiddenCount > 0)
|
|
351
|
+
lines.push(
|
|
352
|
+
` (${hiddenCount} Element${hiddenCount > 1 ? 'e' : ''} css-unsichtbar \u2014 siehe structured)`,
|
|
353
|
+
);
|
|
354
|
+
|
|
355
|
+
// §HEAL-7/B (F-TF-003): Verdikt-/Korrektur-Attribution per OBJEKT-IDENTITÄT.
|
|
356
|
+
// checkAllConstraints misst per .find() das ERSTE Element einer id — ein
|
|
357
|
+
// failing-issue gehört daher GENAU diesem Objekt. Der reine id-String-
|
|
358
|
+
// Vergleich heftete Korrektur + ✗ auch an UNGEMESSENE Namensvettern
|
|
359
|
+
// (Boden-Wahrheit f003: beide rect#x trugen [dy=2470px] ✗). firstById
|
|
360
|
+
// spiegelt die Mess-Semantik: nur das gemessene Objekt trägt Korrektur/✗.
|
|
361
|
+
// Zweite Verteidigungslinie — die Pipeline-Wache (MEASUREMENT_AMBIGUOUS)
|
|
362
|
+
// lässt ambige ids gar nicht erst in failing. unchecked (⚠) bleibt bewusst
|
|
363
|
+
// id-basiert: eine VERWEIGERTE Messung impliziert ALLE Namensvettern (kein
|
|
364
|
+
// Objekt wurde gemessen, der Vorbehalt gilt dem Namen).
|
|
365
|
+
const firstById = new Map();
|
|
366
|
+
for (const e of elements) {
|
|
367
|
+
if (!firstById.has(e.id)) firstById.set(e.id, e);
|
|
368
|
+
}
|
|
369
|
+
const isMeasuredCarrier = (el) => firstById.get(el.id) === el;
|
|
370
|
+
|
|
371
|
+
// 5. ELEMENT-BAUM (Top 7)
|
|
372
|
+
const maxShow = Math.min(elements.length, 7);
|
|
373
|
+
for (let i = 0; i < maxShow; i++) {
|
|
374
|
+
const el = elements[i];
|
|
375
|
+
const prefix = i < maxShow - 1 ? '\u251C\u2500' : '\u2514\u2500';
|
|
376
|
+
// \u00A7HEAL-R6 / T1: Tinten-tot ist ORTHOGONAL zum Constraint-Verdikt. getStatus
|
|
377
|
+
// bleibt die Constraint-Wahrheit (\u2717 fail / \u26A0 unchecked / \u2713 ok); ein bare \u2713
|
|
378
|
+
// wird bei Tinten-Tot zu \u26A0 degradiert (kein falsches "korrekt"), ein echtes \u2717
|
|
379
|
+
// bleibt \u2717 (Constraint-Fehler dominiert). Der sprechende Vermerk h\u00E4ngt IMMER
|
|
380
|
+
// an, wenn paint-tot \u2014 so verschwindet die Unsichtbarkeit in KEINEM Fall.
|
|
381
|
+
const paintDead = isPaintDead(el);
|
|
382
|
+
// \u00A7HEAL-R6 Variante 1: Tinten-Unbestimmtheit ist ORTHOGONAL zum Constraint-
|
|
383
|
+
// Verdikt UND zu paintDead. STRIKT getrennt: ein indeterminate-Element ist NICHT
|
|
384
|
+
// tot (KEINE "unsichtbar"-Behauptung), aber auch NICHT als korrekt-sichtbar zu
|
|
385
|
+
// melden \u2014 ehrlich unbestimmt. Wie paintDead degradiert es ein bare \u2713 zu \u26A0.
|
|
386
|
+
const paintIndeterminate = isPaintIndeterminate(el);
|
|
387
|
+
// \u00A7HEAL-R6 / T2: Tinten-\u00DCberlauf ist ORTHOGONAL zum Constraint-Verdikt UND zu
|
|
388
|
+
// paintDead (ein Element kann beides ODER nur eines tragen). Wie paintDead
|
|
389
|
+
// degradiert ein bare \u2713 zu \u26A0 (kein falsches "korrekt"), ein echtes \u2717 bleibt \u2717.
|
|
390
|
+
const paintOverflow = isPaintOverflow(el);
|
|
391
|
+
// \u00A7D5 / R6-STATE: Zustands-Abh\u00E4ngigkeit ist ORTHOGONAL zum Constraint-Verdikt
|
|
392
|
+
// UND zu paintDead/overflow. Wie diese degradiert sie ein bare \u2713 zu \u26A0 (kein
|
|
393
|
+
// falsches \u201Ekorrekt"), ein echtes \u2717 bleibt \u2717. KEINE \u201Eunsichtbar"-Behauptung.
|
|
394
|
+
const stateDependent = isStateDependent(el);
|
|
395
|
+
// §F-AT-6-09 / R6-MEDIA: Viewport-Abhängigkeit ist ORTHOGONAL zum Constraint-
|
|
396
|
+
// Verdikt UND zu paintDead/overflow/state. Wie diese degradiert sie ein bare
|
|
397
|
+
// Häkchen zu ⚠ (kein falsches "korrekt"), ein echtes ✗ bleibt ✗.
|
|
398
|
+
const mediaDependent = isMediaDependent(el);
|
|
399
|
+
// \u00A7HEAL-5 / Zeit-Achse: Zeit-Varianz ist ORTHOGONAL zum Constraint-Verdikt
|
|
400
|
+
// UND zu paintDead/overflow/state/media. Wie diese degradiert sie ein bare
|
|
401
|
+
// H\u00E4kchen zu \u26A0 (kein falsches "korrekt"), ein echtes \u2717 bleibt \u2717.
|
|
402
|
+
const motionDependent = isMotionDependent(el);
|
|
403
|
+
// §H10 R11-06 / Paint-Zeit-Achse: Paint-Zeit-Varianz ist ORTHOGONAL zum
|
|
404
|
+
// Constraint-Verdikt UND zu motionDependent (Geometrie-Zeit ≠ Paint-Zeit).
|
|
405
|
+
// Wie diese degradiert sie ein bare ✓ zu ⚠, ein echtes ✗ bleibt ✗.
|
|
406
|
+
const paintTimeVariant = isPaintTimeVariant(el);
|
|
407
|
+
// \u00A7F-AT-7-02: die sichtbare Farbe stammt aus dem stroke bzw. mehrere Farbquellen
|
|
408
|
+
// malen sichtbar \u2014 ORTHOGONAL zum Constraint-Verdikt. Wie die anderen Notes
|
|
409
|
+
// degradiert ein bare \u2713 zu \u26A0 (kein falsches \u201Ekorrekt"), ein echtes \u2717 bleibt \u2717.
|
|
410
|
+
const colorFromStroke = isColorFromStroke(el);
|
|
411
|
+
const multiplePaintSources = isMultiplePaintSources(el);
|
|
412
|
+
let status = getStatus(el, failing, unchecked, isMeasuredCarrier(el));
|
|
413
|
+
if (
|
|
414
|
+
(paintDead ||
|
|
415
|
+
paintIndeterminate ||
|
|
416
|
+
paintOverflow ||
|
|
417
|
+
stateDependent ||
|
|
418
|
+
mediaDependent ||
|
|
419
|
+
motionDependent ||
|
|
420
|
+
paintTimeVariant ||
|
|
421
|
+
colorFromStroke ||
|
|
422
|
+
multiplePaintSources) &&
|
|
423
|
+
status === '\u2713'
|
|
424
|
+
)
|
|
425
|
+
status = '\u26A0';
|
|
426
|
+
// Vermerk OHNE eigenes Glyph \u2014 das Status-Glyph (\u26A0) am Zeilenende tr\u00E4gt das
|
|
427
|
+
// Symbol. Der Text macht die Ursache(n) explizit. Beide Vermerke k\u00F6nnen
|
|
428
|
+
// gleichzeitig anh\u00E4ngen (orthogonal). visual_bbox-Zahl, wenn messbar, sonst
|
|
429
|
+
// 'not_measurable' (ehrlich: \u00DCberlauf da, Schranke unsicher). paintDead und
|
|
430
|
+
// paintIndeterminate sind disjunkt (false \u2260 'indeterminate'); der unbestimmte
|
|
431
|
+
// Vermerk behauptet NIE Unsichtbarkeit, nur Unbestimmtheit.
|
|
432
|
+
const paintNote = paintDead
|
|
433
|
+
? ' unsichtbar (PAINT_NOT_VISIBLE)'
|
|
434
|
+
: paintIndeterminate
|
|
435
|
+
? ' Sichtbarkeit unbestimmt (PAINT_PRESENCE_INDETERMINATE)'
|
|
436
|
+
: '';
|
|
437
|
+
const overflowNote = paintOverflow
|
|
438
|
+
? ` Tinten-\u00DCberlauf (has_paint_overflow, visual_bbox=${formatVisualBbox(el.visual_bbox)})`
|
|
439
|
+
: '';
|
|
440
|
+
// \u00A7D5 / R6-STATE: ehrlicher Vermerk, dass interaktive Alt-Zust\u00E4nde existieren
|
|
441
|
+
// (orthogonal, h\u00E4ngt zus\u00E4tzlich an wie die anderen Notes).
|
|
442
|
+
const stateNote = stateDependent
|
|
443
|
+
? ' zustands-abh\u00E4ngig (STATE_DEPENDENT)'
|
|
444
|
+
: '';
|
|
445
|
+
// \u00A7F-AT-6-09 / R6-MEDIA: ehrlicher Vermerk, dass ein anderer Viewport dieses
|
|
446
|
+
// Element anders rendert (orthogonal, h\u00E4ngt zus\u00E4tzlich an wie die anderen Notes).
|
|
447
|
+
const mediaNote = mediaDependent
|
|
448
|
+
? ' viewport-abh\u00E4ngig (MEDIA_DEPENDENT)'
|
|
449
|
+
: '';
|
|
450
|
+
// \u00A7HEAL-5 / Zeit-Achse: ehrlicher Vermerk, dass die Geometrie an anderem t
|
|
451
|
+
// anders ist (orthogonal, h\u00E4ngt zus\u00E4tzlich an wie die anderen Notes).
|
|
452
|
+
const motionNote = motionDependent
|
|
453
|
+
? ' zeit-variant (MOTION_DEPENDENT)'
|
|
454
|
+
: '';
|
|
455
|
+
// §H10 R11-06: ehrlicher Vermerk, dass Farbe/Sichtbarkeit an anderem t
|
|
456
|
+
// anders ist (orthogonal, hängt zusätzlich an wie die anderen Notes).
|
|
457
|
+
const paintTimeNote = paintTimeVariant
|
|
458
|
+
? ' paint-zeit-variant (PAINT_TIME_VARIANT)'
|
|
459
|
+
: '';
|
|
460
|
+
// \u00A7F-AT-7-02 (SK5): ehrlicher Vermerk, dass die sichtbare Farbe aus dem stroke
|
|
461
|
+
// stammt (der fill tr\u00E4gt keine sichtbare Farbe bei) bzw. dass mehrere Quellen
|
|
462
|
+
// sichtbar malen \u2014 orthogonal, h\u00E4ngt zus\u00E4tzlich an wie die anderen Notes.
|
|
463
|
+
const colorNote = colorFromStroke
|
|
464
|
+
? ' Farbe aus Rand (COLOR_FROM_STROKE)'
|
|
465
|
+
: multiplePaintSources
|
|
466
|
+
? ' mehrere Farbquellen (MULTIPLE_PAINT_SOURCES)'
|
|
467
|
+
: '';
|
|
468
|
+
const pos = el.span || `${el.cell}, ${el.direction}`;
|
|
469
|
+
const text = el.textContent ? ` "${el.textContent}"` : '';
|
|
470
|
+
// \u00A7F-AT-7-02 (SK5): bei COLOR_FROM_STROKE IST el.color bereits die stroke-Farbe
|
|
471
|
+
// (grid.js-Projektion) \u2014 das redundante `[Rand: red]`-Suffix w\u00E4re eine doppelte
|
|
472
|
+
// Nennung derselben Farbe und wird unterdr\u00FCckt. Bei MULTIPLE_PAINT_SOURCES ist
|
|
473
|
+
// el.color der fill und der stroke eine ECHT separate sichtbare Farbe \u2192 das
|
|
474
|
+
// Suffix bleibt (es tr\u00E4gt eine zus\u00E4tzliche Wahrheit, keine Redundanz).
|
|
475
|
+
const strokeInfo =
|
|
476
|
+
el.stroke && el.stroke !== 'transparent' && !colorFromStroke
|
|
477
|
+
? ` [Rand: ${el.stroke}]`
|
|
478
|
+
: '';
|
|
479
|
+
const opacityWarn = el.opacity < 0.3 ? ' (fast unsichtbar)' : '';
|
|
480
|
+
|
|
481
|
+
// §E1 D-006/R2: Korrektur-Hinweis aus der GEGATETEN failing-Liste. Bei
|
|
482
|
+
// not_measurable/approximate ist dx/dy/dw/dh entfernt → formatCorrection
|
|
483
|
+
// liefert '' → kein Leak. getStatus laeuft ebenfalls auf der gegateten
|
|
484
|
+
// Liste: das Status-Verdikt (✗) ist reliability-UNABHAENGIG, weil das Gate
|
|
485
|
+
// die issue-Identitaet + WAS-detail erhaelt — nur die Pixel-Vorschreibung
|
|
486
|
+
// faellt weg (Anti-Ueber-Gaten: not_measurable bleibt ✗, ohne Pixel-Hinweis).
|
|
487
|
+
// §HEAL-7/B: nur das GEMESSENE Objekt (erstes seiner id) bekommt ein
|
|
488
|
+
// failing-issue zugeordnet — Namensvettern nie (Objekt-Identität).
|
|
489
|
+
// §H10 R11-28: Attribution id-only, byte-symmetrisch zu structured.js —
|
|
490
|
+
// ein Verdikt heftet an das gemessene Subjekt, nie an detail-erwähnte
|
|
491
|
+
// Dritte (die Referenz-Beteiligung bleibt via FAILING-Top-Zeile sichtbar).
|
|
492
|
+
const issue = isMeasuredCarrier(el)
|
|
493
|
+
? failing.find((iss) => iss.id === el.id)
|
|
494
|
+
: undefined;
|
|
495
|
+
// G2 (D-006): Korrektur-Hinweis wird aus den STRUKTURIERTEN Feldern
|
|
496
|
+
// (dx/dy/dw/dh) gebaut, NICHT mehr per String-Parse aus dem detail-Feld.
|
|
497
|
+
const correctionText = formatCorrection(issue);
|
|
498
|
+
const spotterHint = correctionText ? ` [${correctionText}]` : '';
|
|
499
|
+
|
|
500
|
+
lines.push(
|
|
501
|
+
`${prefix} ${el.tag}#${el.id}: ${pos}, ${el.color}${strokeInfo}${opacityWarn}${paintNote}${overflowNote}${stateNote}${mediaNote}${motionNote}${paintTimeNote}${colorNote}${text}${spotterHint} ${status}`,
|
|
502
|
+
);
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
if (elements.length > maxShow)
|
|
506
|
+
lines.push(` (${elements.length - maxShow} weitere Elemente)`);
|
|
507
|
+
|
|
508
|
+
return lines.join('\n');
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* §HEAL-R6 / T1 Error-Pfad-Ehrlichkeit (F-AT-6-05): die LLM-zugewandte Prosa für
|
|
513
|
+
* ein Error-Resultat, das einen Sanitize-Verlust trägt (canvas_validity=lossy).
|
|
514
|
+
* Die bestehende `Fehler: …`-Zeile bleibt (Render schlug fehl), aber die laute
|
|
515
|
+
* Verlust-Zeile hängt an — sonst verschwände der Verlust still im Error-Pfad.
|
|
516
|
+
* Dieselbe Hinweis-Formulierung wie in formatReport (§H9: jetzt WIRKLICH eine
|
|
517
|
+
* Wahrheits-Quelle — sanitizeLossLine — mit den echten Ursachen).
|
|
518
|
+
*
|
|
519
|
+
* @param {string} message - die resolved.message (Render-Fehler-Text).
|
|
520
|
+
* @param {Array<{tag:string, reason:string, value?:string}>} [sanitizeLoss]
|
|
521
|
+
* @returns {string}
|
|
522
|
+
*/
|
|
523
|
+
export function formatErrorWithLoss(message, sanitizeLoss) {
|
|
524
|
+
return [`Fehler: ${message}`, sanitizeLossLine(sanitizeLoss)].join('\n');
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
export function formatArrangeReport(attributes, warnings) {
|
|
528
|
+
const ids = Object.keys(attributes);
|
|
529
|
+
const lines = [
|
|
530
|
+
`ARRANGE: ${ids.length} Element${ids.length !== 1 ? 'e' : ''} platziert`,
|
|
531
|
+
];
|
|
532
|
+
for (const id of ids) {
|
|
533
|
+
const attrs = attributes[id];
|
|
534
|
+
const parts = Object.entries(attrs).map(([k, v]) =>
|
|
535
|
+
typeof v === 'number' ? `${k}=${Math.round(v)}` : `${k}="${v}"`,
|
|
536
|
+
);
|
|
537
|
+
lines.push(` #${id}: ${parts.join(', ')}`);
|
|
538
|
+
}
|
|
539
|
+
if (warnings.length > 0) {
|
|
540
|
+
lines.push('');
|
|
541
|
+
lines.push(`WARNUNGEN: ${warnings.length}`);
|
|
542
|
+
for (const w of warnings) lines.push(` \u26A0 ${w}`);
|
|
543
|
+
}
|
|
544
|
+
return lines.join('\n');
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/**
|
|
548
|
+
* formatCorrection — baut den Korrektur-Hinweis aus den STRUKTURIERTEN
|
|
549
|
+
* Feldern eines failing-issue (dx/dy/dw/dh), nicht aus dem detail-String.
|
|
550
|
+
* G2 (D-006): Single Source of Truth ist das strukturierte Delta. Liefert
|
|
551
|
+
* '' wenn keine Korrektur-Felder gesetzt sind (z.B. non-spatiale COLOR-fails).
|
|
552
|
+
*/
|
|
553
|
+
function formatCorrection(issue) {
|
|
554
|
+
if (!issue) return '';
|
|
555
|
+
const parts = [];
|
|
556
|
+
if (typeof issue.dx === 'number') parts.push(`dx=${issue.dx}px`);
|
|
557
|
+
if (typeof issue.dy === 'number') parts.push(`dy=${issue.dy}px`);
|
|
558
|
+
if (typeof issue.dw === 'number') parts.push(`dw=${issue.dw}px`);
|
|
559
|
+
if (typeof issue.dh === 'number') parts.push(`dh=${issue.dh}px`);
|
|
560
|
+
return parts.join(', ');
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// §HEAL-R6 / T1: ein Element ist „tinten-tot", wenn der Renderer paint_visible:false
|
|
564
|
+
// gesetzt hat ODER (redundant-robust) die PAINT_NOT_VISIBLE-Warning trägt. KEINE
|
|
565
|
+
// Mess-Logik — reines Durchreichen des bereits gemessenen Signals (Renderer →
|
|
566
|
+
// grid.js → hier). Beide Quellen geprüft, damit das Signal nicht an einer fehlenden
|
|
567
|
+
// Durchreichung verloren geht (fail-loud statt fail-silent).
|
|
568
|
+
function isPaintDead(el) {
|
|
569
|
+
if (!el) return false;
|
|
570
|
+
if (el.paint_visible === false) return true;
|
|
571
|
+
return Array.isArray(el.warnings) && el.warnings.includes('PAINT_NOT_VISIBLE');
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// §HEAL-R6 Variante 1: ein Element ist „tinten-unbestimmt", wenn der Renderer
|
|
575
|
+
// paint_visible:'indeterminate' gesetzt hat (ein räumlicher Operator — clip-path/
|
|
576
|
+
// mask/pattern/filter / nicht-endliche CTM — ist present, aber raster-frei NICHT
|
|
577
|
+
// als tot/lebendig entscheidbar). Analog isPaintDead reine Durchreichung des
|
|
578
|
+
// gemessenen Signals (Renderer → grid.js → hier), KEINE Mess-Logik. Beide Quellen
|
|
579
|
+
// (Feld + Warning) geprüft (fail-loud). STRIKT getrennt von isPaintDead: ein
|
|
580
|
+
// indeterminate-Element wird NIE als „unsichtbar/tot" markiert — nur als unbestimmt.
|
|
581
|
+
function isPaintIndeterminate(el) {
|
|
582
|
+
if (!el) return false;
|
|
583
|
+
if (el.paint_visible === 'indeterminate') return true;
|
|
584
|
+
return (
|
|
585
|
+
Array.isArray(el.warnings) &&
|
|
586
|
+
el.warnings.includes('PAINT_PRESENCE_INDETERMINATE')
|
|
587
|
+
);
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// §HEAL-R6 / T2: ein Element hat „Tinten-Überlauf", wenn der Renderer
|
|
591
|
+
// has_paint_overflow:true gesetzt hat (Filter/stroke/Marker malen über die als
|
|
592
|
+
// reliable gemeldete geom-bbox hinaus). KEINE Mess-Logik — reines Durchreichen
|
|
593
|
+
// (Renderer → grid.js → hier). Symmetrisch zu isPaintDead.
|
|
594
|
+
function isPaintOverflow(el) {
|
|
595
|
+
return !!el && el.has_paint_overflow === true;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// §D5 / R6-STATE: ein Element ist „zustands-abhängig", wenn der Renderer
|
|
599
|
+
// state_dependent:true gesetzt hat (interaktiver Pseudo-Selektor self/Vorfahr ODER
|
|
600
|
+
// SMIL-Event-Token zielt darauf) ODER (redundant-robust) die STATE_DEPENDENT-
|
|
601
|
+
// Warning trägt. KEINE Mess-Logik — reines Durchreichen des bereits gemessenen
|
|
602
|
+
// Signals (Renderer → grid.js → hier). Beide Quellen geprüft (fail-loud statt
|
|
603
|
+
// fail-silent). Symmetrisch zu isPaintDead/isPaintIndeterminate/isPaintOverflow.
|
|
604
|
+
function isStateDependent(el) {
|
|
605
|
+
if (!el) return false;
|
|
606
|
+
if (el.state_dependent === true) return true;
|
|
607
|
+
return Array.isArray(el.warnings) && el.warnings.includes('STATE_DEPENDENT');
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
// §F-AT-6-09 / R6-MEDIA: ein Element ist „viewport-abhängig", wenn der Renderer
|
|
611
|
+
// media_dependent:true gesetzt hat (Selektor in einem Viewport-@media trifft es)
|
|
612
|
+
// ODER (redundant-robust) die MEDIA_DEPENDENT-Warning trägt. KEINE Mess-Logik —
|
|
613
|
+
// reines Durchreichen des bereits gemessenen Signals (Renderer → grid.js → hier).
|
|
614
|
+
// Beide Quellen geprüft (fail-loud). Symmetrisch zu isStateDependent.
|
|
615
|
+
function isMediaDependent(el) {
|
|
616
|
+
if (!el) return false;
|
|
617
|
+
if (el.media_dependent === true) return true;
|
|
618
|
+
return Array.isArray(el.warnings) && el.warnings.includes('MEDIA_DEPENDENT');
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// §HEAL-5 / Zeit-Achse: ein Element ist „zeit-variant", wenn der Renderer
|
|
622
|
+
// motion_dependent:true gesetzt hat (clock-rooted SMIL-GEOMETRIE zielt darauf)
|
|
623
|
+
// ODER (redundant-robust) die MOTION_DEPENDENT-Warning trägt. KEINE Mess-Logik —
|
|
624
|
+
// reines Durchreichen des bereits gemessenen Signals (Renderer → grid.js →
|
|
625
|
+
// hier). Beide Quellen geprüft (fail-loud). Symmetrisch zu isMediaDependent.
|
|
626
|
+
function isMotionDependent(el) {
|
|
627
|
+
if (!el) return false;
|
|
628
|
+
if (el.motion_dependent === true) return true;
|
|
629
|
+
return Array.isArray(el.warnings) && el.warnings.includes('MOTION_DEPENDENT');
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
// §H10 R11-06 / Paint-Zeit-Achse: ein Element ist „paint-zeit-variant", wenn der
|
|
633
|
+
// Renderer paint_time_variant:true gesetzt hat (clock-rooted SMIL auf einem
|
|
634
|
+
// NICHT-Geometrie-Kanal: fill/opacity/…) ODER (redundant-robust) die
|
|
635
|
+
// PAINT_TIME_VARIANT-Warning trägt. KEINE Mess-Logik — reines Durchreichen
|
|
636
|
+
// (Renderer → grid.js → hier). Symmetrisch zu isMotionDependent.
|
|
637
|
+
function isPaintTimeVariant(el) {
|
|
638
|
+
if (!el) return false;
|
|
639
|
+
if (el.paint_time_variant === true) return true;
|
|
640
|
+
return (
|
|
641
|
+
Array.isArray(el.warnings) && el.warnings.includes('PAINT_TIME_VARIANT')
|
|
642
|
+
);
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
// §F-AT-7-02 STILLE STROKE-FARB-LÜGE (Heilung, SK5): die sichtbare Farbe stammt aus
|
|
646
|
+
// dem stroke (fill trägt keine sichtbare Farbe bei). Reine Durchreichung des
|
|
647
|
+
// gemessenen Signals (Renderer → grid.js → hier), KEINE Mess-Logik. Beide Quellen
|
|
648
|
+
// (internes Feld + Warning) geprüft (fail-loud). Symmetrisch zu isStateDependent.
|
|
649
|
+
function isColorFromStroke(el) {
|
|
650
|
+
if (!el) return false;
|
|
651
|
+
if (el.visible_color_source === 'stroke') return true;
|
|
652
|
+
return (
|
|
653
|
+
Array.isArray(el.warnings) && el.warnings.includes('COLOR_FROM_STROKE')
|
|
654
|
+
);
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
// §F-AT-7-02 (SK5): fill UND stroke malen beide sichtbar — „eine Farbe" ist dann ein
|
|
658
|
+
// Urteil, keine Messung. Reine Durchreichung (Renderer → grid.js → hier). Beide
|
|
659
|
+
// Quellen geprüft (fail-loud). Disjunkt zu isColorFromStroke. Symmetrisch zu oben.
|
|
660
|
+
function isMultiplePaintSources(el) {
|
|
661
|
+
if (!el) return false;
|
|
662
|
+
if (el.visible_color_source === 'multiple') return true;
|
|
663
|
+
return (
|
|
664
|
+
Array.isArray(el.warnings) && el.warnings.includes('MULTIPLE_PAINT_SOURCES')
|
|
665
|
+
);
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
// visual_bbox ist entweder ein {x,y,w,h}-Objekt ODER das Literal 'not_measurable'
|
|
669
|
+
// (Überlauf existiert, sichere Schranke aber nicht ableitbar). Beides ehrlich in
|
|
670
|
+
// die Prosa schreiben — die Zahl NIE verschweigen, das Sentinel NIE als Zahl tarnen.
|
|
671
|
+
function formatVisualBbox(vb) {
|
|
672
|
+
if (vb && typeof vb === 'object') {
|
|
673
|
+
const r = (n) => Math.round(n * 10) / 10;
|
|
674
|
+
return `{x:${r(vb.x)},y:${r(vb.y)},w:${r(vb.w)},h:${r(vb.h)}}`;
|
|
675
|
+
}
|
|
676
|
+
return vb === 'not_measurable' ? 'not_measurable' : '?';
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
// \u00A7HEAL-7/B: measuredCarrier (Objekt-Identit\u00E4t, erstes Element seiner id) \u2014
|
|
680
|
+
// das \u2717-Verdikt heftet NUR an das gemessene Objekt, nie an Namensvettern.
|
|
681
|
+
// Das \u26A0 (unchecked) bleibt id-basiert (verweigerte Messung gilt dem Namen).
|
|
682
|
+
// \u00A7H10 R11-28: id-only wie structured.js \u2014 keine detail-String-Attribution
|
|
683
|
+
// (mentionsElementId entfernt: jedes failing-issue tr\u00E4gt id unkonditional,
|
|
684
|
+
// der Disjunkt produzierte ausschlie\u00DFlich \u00DCber-Attribution an Erw\u00E4hnte).
|
|
685
|
+
function getStatus(el, failing, unchecked, measuredCarrier) {
|
|
686
|
+
if (measuredCarrier && failing.some((i) => i.id === el.id)) return '\u2717';
|
|
687
|
+
if (unchecked.some((u) => u.id === el.id)) return '\u26A0';
|
|
688
|
+
return '\u2713';
|
|
689
|
+
}
|