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,214 @@
1
+ /**
2
+ * honesty.js - Honesty-Gate (core-Invariante, Schritt 1: Fundament)
3
+ * Vector Mirror v2.0
4
+ *
5
+ * Die deterministische (REGEL-9, LLM-frei) und hexagonal-REINE (REGEL-4:
6
+ * KEINE Imports aus adapters/ oder interface/ — diese Datei hat bewusst
7
+ * NULL Imports) Klassifikations-/Assertions-Bibliothek, die SPAETER vor
8
+ * jeder Emission laeuft. Schritt 1 liefert nur die Bibliothek + Unit-Tests;
9
+ * es gibt JETZT noch keinen Caller (Verdrahtung = Schritt 3/4).
10
+ *
11
+ * Vier reine Funktionen. Pessimismus-Propagation (Spotter-Anti-Luege):
12
+ * im Zweifel wird Sicherheit verweigert, nicht behauptet.
13
+ *
14
+ * Quell-Verankerung (byte-genauer Port der heutigen Semantik):
15
+ * - allowDeltas == adapters/emitter/structured.js:359
16
+ * - assertEmissionGated == interface/schema.js:316-345 (superRefine, Live-Form)
17
+ * - countTruncation ersetzt spaeter das harte structured.js:426 suppressed:0
18
+ *
19
+ * Determinismus: keine Date, kein Math.random, kein I/O, kein LLM.
20
+ */
21
+
22
+ /**
23
+ * allowDeltas — das SSOT-Gate. Byte-genauer Port der heutigen Semantik aus
24
+ * structured.js:359 (`const allowDeltas = elReliability === 'reliable';`).
25
+ *
26
+ * NUR `reliability → bool`. Die "default-reliable-bei-fehlender-id"-Logik
27
+ * (Lookup-Concern) bleibt beim spaeteren Caller — sie ist hier bewusst NICHT
28
+ * enthalten. 'approximate' / 'not_measurable' / alles andere → false
29
+ * (Pessimismus). Strikt case-sensitiv, exakt wie der Tripel-Gleich-Vergleich.
30
+ *
31
+ * @param {string} reliability - z.B. 'reliable' | 'approximate' | 'not_measurable'
32
+ * @returns {boolean} true gdw. reliability strikt === 'reliable'
33
+ */
34
+ export function allowDeltas(reliability) {
35
+ return reliability === 'reliable';
36
+ }
37
+
38
+ /**
39
+ * bboxTrustedForVerdict — §H10 R11-07: darf eine bbox ein GRÜNES Verdikt
40
+ * tragen? Wohnt in der SSOT-Datei der Reliability-Semantik (REGEL-4, W1b-Pin),
41
+ * ist aber bewusst ein ANDERES Prädikat als allowDeltas (PRAEDIKAT-DISZIPLIN,
42
+ * s.o.): allowDeltas verweigert Pixel-Korrekturen schon bei 'approximate'
43
+ * (eine VORSCHREIBUNG braucht die exakte Zahl); das VERDIKT bleibt bei
44
+ * 'approximate' tragfähig (die Zahl ist annähernd wahr — Anti-Über-Gaten).
45
+ * NUR 'not_measurable' (3D-Transform / Non-SMIL-Motion: die Zahl selbst ist
46
+ * erklärt nicht reproduzierbar) trägt weder Korrektur noch grünes Verdikt.
47
+ *
48
+ * @param {string} reliability - z.B. 'reliable' | 'approximate' | 'not_measurable'
49
+ * @returns {boolean} false gdw. reliability strikt === 'not_measurable'
50
+ */
51
+ export function bboxTrustedForVerdict(reliability) {
52
+ return reliability !== 'not_measurable';
53
+ }
54
+
55
+ /**
56
+ * gateCorrections — die EINE Pessimismus-Entscheidung pro failing-issue, am
57
+ * Emissions-Rand, EINMAL. Reiner (REGEL-4, importfrei) Port der heute in
58
+ * structured.js:358-365 verstreuten Inline-Logik in EINE Wahrheits-Quelle.
59
+ *
60
+ * Annotiert jedes issue mit `_gated = allowDeltas(reliabilityOf(issue.id) ?? '?')`.
61
+ * `reliabilityOf` ist eine reine Caller-Closure (id → reliability|undefined); der
62
+ * '?'-Default bei fehlendem Treffer macht `allowDeltas` zum Default-deny
63
+ * (Spotter-Anti-Luege: unbekannte id → keine Pixel-Korrektur).
64
+ *
65
+ * Bei `_gated === false` liefert die Funktion ein um dx/dy/dw/dh BEREINIGTES
66
+ * Derivat — UND eine um die Korrektur-VORSCHREIBUNG bereinigte `detail`-Prosa
67
+ * (sanitizeCorrectionDetail). Der Spotter-Anti-Luege-Schutz deckt damit BEIDE
68
+ * Kanaele, durch die ein Delta entkommen kann: das strukturierte Objekt-Feld
69
+ * (dx/dy/dw/dh) UND den detail-STRING (`. Korrektur: dx=..`, `Δw=..`,
70
+ * `Kürzester Fluchtweg: ..`), den die Constraint-Produzenten in denselben
71
+ * String einbetten. EIN Gate, vollstaendig. Bei `_gated === true` bleibt das
72
+ * issue (bis auf das additive `_gated`-Vertragsfeld) BYTE-IDENTISCH — die
73
+ * Sanitisierung laeuft NUR im deny-Zweig.
74
+ *
75
+ * PRAEDIKAT-DISZIPLIN (REGEL-9, bewusst NICHT zusammenfuehren): dieses Live-Gate
76
+ * nutzt `allowDeltas` (=== 'reliable', 4 Keys dx/dy/dw/dh, deny-unknown).
77
+ * `assertEmissionGated` (test-only Backstop, schema.js-Spiegel) nutzt ein
78
+ * ANDERES Praedikat (dx/dy-only, allow-unknown). Die beiden DECKEN SICH NICHT
79
+ * und duerfen es nicht — ein Merge wuerde entweder das Live-Gate aufweichen
80
+ * (dw/dh-Leak) oder den Backstop ueber-streng machen (unknown-id-Fehlalarm).
81
+ *
82
+ * @param {Array<{id?: string}>} failing - die failing-Liste (arbitrated.failing).
83
+ * @param {(id: string) => (string|undefined)} reliabilityOf - reine Lookup-Closure.
84
+ * @returns {Array} annotierte (und bei deny bereinigte) issue-Derivate.
85
+ */
86
+ export function gateCorrections(failing, reliabilityOf) {
87
+ if (!Array.isArray(failing)) return [];
88
+ return failing.map((issue) => {
89
+ const _gated = allowDeltas(reliabilityOf(issue.id) ?? '?');
90
+ if (_gated) return { ...issue, _gated };
91
+ const { dx, dy, dw, dh, ...rest } = issue;
92
+ if (typeof rest.detail === 'string') {
93
+ rest.detail = sanitizeCorrectionDetail(rest.detail);
94
+ }
95
+ return { ...rest, _gated };
96
+ });
97
+ }
98
+
99
+ /**
100
+ * sanitizeCorrectionDetail — entfernt die VORGESCHRIEBENE Pixel-Korrektur aus
101
+ * dem detail-String eines failing-issue, behaelt aber die WAS/WIEVIEL-MESSUNG
102
+ * (VISION Prinzip 3: das Auge misst die Luecke, verschreibt sie nicht). Nur im
103
+ * deny-Zweig von gateCorrections aufgerufen (reliable bleibt byte-identisch).
104
+ *
105
+ * Exhaustiver Port der 3 Produzenten-Formen (grep-belegt, src/core/constraints):
106
+ * - `<WAS>. Korrektur: <deltas>` (centered-in, LEFT-OF, ABOVE,
107
+ * ALIGNED-LEFT/TOP, INSIDE)
108
+ * - `<WAS>. Kürzester Fluchtweg: <delta>` (NO-OVERLAP)
109
+ * - `Grösse weicht ab (Δw=..px, Δh=..px)` (SAME-SIZE)
110
+ * In allen Faellen wird die delta-tragende Vorschreibungs-Klausel abgeschnitten,
111
+ * die reine Beschreibung des Bruchs bleibt. distance.js (`Zu nah dran (..px statt
112
+ * ..px, Defizit ~..px)`) traegt KEINE Vorschreibung → bleibt unangetastet
113
+ * (ehrliche Distanz-Messung, kein Leak).
114
+ *
115
+ * @param {string} detail
116
+ * @returns {string}
117
+ */
118
+ function sanitizeCorrectionDetail(detail) {
119
+ // Form 1+2: alles ab der Vorschreibungs-Klausel (Korrektur:/Fluchtweg:) inkl.
120
+ // des vorausgehenden Satz-Trenners abschneiden.
121
+ let out = detail.replace(/\.\s*(Korrektur|Kürzester Fluchtweg):.*$/u, '.');
122
+ // Form 3: den ` (Δw=..px, Δh=..px)`-Paren entfernen (SAME-SIZE).
123
+ out = out.replace(/\s*\(Δ[wh]=[^)]*\)/u, '');
124
+ return out;
125
+ }
126
+
127
+ /**
128
+ * classifyCanvas — Canvas-Validitaet aus zwei orthogonalen Renderer-Signalen.
129
+ *
130
+ * Pessimismus-Praezedenz (am-staerksten-degradiert gewinnt). Reihenfolge:
131
+ * 1. nicht-leerer sanitizeLoss → 'lossy' (DOMPurify hat
132
+ * Semantik entfernt; dominiert alles)
133
+ * 2. viewBoxValidity === 'degenerate' → 'degenerate' (NaN/zero/negativ)
134
+ * 3. viewBoxValidity === 'default_replaced' → 'default_replaced' (300x150 legitim)
135
+ * 4. sonst → 'valid'
136
+ *
137
+ * Die Eingabe-Felder erzeugt der Renderer (Schritt 2); hier nur die reine
138
+ * Klassifikation synthetischer Eingaben.
139
+ *
140
+ * @param {{viewBoxValidity?: string, sanitizeLoss?: Array}} resolved
141
+ * @returns {'valid'|'default_replaced'|'degenerate'|'lossy'}
142
+ */
143
+ export function classifyCanvas({ viewBoxValidity, sanitizeLoss } = {}) {
144
+ if (Array.isArray(sanitizeLoss) && sanitizeLoss.length > 0) return 'lossy';
145
+ if (viewBoxValidity === 'degenerate') return 'degenerate';
146
+ if (viewBoxValidity === 'default_replaced') return 'default_replaced';
147
+ return 'valid';
148
+ }
149
+
150
+ /**
151
+ * assertEmissionGated — Live-Form der superRefine-Invariante aus
152
+ * schema.js:316-345. Reine Pruefung: liefert die Liste der Verstoesse (entscheidet
153
+ * NICHT ueber die Fail-closed-Reaktion — das tut der Caller in Schritt 3).
154
+ *
155
+ * Ein Verstoss liegt vor, wenn eine correction ein dx oder dy traegt, deren
156
+ * Ziel-Element bbox_reliability ∈ {'not_measurable','approximate'} hat.
157
+ * Map-Lookup-Muster wie schema.js:319-321; '#'-Praefix-Strip wie schema.js:326;
158
+ * Lookup-Fehlschlag (reliability === undefined) ist KEIN Verstoss (schema.js:332).
159
+ *
160
+ * @param {{scene?: {elements?: Array}, corrections?: Array}} structured
161
+ * @returns {Array<{index: number, element: string, reliability: string, message: string}>}
162
+ */
163
+ export function assertEmissionGated(structured) {
164
+ const violations = [];
165
+ if (!structured?.scene) return violations;
166
+ const elements = structured.scene.elements;
167
+ if (!Array.isArray(elements)) return violations;
168
+ const corrections = structured.corrections;
169
+ if (!Array.isArray(corrections)) return violations;
170
+
171
+ const reliabilityById = new Map();
172
+ for (const el of elements) {
173
+ reliabilityById.set(el.id, el.bbox_reliability);
174
+ }
175
+
176
+ for (let i = 0; i < corrections.length; i++) {
177
+ const c = corrections[i];
178
+ if (!c || typeof c.element !== 'string') continue;
179
+ const id = c.element.replace(/^#/, '');
180
+ const reliability = reliabilityById.get(id);
181
+ // Lookup-Fehlschlag: separater REGEL-3-Bruch (β-003), hier kein Verstoss.
182
+ if (reliability === undefined) continue;
183
+ if (reliability === 'not_measurable' || reliability === 'approximate') {
184
+ const hasDx = c.dx !== undefined;
185
+ const hasDy = c.dy !== undefined;
186
+ if (hasDx || hasDy) {
187
+ violations.push({
188
+ index: i,
189
+ element: c.element,
190
+ reliability,
191
+ message:
192
+ `REGEL-3: corrections[${i}].element='${c.element}' verweist auf scene.elements mit ` +
193
+ `bbox_reliability='${reliability}' — dx/dy duerfen nicht emittiert werden (Spotter-Anti-Luege).`,
194
+ });
195
+ }
196
+ }
197
+ }
198
+ return violations;
199
+ }
200
+
201
+ /**
202
+ * countTruncation — ehrlicher Trunkierungs-Zaehler. Ersetzt spaeter das harte
203
+ * structured.js:426 `suppressed: 0`.
204
+ *
205
+ * @param {Array} items - die vollstaendige (ungekuerzte) Liste
206
+ * @param {number} cap - die Obergrenze sichtbarer Eintraege
207
+ * @returns {{total: number, returned: number, suppressed: number}}
208
+ */
209
+ export function countTruncation(items, cap) {
210
+ const total = items.length;
211
+ const returned = Math.min(cap, total);
212
+ const suppressed = Math.max(0, total - cap);
213
+ return { total, returned, suppressed };
214
+ }
@@ -0,0 +1,104 @@
1
+ /**
2
+ * auto_ids.js — §1.3 Auto-ID Content-Addressed Hash-Namespace
3
+ * Vector Mirror v2.0 (Sprint-β2 §1.3 LAYER-TRENNUNG, Patch2)
4
+ *
5
+ * Core module: NO imports from adapters/ or interface/ (REGEL-4 Hexagonal).
6
+ * NO external deps — node:crypto is built-in (REGEL-7 Eigenstaendigkeit).
7
+ *
8
+ * Layer-Trennung (Patch2, Mini-Review-Konvergenz Opus+Codex):
9
+ * Dieses Modul ist PURE FORMAT-Layer. Es kennt das Auto-ID-Format
10
+ * `_<8hex>_<tag><n>` und erzeugt Strings nach diesem Format — aber es
11
+ * kennt KEINE Tag-Allowlist und entscheidet NICHT, welche Tags gültig
12
+ * sind. Domain-Vertrag (Spotter-Set + Format-Regex) lebt in
13
+ * `../element_vocabulary.js`. Aufrufer entscheidet vorab, welche Tags
14
+ * überhaupt eine Auto-ID erhalten (Renderer-Adapter ruft isSpotterTag
15
+ * aus element_vocabulary.js).
16
+ *
17
+ * Konsequenz: formatAutoId/extractAutoIds sind NICHT "total" über
18
+ * beliebige Tags (F-PATCH-CODEX-002) — sie sind als pure Format-Helper
19
+ * konzipiert. Das ist BEABSICHTIGT: die Allowlist gehört in den
20
+ * Domain-Layer, nicht in den Format-Layer. Würde extractAutoIds selbst
21
+ * filtern, wäre `element_vocabulary.js` doppelt importiert (Layer-
22
+ * Vermischung). Die Verantwortung "nur Spotter-Tags reinreichen"
23
+ * liegt beim Caller (siehe playwright.js nextAutoId-Gate).
24
+ *
25
+ * Adressiert D-004 (ADR-025 §3 Auto-ID-Versprechen, 0 Treffer im Code) und
26
+ * FIX_PLAN §1.3 (Beschluss B-4-modified, 8 hex statt 6 hex).
27
+ *
28
+ * Format: _<8hex>_<tag><n>
29
+ * <8hex> = sha256(sanitizedSvg) als hex, truncated auf 8 chars (32 bit)
30
+ * <tag> = lowercase Element-Tag (rect, circle, path, ...)
31
+ * <n> = monoton steigender, tag-lokaler Counter (1, 2, 3, ...)
32
+ *
33
+ * Birthday-Bound (FIX_PLAN §1.3 Tabelle):
34
+ * 8 hex (32 bit) → P(Kollision) bei N=1000 ≈ 0.012 %, 50%-Punkt N ≈ 77163.
35
+ *
36
+ * Hash-Berechnung MUSS in Node-Land VOR page.setContent passieren (H-18 §1.3).
37
+ * Der Browser-Kontext hat kein node:crypto — daher ist diese Funktion PURE
38
+ * und arbeitet auf dem bereits-sanitized SVG-String (sanitizedSvg) plus
39
+ * einer in-Browser-extrahierten DOM-Element-Liste (tag-Liste reicht).
40
+ */
41
+ import { createHash } from 'node:crypto';
42
+
43
+ /**
44
+ * Berechnet den 8-hex SHA-256-Praefix fuer den uebergebenen sanitizedSvg.
45
+ * Pure: gleiche Eingabe → gleiche Ausgabe (Determinismus-Garantie).
46
+ *
47
+ * @param {string} sanitizedSvg - DOMPurified SVG-String (Node-Land).
48
+ * @returns {string} 8-stelliger lowercase hex-Praefix.
49
+ */
50
+ export function computeSvgHashPrefix(sanitizedSvg) {
51
+ if (typeof sanitizedSvg !== 'string') {
52
+ throw new TypeError('computeSvgHashPrefix erwartet einen String');
53
+ }
54
+ return createHash('sha256').update(sanitizedSvg).digest('hex').slice(0, 8);
55
+ }
56
+
57
+ /**
58
+ * Generiert die Auto-ID fuer ein einzelnes Element.
59
+ * Format: _<8hex>_<tag><n>
60
+ *
61
+ * @param {string} hashPrefix - 8-hex Praefix (siehe computeSvgHashPrefix).
62
+ * @param {string} tag - lowercase Element-Tag (z.B. 'rect').
63
+ * @param {number} n - tag-lokaler Counter (1-basiert).
64
+ * @returns {string} Auto-ID.
65
+ */
66
+ export function formatAutoId(hashPrefix, tag, n) {
67
+ return `_${hashPrefix}_${tag}${n}`;
68
+ }
69
+
70
+ /**
71
+ * Pure Generator-Funktion fuer Auto-IDs ueber eine Tag-Sequenz.
72
+ *
73
+ * Eingabe ist die geordnete Tag-Sequenz der DOM-Elemente OHNE explizite
74
+ * SVG-id (Reihenfolge wie sie im SVG-DOM erscheinen). Der Counter ist
75
+ * tag-lokal und monoton steigend, sodass _<hash>_rect1, _<hash>_rect2,
76
+ * _<hash>_circle1, _<hash>_rect3 entstehen — analog der bisherigen
77
+ * positional-stable DOM-Path-Logik, aber content-addressed via Hash-Praefix.
78
+ *
79
+ * Aufrufer-Vertrag (playwright.js):
80
+ * - sanitizedSvg ist im Node-Land VOR page.setContent verfuegbar.
81
+ * - tagSequence wird in der Browser-Iteration aufgebaut (gleiche Reihenfolge
82
+ * wie elements.push()), nur fuer Elemente ohne explizite id.
83
+ * - Rueckgabe: Array von Auto-IDs in derselben Reihenfolge wie tagSequence.
84
+ *
85
+ * @param {string} sanitizedSvg - DOMPurified SVG-String.
86
+ * @param {ReadonlyArray<string>} tagSequence - Geordnete Tag-Liste der
87
+ * Elemente ohne explizite id (z.B. ['rect', 'rect', 'circle']).
88
+ * @returns {string[]} Auto-IDs in derselben Reihenfolge.
89
+ */
90
+ export function extractAutoIds(sanitizedSvg, tagSequence) {
91
+ if (!Array.isArray(tagSequence)) {
92
+ throw new TypeError('extractAutoIds erwartet ein Array als tagSequence');
93
+ }
94
+ const hashPrefix = computeSvgHashPrefix(sanitizedSvg);
95
+ const counters = new Map();
96
+ const out = new Array(tagSequence.length);
97
+ for (let i = 0; i < tagSequence.length; i++) {
98
+ const tag = tagSequence[i];
99
+ const next = (counters.get(tag) || 0) + 1;
100
+ counters.set(tag, next);
101
+ out[i] = formatAutoId(hashPrefix, tag, next);
102
+ }
103
+ return out;
104
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * tolerance.js - 5% Tolerance Rule
3
+ * Extracted from mirror.js:32-33
4
+ * Vector Mirror v2.0
5
+ *
6
+ * Tolerance formula: tolerance = min(cellW * 0.5, max(2, dimension * 0.05))
7
+ * See BAUPLAN S.13 (Eichkoerper EK-3: 5%-Toleranz-Regel)
8
+ */
9
+
10
+ /**
11
+ * Returns tolerance for X-axis based on dimension and grid.
12
+ */
13
+ export function getTolX(dimension, grid) {
14
+ return Math.min(grid.cellW * 0.5, Math.max(2, dimension * 0.05));
15
+ }
16
+
17
+ /**
18
+ * Returns tolerance for Y-axis based on dimension and grid.
19
+ */
20
+ export function getTolY(dimension, grid) {
21
+ return Math.min(grid.cellH * 0.5, Math.max(2, dimension * 0.05));
22
+ }