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,649 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* structured.js — Structured Content Emitter (JSON)
|
|
3
|
+
* Vector Mirror v2.5 (P1-04 migrated)
|
|
4
|
+
*
|
|
5
|
+
* Adapter module: formats gridMap + arbitrated into MCP `structuredContent`.
|
|
6
|
+
*
|
|
7
|
+
* P1-04 changes:
|
|
8
|
+
* - Consumes the tri-state shape: `arbitrated.{passing, failing, unchecked, diff, totals}`.
|
|
9
|
+
* - status ∈ {'PASS', 'FAIL', 'PARTIAL'} — 'PARTIAL' when unchecked>0 ∧ failing===0.
|
|
10
|
+
* - `iteration.total_issues` = failing + unchecked.
|
|
11
|
+
* - Surfaces `unchecked` entries with reasonCode/hint/suggestedCorrection.
|
|
12
|
+
*
|
|
13
|
+
* Tag-Mapping (BAUPLAN 10.6):
|
|
14
|
+
* dx -> circle:cx, rect:x, text:x
|
|
15
|
+
* dy -> circle:cy, rect:y, text:y
|
|
16
|
+
* dw -> circle:r(/2), rect:width
|
|
17
|
+
* dh -> circle:r(/2), rect:height
|
|
18
|
+
* §1.5 Transform-Fallback (Sprint §1.5): Für Tags OHNE native dx/dy/dw/dh-
|
|
19
|
+
* Mapping (path/polygon/polyline/a/switch/textpath/use/foreignobject) emittiert
|
|
20
|
+
* der Emitter NICHT mehr den toxischen Literal-Durchstich (alter Nullish-
|
|
21
|
+
* Coalescing-Fallback auf den Roh-Delta-Key → ungültiges <path dx=...>), sondern:
|
|
22
|
+
* - Position (dx|dy): EINEN aggregierten transform-Fix (front-prepended
|
|
23
|
+
* translate via prependTranslate).
|
|
24
|
+
* - Größe (dw|dh): KEINEN Fix, sondern reason='SIZE_FIX_UNSUPPORTED_FOR_TAG'.
|
|
25
|
+
* tspan ist Sonderfall: nutzt sein natives dx/dy als RELATIVEN Shift (SVG 1.1
|
|
26
|
+
* §10.5), NICHT transform und NICHT absolute Koordinaten.
|
|
27
|
+
*
|
|
28
|
+
* DEPENDS: core/element_vocabulary.js (isTagDeltaAttributable), lib/transforms.js
|
|
29
|
+
* (prependTranslate) — beide Hexagonal-rein.
|
|
30
|
+
*/
|
|
31
|
+
import { isTagDeltaAttributable } from '../../core/element_vocabulary.js';
|
|
32
|
+
import { countTruncation } from '../../core/honesty.js';
|
|
33
|
+
import { prependTranslate } from '../../lib/transforms.js';
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Maps a delta key (dx/dy/dw/dh) to the SVG attribute for a given tag.
|
|
37
|
+
* BAUPLAN 10.6: Tag-Mapping table.
|
|
38
|
+
*
|
|
39
|
+
* §1.5: Nur noch für DELTA_ATTRIBUTABLE_TAGS aufgerufen (whitelist-Pfad). Der
|
|
40
|
+
* toxische Roh-Delta-Key-Fallback (Nullish-Coalescing) ist ELIMINIERT — Non-Whitelist-Tags laufen NIE
|
|
41
|
+
* mehr durch diese Funktion (buildFix verzweigt vorher auf isTagDeltaAttributable).
|
|
42
|
+
* Gibt undefined zurück, falls ein Tag/deltaKey ohne Mapping doch hier landet
|
|
43
|
+
* (defensiv: buildFix behandelt undefined als "kein Fix", kein Literal-Durchstich).
|
|
44
|
+
*/
|
|
45
|
+
function deltaToAttribute(tag, deltaKey) {
|
|
46
|
+
const map = {
|
|
47
|
+
circle: { dx: 'cx', dy: 'cy', dw: 'r', dh: 'r' },
|
|
48
|
+
rect: { dx: 'x', dy: 'y', dw: 'width', dh: 'height' },
|
|
49
|
+
text: { dx: 'x', dy: 'y' },
|
|
50
|
+
ellipse: { dx: 'cx', dy: 'cy', dw: 'rx', dh: 'ry' },
|
|
51
|
+
line: { dx: 'x1', dy: 'y1' },
|
|
52
|
+
image: { dx: 'x', dy: 'y', dw: 'width', dh: 'height' },
|
|
53
|
+
};
|
|
54
|
+
return map[tag]?.[deltaKey];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Builds a fix object from a correction delta + element.
|
|
59
|
+
* OBL-011: Attribut-Extraktion + fix-Objekt.
|
|
60
|
+
*/
|
|
61
|
+
function buildFix(el, deltaKey, deltaValue) {
|
|
62
|
+
if (deltaValue === undefined || deltaValue === 0) return null;
|
|
63
|
+
const attr = deltaToAttribute(el.tag, deltaKey);
|
|
64
|
+
// §1.5: Defensive — buildFix wird nur noch für whitelist-Tags aufgerufen
|
|
65
|
+
// (isTagDeltaAttributable-Gate in buildElementFixes). Falls doch ein Tag ohne
|
|
66
|
+
// Mapping hier landet, KEIN Literal-Durchstich mehr (alter Roh-Key-Bug).
|
|
67
|
+
if (attr === undefined) return null;
|
|
68
|
+
const bbox = el.bbox || { x: 0, y: 0, w: 0, h: 0 };
|
|
69
|
+
|
|
70
|
+
let current = 0;
|
|
71
|
+
let targetDelta = deltaValue;
|
|
72
|
+
|
|
73
|
+
if (deltaKey === 'dx') {
|
|
74
|
+
if (attr === 'cx') current = el.cx ?? bbox.x + bbox.w / 2;
|
|
75
|
+
else current = bbox.x;
|
|
76
|
+
} else if (deltaKey === 'dy') {
|
|
77
|
+
if (attr === 'cy') current = el.cy ?? bbox.y + bbox.h / 2;
|
|
78
|
+
else current = bbox.y;
|
|
79
|
+
} else if (deltaKey === 'dw') {
|
|
80
|
+
if (attr === 'r' || attr === 'rx') {
|
|
81
|
+
current = bbox.w / 2;
|
|
82
|
+
targetDelta = deltaValue / 2;
|
|
83
|
+
} else {
|
|
84
|
+
current = bbox.w;
|
|
85
|
+
}
|
|
86
|
+
} else if (deltaKey === 'dh') {
|
|
87
|
+
if (attr === 'r' || attr === 'ry') {
|
|
88
|
+
current = bbox.h / 2;
|
|
89
|
+
targetDelta = deltaValue / 2;
|
|
90
|
+
} else {
|
|
91
|
+
current = bbox.h;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
current = Math.round(current);
|
|
96
|
+
const target = current + Math.round(targetDelta);
|
|
97
|
+
return { attribute: attr, current: String(current), target: String(target) };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* §1.5 Transform-Fallback (R-A/R-B): Aggregiert dx UND dy zu EINEM transform-Fix
|
|
102
|
+
* für Tags ohne native Positions-Attribute (path/polygon/polyline/a/switch/use/...).
|
|
103
|
+
*
|
|
104
|
+
* Das translate wird FRONT-PREPENDED (prependTranslate): dx/dy sind Welt-px-Deltas
|
|
105
|
+
* vom Spotter (CTM-projizierte BBox), die außen wrappen müssen, damit sie unabhängig
|
|
106
|
+
* von einem Autor-scale()/rotate() in Welt-Koordinaten wirken (SOTA Präzision 1).
|
|
107
|
+
*
|
|
108
|
+
* current = bestehender transform-Wert (oder '' wenn keiner), target = der neue
|
|
109
|
+
* transform-String. EIN Fix-Objekt für beide Achsen (R-B: NICHT pro-Achse).
|
|
110
|
+
*
|
|
111
|
+
* @returns {{attribute:'transform', current:string, target:string, warning:string}|null}
|
|
112
|
+
* null, wenn weder dx noch dy einen Effekt haben.
|
|
113
|
+
*/
|
|
114
|
+
function buildTransformFix(el, dx, dy) {
|
|
115
|
+
const dxNum = dx ?? 0;
|
|
116
|
+
const dyNum = dy ?? 0;
|
|
117
|
+
if (dxNum === 0 && dyNum === 0) return null;
|
|
118
|
+
const existing = el.transform ?? '';
|
|
119
|
+
return {
|
|
120
|
+
attribute: 'transform',
|
|
121
|
+
current: existing,
|
|
122
|
+
target: prependTranslate(existing, Math.round(dxNum), Math.round(dyNum)),
|
|
123
|
+
// praezision_2 (low-effort): statischer Hinweis — eine CSS transform-Property
|
|
124
|
+
// (inline/extern) würde dieses Attribut überstimmen. KEINE Kaskaden-Analyse.
|
|
125
|
+
warning: 'CSS transform property may override this attribute',
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Parst den ersten numerischen Token eines SVG dx/dy-Attribut-Werts (das eine
|
|
131
|
+
* Längen-LISTE sein kann, z.B. "4 2 1"). Liefert die Skalar-Basis für den
|
|
132
|
+
* relativen tspan-Shift; 0, wenn kein/ungültiger Wert.
|
|
133
|
+
*/
|
|
134
|
+
function parseNativeShift(raw) {
|
|
135
|
+
if (raw == null) return 0;
|
|
136
|
+
const m = String(raw)
|
|
137
|
+
.trim()
|
|
138
|
+
.match(/-?\d+(?:\.\d+)?/);
|
|
139
|
+
if (!m) return 0;
|
|
140
|
+
const n = Number(m[0]);
|
|
141
|
+
return Number.isFinite(n) ? n : 0;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* §1.5 tspan-Sonderfall (R-C / praezision_4 + Patch P2): tspan trägt native dx/dy,
|
|
146
|
+
* aber als RELATIVEN Shift (SVG 1.1 §10.5) — KEIN transform und KEINE absolute
|
|
147
|
+
* Koordinate. Der Fix modelliert den Shift RELATIV zum bestehenden dx-Attribut
|
|
148
|
+
* (Patch P2): current = Autor-dx (Skalar-Erstwert, 0 wenn keiner), target =
|
|
149
|
+
* current + delta. So ÜBERSCHREIBT der Fix das Autor-dx nicht, sondern verschiebt
|
|
150
|
+
* relativ dazu. Hat tspan kein dx → current='0' (Default, korrekt).
|
|
151
|
+
*
|
|
152
|
+
* Eigene Achsen-fixes (dx → attribute 'dx', dy → attribute 'dy'), weil das Schema
|
|
153
|
+
* single-attribute ist; pro-Achse ist hier korrekt (dx/dy sind native tspan-Attribute).
|
|
154
|
+
*
|
|
155
|
+
* @param {string} deltaKey - 'dx' | 'dy'.
|
|
156
|
+
* @param {number} deltaValue - Spotter-Delta.
|
|
157
|
+
* @param {string|number|undefined} nativeBaseRaw - bestehender dx-Attribut-Wert (nur dx-Achse).
|
|
158
|
+
* @returns {{attribute:'dx'|'dy', current:string, target:string}|null}
|
|
159
|
+
*/
|
|
160
|
+
function buildTspanShiftFix(deltaKey, deltaValue, nativeBaseRaw) {
|
|
161
|
+
if (deltaValue === undefined || deltaValue === 0) return null;
|
|
162
|
+
const attr = deltaKey === 'dx' ? 'dx' : deltaKey === 'dy' ? 'dy' : null;
|
|
163
|
+
if (attr === null) return null;
|
|
164
|
+
// Patch P2: native_dx surfaced nur für die dx-Achse (Renderer liest dx-Attribut).
|
|
165
|
+
// dy-Achse bleibt Relativbasis 0 (kein dy-Attribut surfaced; konservativ korrekt).
|
|
166
|
+
const base = attr === 'dx' ? parseNativeShift(nativeBaseRaw) : 0;
|
|
167
|
+
return {
|
|
168
|
+
attribute: attr,
|
|
169
|
+
current: String(Math.round(base)),
|
|
170
|
+
target: String(Math.round(base + deltaValue)),
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* §1.5 Orchestrierung (R-A..R-E): berechnet für ein failing-Element alle fixes
|
|
176
|
+
* UND einen optionalen unsupported-reason. 4-Weg-Verzweigung:
|
|
177
|
+
* 1. whitelist (isTagDeltaAttributable): bestehender Pfad, pro-Achse buildFix.
|
|
178
|
+
* 2. tspan: native dx/dy als relativer Shift (buildTspanShiftFix), pro-Achse.
|
|
179
|
+
* 3. non-whitelist + dx|dy: EIN aggregierter transform-Fix (buildTransformFix).
|
|
180
|
+
* 4. non-whitelist + dw|dh: KEIN Fix, reason='SIZE_FIX_UNSUPPORTED_FOR_TAG'.
|
|
181
|
+
*
|
|
182
|
+
* @returns {{fixes:Array, reason:(string|undefined)}}
|
|
183
|
+
* fixes: 0..n Fix-Objekte (Reihenfolge dx,dy,dw,dh). reason: gesetzt, wenn ein
|
|
184
|
+
* dw/dh-Delta auf einem Tag ohne Size-Mapping vorlag (Größen-Fix nicht möglich).
|
|
185
|
+
*/
|
|
186
|
+
function buildElementFixes(el, issue) {
|
|
187
|
+
const tag = String(el.tag).toLowerCase();
|
|
188
|
+
const fixes = [];
|
|
189
|
+
let reason;
|
|
190
|
+
|
|
191
|
+
if (isTagDeltaAttributable(tag)) {
|
|
192
|
+
// Pfad 1: native Attribut-Mapping (circle/rect/text/ellipse/line/image).
|
|
193
|
+
for (const key of ['dx', 'dy', 'dw', 'dh']) {
|
|
194
|
+
if (issue[key] !== undefined) {
|
|
195
|
+
const f = buildFix(el, key, issue[key]);
|
|
196
|
+
if (f) fixes.push(f);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return { fixes, reason };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (tag === 'tspan') {
|
|
203
|
+
// Pfad 2: tspan native relativer Shift (R-C). Größen-Deltas sind für tspan
|
|
204
|
+
// nicht modellierbar → wie andere Non-Whitelist-Tags als unsupported melden.
|
|
205
|
+
for (const key of ['dx', 'dy']) {
|
|
206
|
+
if (issue[key] !== undefined) {
|
|
207
|
+
const f = buildTspanShiftFix(key, issue[key], el.native_dx);
|
|
208
|
+
if (f) fixes.push(f);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
if (issue.dw !== undefined || issue.dh !== undefined) {
|
|
212
|
+
reason = 'SIZE_FIX_UNSUPPORTED_FOR_TAG';
|
|
213
|
+
}
|
|
214
|
+
return { fixes, reason };
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Pfad 3+4: non-whitelist (path/polygon/polyline/a/switch/textpath/use/foreignobject).
|
|
218
|
+
const transformFix = buildTransformFix(el, issue.dx, issue.dy);
|
|
219
|
+
if (transformFix) fixes.push(transformFix);
|
|
220
|
+
if (issue.dw !== undefined || issue.dh !== undefined) {
|
|
221
|
+
// Pfad 4: Größen-Fix ohne Mapping → unsupported statt erfundenem scale().
|
|
222
|
+
reason = 'SIZE_FIX_UNSUPPORTED_FOR_TAG';
|
|
223
|
+
}
|
|
224
|
+
return { fixes, reason };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Computes convergence from previous and current issue counts.
|
|
229
|
+
* BAUPLAN 10.5: Stateless — client provides previousIssueCount.
|
|
230
|
+
*
|
|
231
|
+
* §1.6 KONVERGENZ-EHRLICHKEIT (Epistemischer Vertrag): currentIssueCount MUSS
|
|
232
|
+
* totalIssues = failing + unchecked sein — niemals failingCount allein. Sonst
|
|
233
|
+
* meldet der Übergang "1 Failing → 1 Unchecked" IMPROVING (failing sinkt 1→0),
|
|
234
|
+
* obwohl die Wahrheit "Pipeline blind" lautet (Constraint nicht mehr auswertbar).
|
|
235
|
+
* Das wäre eine Fortschritts-Lüge an den LLM. Vergleichswert (previousIssueCount)
|
|
236
|
+
* und aktueller Wert müssen dieselbe Metrik (totalIssues) sein, damit der
|
|
237
|
+
* Closed-Loop wahrheitsgemäß bleibt. Die Klassifikations-Logik selbst ist
|
|
238
|
+
* metrik-agnostisch und bleibt unangetastet (DNA bewahren).
|
|
239
|
+
*/
|
|
240
|
+
function computeConvergence(previousIssueCount, currentIssueCount) {
|
|
241
|
+
if (currentIssueCount === 0) return 'SOLVED';
|
|
242
|
+
if (previousIssueCount === undefined || previousIssueCount === null) {
|
|
243
|
+
// §H9 K-12: ohne Historie KEINE Trend-Behauptung — STAGNATING/DIVERGING
|
|
244
|
+
// sind Verlaufs-Urteile über zwei Messpunkte; bei der ersten Messung gibt
|
|
245
|
+
// es nur einen. BASELINE ist das ehrliche Vokabular für "erste Messung,
|
|
246
|
+
// N Issues, kein Trend behauptbar" (Schema-Enum additiv nachgezogen,
|
|
247
|
+
// schema.js). SOLVED bei 0 Issues bleibt: Zustands-, keine Trend-Aussage.
|
|
248
|
+
return 'BASELINE';
|
|
249
|
+
}
|
|
250
|
+
if (currentIssueCount < previousIssueCount) return 'IMPROVING';
|
|
251
|
+
if (currentIssueCount === previousIssueCount) return 'STAGNATING';
|
|
252
|
+
return 'DIVERGING';
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Derives the tri-state document status.
|
|
257
|
+
* PASS — kein Fehler, alles geprueft
|
|
258
|
+
* FAIL — mind. 1 failing
|
|
259
|
+
* PARTIAL — kein failing, aber unchecked > 0 (LLM darf nicht "alles ok" annehmen)
|
|
260
|
+
*/
|
|
261
|
+
function deriveStatus(failingCount, uncheckedCount) {
|
|
262
|
+
if (failingCount > 0) return 'FAIL';
|
|
263
|
+
if (uncheckedCount > 0) return 'PARTIAL';
|
|
264
|
+
return 'PASS';
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Formats gridMap + arbitrated into structured JSON (MCP structuredContent).
|
|
269
|
+
* BAUPLAN 10.4: Full schema with status, iteration, scene, corrections, diff, unchecked.
|
|
270
|
+
*/
|
|
271
|
+
export function formatStructured(gridMap, arbitrated, opts = {}) {
|
|
272
|
+
const { canvas, elements } = gridMap;
|
|
273
|
+
const failing = arbitrated.failing || [];
|
|
274
|
+
const unchecked = arbitrated.unchecked || [];
|
|
275
|
+
const diffEntries = arbitrated.diff || [];
|
|
276
|
+
|
|
277
|
+
// §E1 fail-closed-Vertrag (REGEL-8): jedes failing-issue MUSS am Emissions-
|
|
278
|
+
// Rand durch honesty.js#gateCorrections gelaufen sein (Caller-Pflicht,
|
|
279
|
+
// pipeline.js analyze/compare). Fehlt das _gated-Vertragsfeld, ist die
|
|
280
|
+
// Reliability-Entscheidung NICHT getroffen → Wurf, statt ungegated zu
|
|
281
|
+
// emittieren. So ist eine ungegatete Delta-Emission mechanisch unmoeglich.
|
|
282
|
+
for (const issue of failing) {
|
|
283
|
+
if (issue._gated === undefined) {
|
|
284
|
+
throw new Error(
|
|
285
|
+
'formatStructured: ungegatetes failing-issue (kein _gated) — ' +
|
|
286
|
+
'gateCorrections am Emissions-Rand fehlt (REGEL-8 fail-closed).',
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const failingCount = failing.length;
|
|
292
|
+
const uncheckedCount = unchecked.length;
|
|
293
|
+
const totalIssues = failingCount + uncheckedCount;
|
|
294
|
+
|
|
295
|
+
const prevCount = opts.previousIssueCount;
|
|
296
|
+
const sequence = prevCount !== undefined && prevCount !== null ? 2 : 1;
|
|
297
|
+
// §1.6: Konvergenz auf totalIssues (failing + unchecked), NICHT failingCount.
|
|
298
|
+
// Failing→Unchecked ist nie IMPROVING — der Constraint wurde nur unsichtbar,
|
|
299
|
+
// nicht gelöst. previousIssueCount ist (im Closed-Loop) ebenfalls ein
|
|
300
|
+
// totalIssues-Wert → Vergleich ist metrik-konsistent und lügt nicht.
|
|
301
|
+
const convergence = computeConvergence(prevCount, totalIssues);
|
|
302
|
+
// §1.3 Schicht 2: analysisId vom Caller (analyze→neu, compare→referenced).
|
|
303
|
+
// Pflicht-Feld im Output-Schema. Kein Fallback hier — Caller muss liefern.
|
|
304
|
+
const { analysisId } = opts;
|
|
305
|
+
|
|
306
|
+
// §1.4
|
|
307
|
+
// VOR dem slice(0,7)-Cap: alle warning-tragenden Elemente auf Position >=7
|
|
308
|
+
// in truncated_warnings hoisten. So bleibt das Reliability-Signal sichtbar,
|
|
309
|
+
// auch wenn das Element selbst aus scene.elements wegen Token-Limit
|
|
310
|
+
// (BAUPLAN OBL-008) verschwindet. REGEL-3 ueberstimmt BAUPLAN-Konvention.
|
|
311
|
+
// Position ist 0-basierte Original-Position vor dem Slicing.
|
|
312
|
+
const SCENE_MAX_ELEMENTS = 7;
|
|
313
|
+
const truncatedWarnings = [];
|
|
314
|
+
for (let i = SCENE_MAX_ELEMENTS; i < elements.length; i++) {
|
|
315
|
+
const el = elements[i];
|
|
316
|
+
if (el && Array.isArray(el.warnings) && el.warnings.length > 0) {
|
|
317
|
+
truncatedWarnings.push({
|
|
318
|
+
element_id: el.id,
|
|
319
|
+
warnings: el.warnings,
|
|
320
|
+
position: i,
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const sceneElements = elements.slice(0, SCENE_MAX_ELEMENTS).map((el) => {
|
|
326
|
+
const failingIssue = failing.find((i) => i.id === el.id);
|
|
327
|
+
const uncheckedIssue = unchecked.find((i) => i.id === el.id);
|
|
328
|
+
const diffIssue = diffEntries.find((i) => i.id === el.id);
|
|
329
|
+
let status = 'ok';
|
|
330
|
+
if (failingIssue) status = 'fail';
|
|
331
|
+
else if (uncheckedIssue || diffIssue) status = 'warn';
|
|
332
|
+
return {
|
|
333
|
+
id: el.id,
|
|
334
|
+
tag: el.tag,
|
|
335
|
+
cell: el.span || el.cell,
|
|
336
|
+
color: el.color,
|
|
337
|
+
status,
|
|
338
|
+
// §1.2b L-002b: Reliability-Propagation auf MCP-Caller-Ebene
|
|
339
|
+
// (Datenkette Renderer -> grid.mappedElements -> hier -> MCP structuredContent).
|
|
340
|
+
// grid.js reicht bereits durch — wir reichen weiter, damit der Caller das
|
|
341
|
+
// 3D-Signal sieht (REGEL-3 Spotter-Anti-Luege).
|
|
342
|
+
bbox_reliability: el.bbox_reliability,
|
|
343
|
+
// §1.5 Block D: Parent-Kontext für tspan/textPath durchreichen (R-C).
|
|
344
|
+
// Nur emittieren, wenn vorhanden (Common-Case: Top-Level-Elemente ohne
|
|
345
|
+
// relevanten Parent tragen das Feld nicht → Output-Volumen + Schema-
|
|
346
|
+
// Optional-Vertrag konsistent). Datenkette: playwright.js → grid.js →
|
|
347
|
+
// hier. ACHTUNG: grid.js muss parent_id/parent_tag ebenfalls durchreichen
|
|
348
|
+
// (analog bbox_reliability), sonst ist el.parent_id hier undefined.
|
|
349
|
+
...(el.parent_id != null ? { parent_id: el.parent_id } : {}),
|
|
350
|
+
...(el.parent_tag != null ? { parent_tag: el.parent_tag } : {}),
|
|
351
|
+
...(el.warnings ? { warnings: el.warnings } : {}),
|
|
352
|
+
// §E4 Paint-Extent-Ehrlichkeit (F-AT-004, DoD-3): visual_bbox/has_paint_overflow
|
|
353
|
+
// bis zur MCP-Caller-Boundary durchreichen (analog bbox_reliability/warnings).
|
|
354
|
+
// Datenkette: playwright.js → grid.js → hier. Nur emittiert, wenn vorhanden
|
|
355
|
+
// (filterlose Elemente tragen die Felder NICHT — Negativ-Kontrolle).
|
|
356
|
+
...(el.has_paint_overflow != null
|
|
357
|
+
? { has_paint_overflow: el.has_paint_overflow }
|
|
358
|
+
: {}),
|
|
359
|
+
...(el.visual_bbox != null ? { visual_bbox: el.visual_bbox } : {}),
|
|
360
|
+
// §HEAL-R6 / T1 Paint-Presence (F-AT-6-01, DoD-2): bis zur MCP-Caller-Boundary
|
|
361
|
+
// durchreichen (analog bbox_reliability/visual_bbox). Datenkette:
|
|
362
|
+
// playwright.js → grid.js → hier. paint_visible nur bei painted===false.
|
|
363
|
+
...(el.fill_paint_factor != null
|
|
364
|
+
? { fill_paint_factor: el.fill_paint_factor }
|
|
365
|
+
: {}),
|
|
366
|
+
...(el.stroke_paint_factor != null
|
|
367
|
+
? { stroke_paint_factor: el.stroke_paint_factor }
|
|
368
|
+
: {}),
|
|
369
|
+
...(el.paint_visible != null ? { paint_visible: el.paint_visible } : {}),
|
|
370
|
+
// §D5 / R6-STATE (state_dependent): bis zur MCP-Caller-Boundary durchreichen
|
|
371
|
+
// (analog paint_visible). Datenkette: playwright.js → grid.js → hier. Ohne
|
|
372
|
+
// diese Zeile lügt der analyze-Pfad still weiter.
|
|
373
|
+
...(el.state_dependent != null
|
|
374
|
+
? { state_dependent: el.state_dependent }
|
|
375
|
+
: {}),
|
|
376
|
+
// §F-AT-6-09 / R6-MEDIA (media_dependent): bis zur MCP-Caller-Boundary
|
|
377
|
+
// durchreichen (analog state_dependent). Datenkette: playwright.js → grid.js →
|
|
378
|
+
// hier. Ohne diese Zeile lügt der analyze-Pfad still weiter.
|
|
379
|
+
...(el.media_dependent != null
|
|
380
|
+
? { media_dependent: el.media_dependent }
|
|
381
|
+
: {}),
|
|
382
|
+
// §HEAL-5 / Zeit-Achse (motion_dependent): bis zur MCP-Caller-Boundary
|
|
383
|
+
// durchreichen (exakt nach media_dependent-Vorlage). Datenkette:
|
|
384
|
+
// playwright.js → grid.js → hier. Ohne diese Zeile lügt der analyze-Pfad
|
|
385
|
+
// still weiter.
|
|
386
|
+
...(el.motion_dependent != null
|
|
387
|
+
? { motion_dependent: el.motion_dependent }
|
|
388
|
+
: {}),
|
|
389
|
+
// §H10 R11-06 / Paint-Zeit-Achse (paint_time_variant): bis zur MCP-
|
|
390
|
+
// Caller-Boundary durchreichen (exakt nach motion_dependent-Vorlage).
|
|
391
|
+
...(el.paint_time_variant != null
|
|
392
|
+
? { paint_time_variant: el.paint_time_variant }
|
|
393
|
+
: {}),
|
|
394
|
+
};
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
// §E1 WELLE-β-002/D-006/D-015: die Reliability-Entscheidung ist EINMAL am
|
|
398
|
+
// Emissions-Rand getroffen (honesty.js#gateCorrections, Caller pipeline.js)
|
|
399
|
+
// und sitzt als _gated-Vertragsfeld auf jedem issue. Das gegatete issue ist
|
|
400
|
+
// bei _gated===false bereits um dx/dy/dw/dh BEREINIGT — hier nur noch lesen,
|
|
401
|
+
// keine zweite Reliability-Map (D-015 echt 3→1). detail-Prosa + constraint-
|
|
402
|
+
// Verweis bleiben (Caller weiss, dass ein Bruch vorliegt, ohne irrefuehrende
|
|
403
|
+
// dx/dy zu bekommen).
|
|
404
|
+
const corrections = failing.map((issue) => {
|
|
405
|
+
const el = elements.find((e) => e.id === issue.id);
|
|
406
|
+
const correction = {
|
|
407
|
+
element: `#${issue.id || 'unknown'}`,
|
|
408
|
+
tag: el?.tag,
|
|
409
|
+
constraint: issue.constraintType || 'UNKNOWN',
|
|
410
|
+
reference: issue.reference ? `#${issue.reference}` : null,
|
|
411
|
+
};
|
|
412
|
+
// β-002 Gate (SSOT): _gated ist allowDeltas(reliability) — bei
|
|
413
|
+
// not_measurable/approximate/unbekannter id false → keine dx/dy/dw/dh/fix.
|
|
414
|
+
const allowDeltas = issue._gated;
|
|
415
|
+
if (allowDeltas) {
|
|
416
|
+
if (issue.dx !== undefined) correction.dx = issue.dx;
|
|
417
|
+
if (issue.dy !== undefined) correction.dy = issue.dy;
|
|
418
|
+
if (issue.dw !== undefined) correction.dw = issue.dw;
|
|
419
|
+
if (issue.dh !== undefined) correction.dh = issue.dh;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
if (el && allowDeltas) {
|
|
423
|
+
// §1.5: 4-Weg-Verzweigung (whitelist | tspan | transform | size-unsupported).
|
|
424
|
+
// buildElementFixes kapselt die Logik; hier nur noch Shape-Mapping auf das
|
|
425
|
+
// bestehende fix/fixes-Contract + optionaler unsupported-reason.
|
|
426
|
+
const { fixes, reason } = buildElementFixes(el, issue);
|
|
427
|
+
if (fixes.length === 1) {
|
|
428
|
+
correction.fix = fixes[0];
|
|
429
|
+
} else if (fixes.length > 1) {
|
|
430
|
+
correction.fix = fixes[0];
|
|
431
|
+
correction.fixes = fixes;
|
|
432
|
+
}
|
|
433
|
+
if (reason !== undefined) {
|
|
434
|
+
correction.reason = reason;
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
return correction;
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
const diffSummary = diffEntries.map((d) => {
|
|
441
|
+
const entry = { type: d.type, id: d.id || 'unknown' };
|
|
442
|
+
if (d.from) entry.from = d.from;
|
|
443
|
+
if (d.to) entry.to = d.to;
|
|
444
|
+
return entry;
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
const uncheckedSummary = unchecked.map((u) => {
|
|
448
|
+
const out = {
|
|
449
|
+
element: u.id !== undefined ? `#${u.id}` : null,
|
|
450
|
+
constraint: u.constraintType || 'UNKNOWN',
|
|
451
|
+
reasonCategory: u.reasonCategory,
|
|
452
|
+
reasonCode: u.reasonCode,
|
|
453
|
+
hint: u.hint,
|
|
454
|
+
};
|
|
455
|
+
if (u.suggestedCorrection) out.suggestedCorrection = u.suggestedCorrection;
|
|
456
|
+
return out;
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
// §1.4
|
|
460
|
+
// Wenn truncated_warnings nicht-leer ist, KANN status nicht PASS sein —
|
|
461
|
+
// der Caller wuerde sonst "alles ok" annehmen, obwohl warning-tragende
|
|
462
|
+
// Elemente unsichtbar wegen slice(0,7)-Cap sind. PARTIAL signalisiert:
|
|
463
|
+
// "kein Fehler im sichtbaren Bereich, aber nicht alles ueberprueft/gezeigt".
|
|
464
|
+
// FAIL bleibt erhalten (failing > 0 trumpt truncated_warnings).
|
|
465
|
+
let derivedStatus = deriveStatus(failingCount, uncheckedCount);
|
|
466
|
+
if (derivedStatus === 'PASS' && truncatedWarnings.length > 0) {
|
|
467
|
+
derivedStatus = 'PARTIAL';
|
|
468
|
+
}
|
|
469
|
+
const out = {
|
|
470
|
+
status: derivedStatus,
|
|
471
|
+
iteration: {
|
|
472
|
+
sequence,
|
|
473
|
+
previous_issues: prevCount ?? 0,
|
|
474
|
+
// §1.6: current_issues = totalIssues (failing + unchecked), damit die
|
|
475
|
+
// Iterations-Metrik mit dem Konvergenz-Verdikt konsistent ist und der
|
|
476
|
+
// Closed-Loop (Caller speist current_issues als previousIssueCount zurück)
|
|
477
|
+
// dieselbe Metrik vergleicht. total_issues/returned_issues bleiben gleich.
|
|
478
|
+
current_issues: totalIssues,
|
|
479
|
+
total_issues: totalIssues,
|
|
480
|
+
returned_issues: totalIssues, // structured ships everything; no cap here
|
|
481
|
+
// §E1 D-008: ehrlicher ELEMENT-Trunkierungs-Zaehler. Der issue-Cap
|
|
482
|
+
// existiert nicht (structured ships all issues), aber scene.elements wird
|
|
483
|
+
// bei >SCENE_MAX_ELEMENTS via slice(0,7) gekuerzt — suppressed zaehlt die
|
|
484
|
+
// dadurch verborgenen Elemente (war hartkodiert 0 = Luege bei >7).
|
|
485
|
+
// Symmetrisch zum truncated_warnings-Hoist. ≤7 → 0 (byte-stabil).
|
|
486
|
+
suppressed: countTruncation(elements, SCENE_MAX_ELEMENTS).suppressed,
|
|
487
|
+
convergence,
|
|
488
|
+
analysisId,
|
|
489
|
+
},
|
|
490
|
+
scene: {
|
|
491
|
+
width: canvas.width,
|
|
492
|
+
height: canvas.height,
|
|
493
|
+
grid: `${gridMap.grid.cellsX}x${gridMap.grid.cellsY}`,
|
|
494
|
+
elements: sceneElements,
|
|
495
|
+
// §HEAL-R6 / T1 "das Kabel" (F-AT-6-08): das classifyCanvas-Verdikt aus dem
|
|
496
|
+
// Caller (pipeline.js). Anti-LECK-3: der Caller speist es aus
|
|
497
|
+
// resolved.sanitize_loss DIREKT, NIE aus gridMap.canvas (das das Feld nicht
|
|
498
|
+
// trägt → wäre immer 'valid' → neue stille Lüge). Nur emittieren, wenn der
|
|
499
|
+
// Caller es liefert (optional-by-default → kein Schema-Bruch).
|
|
500
|
+
...(opts.canvasValidity !== undefined
|
|
501
|
+
? { canvas_validity: opts.canvasValidity }
|
|
502
|
+
: {}),
|
|
503
|
+
// §H10 R11-01: Existenz-Register — css-unsichtbar geskippte Elemente
|
|
504
|
+
// (id+Achse) sind NICHT Teil der Emissions-Menge, dürfen aber nicht
|
|
505
|
+
// verschwinden. Selbes additive optional-Muster wie canvas_validity.
|
|
506
|
+
...(Array.isArray(gridMap.hidden) && gridMap.hidden.length > 0
|
|
507
|
+
? { hidden_elements: gridMap.hidden }
|
|
508
|
+
: {}),
|
|
509
|
+
},
|
|
510
|
+
corrections,
|
|
511
|
+
unchecked: uncheckedSummary,
|
|
512
|
+
diff: diffSummary,
|
|
513
|
+
};
|
|
514
|
+
// §1.4
|
|
515
|
+
// einen Eintrag hat. Optional-by-default haelt das Schema-Optional-Vertrag
|
|
516
|
+
// konsistent und reduziert Output-Volumen im Common-Case (<=7 Elemente).
|
|
517
|
+
if (truncatedWarnings.length > 0) {
|
|
518
|
+
out.meta = { truncated_warnings: truncatedWarnings };
|
|
519
|
+
}
|
|
520
|
+
return out;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* Formats gridMap for inspect output (no constraints, no diff).
|
|
525
|
+
*
|
|
526
|
+
* §HEAL-R6 / T1 "das Kabel" (F-AT-6-08): opts.canvasValidity ist das
|
|
527
|
+
* classifyCanvas-Verdikt aus dem Caller (pipeline.js, aus resolved.sanitize_loss
|
|
528
|
+
* DIREKT — Anti-LECK-3). Optional — fehlt es, bleibt scene.canvas_validity weg.
|
|
529
|
+
*/
|
|
530
|
+
export function formatInspectStructured(gridMap, opts = {}) {
|
|
531
|
+
const { canvas, elements } = gridMap;
|
|
532
|
+
// §1.4
|
|
533
|
+
// formatStructured — inspectOutput konsumiert dasselbe elementSchema +
|
|
534
|
+
// metaSchema und braucht denselben REGEL-3-Hoist (β-003).
|
|
535
|
+
const SCENE_MAX_ELEMENTS = 7;
|
|
536
|
+
const truncatedWarnings = [];
|
|
537
|
+
for (let i = SCENE_MAX_ELEMENTS; i < elements.length; i++) {
|
|
538
|
+
const el = elements[i];
|
|
539
|
+
if (el && Array.isArray(el.warnings) && el.warnings.length > 0) {
|
|
540
|
+
truncatedWarnings.push({
|
|
541
|
+
element_id: el.id,
|
|
542
|
+
warnings: el.warnings,
|
|
543
|
+
position: i,
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
const out = {
|
|
548
|
+
scene: {
|
|
549
|
+
width: canvas.width,
|
|
550
|
+
height: canvas.height,
|
|
551
|
+
grid: `${gridMap.grid.cellsX}x${gridMap.grid.cellsY}`,
|
|
552
|
+
// §E1 D-008 (F-TF-008): ehrlicher ELEMENT-Trunkierungs-Zaehler, symmetrisch
|
|
553
|
+
// zum analyze-Vorbild (formatStructured, iteration.suppressed). inspect hat
|
|
554
|
+
// KEINEN iteration-Block, daher sitzt der Zaehler hier im scene-Block — er
|
|
555
|
+
// beschreibt scene.elements, die unten via slice(0,7) gekuerzt werden.
|
|
556
|
+
// UNKONDITIONAL emittiert (≤7 → 0 = byte-stabil); behebt die stille
|
|
557
|
+
// Mess-Luege (maschineller Konsument verlor >7 Elemente ohne Zaehler).
|
|
558
|
+
suppressed: countTruncation(elements, SCENE_MAX_ELEMENTS).suppressed,
|
|
559
|
+
elements: elements.slice(0, SCENE_MAX_ELEMENTS).map((el) => ({
|
|
560
|
+
id: el.id,
|
|
561
|
+
tag: el.tag,
|
|
562
|
+
cell: el.span || el.cell,
|
|
563
|
+
color: el.color,
|
|
564
|
+
status: 'ok',
|
|
565
|
+
// §1.2b L-002b: Reliability-Propagation auch im inspect-Output, weil
|
|
566
|
+
// inspectOutput dasselbe elementSchema (Pflicht-Promotion L-003) konsumiert.
|
|
567
|
+
// Ohne diese Zeile lehnt MCP-Inspector ab (E2E SC-7).
|
|
568
|
+
bbox_reliability: el.bbox_reliability,
|
|
569
|
+
...(el.warnings ? { warnings: el.warnings } : {}),
|
|
570
|
+
// §E4 Paint-Extent-Ehrlichkeit (F-AT-004, DoD-3): auch im inspect-Output
|
|
571
|
+
// durchreichen (dasselbe elementSchema). Datenkette: playwright.js → grid.js
|
|
572
|
+
// → hier. Nur wenn vorhanden (filterlose Elemente tragen die Felder NICHT).
|
|
573
|
+
...(el.has_paint_overflow != null
|
|
574
|
+
? { has_paint_overflow: el.has_paint_overflow }
|
|
575
|
+
: {}),
|
|
576
|
+
...(el.visual_bbox != null ? { visual_bbox: el.visual_bbox } : {}),
|
|
577
|
+
// §HEAL-R6 / T1 Paint-Presence (F-AT-6-01, DoD-2): auch im inspect-Output
|
|
578
|
+
// durchreichen (dasselbe elementSchema). Ohne diese Zeilen lügt der inspect-
|
|
579
|
+
// Pfad still weiter (paint_visible verschwände). Datenkette: playwright.js →
|
|
580
|
+
// grid.js → hier. Nur wenn vorhanden (paint_visible nur bei painted===false).
|
|
581
|
+
...(el.fill_paint_factor != null
|
|
582
|
+
? { fill_paint_factor: el.fill_paint_factor }
|
|
583
|
+
: {}),
|
|
584
|
+
...(el.stroke_paint_factor != null
|
|
585
|
+
? { stroke_paint_factor: el.stroke_paint_factor }
|
|
586
|
+
: {}),
|
|
587
|
+
...(el.paint_visible != null ? { paint_visible: el.paint_visible } : {}),
|
|
588
|
+
// §D5 / R6-STATE (state_dependent): auch im inspect-Output durchreichen
|
|
589
|
+
// (dasselbe elementSchema). Ohne diese Zeile lügt der inspect-Pfad still
|
|
590
|
+
// weiter. Datenkette: playwright.js → grid.js → hier.
|
|
591
|
+
...(el.state_dependent != null
|
|
592
|
+
? { state_dependent: el.state_dependent }
|
|
593
|
+
: {}),
|
|
594
|
+
// §F-AT-6-09 / R6-MEDIA (media_dependent): auch im inspect-Output
|
|
595
|
+
// durchreichen (dasselbe elementSchema). Datenkette: playwright.js →
|
|
596
|
+
// grid.js → hier.
|
|
597
|
+
...(el.media_dependent != null
|
|
598
|
+
? { media_dependent: el.media_dependent }
|
|
599
|
+
: {}),
|
|
600
|
+
// §HEAL-5 / Zeit-Achse (motion_dependent): auch im inspect-Output
|
|
601
|
+
// durchreichen (dasselbe elementSchema; exakt nach media_dependent-
|
|
602
|
+
// Vorlage). Datenkette: playwright.js → grid.js → hier.
|
|
603
|
+
...(el.motion_dependent != null
|
|
604
|
+
? { motion_dependent: el.motion_dependent }
|
|
605
|
+
: {}),
|
|
606
|
+
// §H10 R11-06 / Paint-Zeit-Achse (paint_time_variant): auch im
|
|
607
|
+
// inspect-Output durchreichen (exakt nach motion_dependent-Vorlage).
|
|
608
|
+
...(el.paint_time_variant != null
|
|
609
|
+
? { paint_time_variant: el.paint_time_variant }
|
|
610
|
+
: {}),
|
|
611
|
+
})),
|
|
612
|
+
// §HEAL-R6 / T1 "das Kabel" (F-AT-6-08): classifyCanvas-Verdikt aus dem
|
|
613
|
+
// Caller (pipeline.js, aus resolved.sanitize_loss DIREKT — Anti-LECK-3).
|
|
614
|
+
// Selbes additive optional-Muster wie formatStructured.
|
|
615
|
+
...(opts.canvasValidity !== undefined
|
|
616
|
+
? { canvas_validity: opts.canvasValidity }
|
|
617
|
+
: {}),
|
|
618
|
+
// §H10 R11-01: Existenz-Register auch im inspect-Output (Kanal-Parität,
|
|
619
|
+
// selbes additive optional-Muster wie formatStructured).
|
|
620
|
+
...(Array.isArray(gridMap.hidden) && gridMap.hidden.length > 0
|
|
621
|
+
? { hidden_elements: gridMap.hidden }
|
|
622
|
+
: {}),
|
|
623
|
+
},
|
|
624
|
+
};
|
|
625
|
+
if (truncatedWarnings.length > 0) {
|
|
626
|
+
out.meta = { truncated_warnings: truncatedWarnings };
|
|
627
|
+
}
|
|
628
|
+
return out;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
/**
|
|
632
|
+
* Formats palette output.
|
|
633
|
+
*/
|
|
634
|
+
export function formatPaletteStructured(elements) {
|
|
635
|
+
return {
|
|
636
|
+
colors: elements.slice(0, 7).map((el) => ({
|
|
637
|
+
id: el.id,
|
|
638
|
+
// §F-AT-7-02 (SK4): die Palette führt den ECHTEN parsed fill, NICHT el.color.
|
|
639
|
+
// Seit der stroke-Farb-Heilung kann el.color bei einem stroke-only-Element die
|
|
640
|
+
// STROKE-Farbe sein (visible_color_source='stroke') — el.color hier zu nutzen
|
|
641
|
+
// erzeugte eine neue Lüge (stroke-only → palette.fill:rot, obwohl der fill
|
|
642
|
+
// 'none'/transparent ist). grid.js bewahrt den parsed fill separat in
|
|
643
|
+
// fill_color; Fallback auf el.color nur, falls fill_color fehlt (Nicht-Grid-
|
|
644
|
+
// Aufrufer) — dort ist el.color weiterhin fill-derived (kein visible_color_source).
|
|
645
|
+
fill: el.fill_color != null ? el.fill_color : el.color,
|
|
646
|
+
stroke: el.stroke && el.stroke !== 'transparent' ? el.stroke : null,
|
|
647
|
+
})),
|
|
648
|
+
};
|
|
649
|
+
}
|