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,233 @@
1
+ /**
2
+ * distance.js - DISTANCE-FROM Constraint
3
+ * Migrated from mirror.js:94-102
4
+ * Vector Mirror v2.0
5
+ * Uses ctx.value (multiplier, default 1) + ctx.grid
6
+ */
7
+ import { registerConstraint } from './registry.js';
8
+
9
+ registerConstraint('DISTANCE-FROM', {
10
+ check(subj, ref, { grid, value }) {
11
+ const gapX = Math.max(
12
+ 0,
13
+ Math.max(subj.bbox.x, ref.bbox.x) -
14
+ Math.min(subj.bbox.x + subj.bbox.w, ref.bbox.x + ref.bbox.w),
15
+ );
16
+ const gapY = Math.max(
17
+ 0,
18
+ Math.max(subj.bbox.y, ref.bbox.y) -
19
+ Math.min(subj.bbox.y + subj.bbox.h, ref.bbox.y + ref.bbox.h),
20
+ );
21
+ const actualDist = Math.sqrt(gapX * gapX + gapY * gapY);
22
+ const targetValue = value ?? 1;
23
+ // D-003: Ein negativer Ziel-Abstand ist eine INVALIDE Mess-Vorgabe — kein
24
+ // physikalisch sinnvoller Abstand kann < 0 sein. Ohne diese Wache liefe
25
+ // `actualDist >= expectedDist` mit expectedDist < 0 IMMER auf pass=true
26
+ // hinaus (jede nicht-negative reale Distanz erfuellt eine negative Schranke),
27
+ // d.h. das Auge meldete still "Alles korrekt" auf eine unmessbare Vorgabe.
28
+ // Das verletzt Prinzip-3 (keine stille Korrektur/Beschoenigung im Auge):
29
+ // wir CLAMPEN NICHT (das waere eine stille Korrektur der Vorgabe), sondern
30
+ // reichen den Wert unveraendert durch und melden ehrlich Unmessbarkeit.
31
+ // pass:null → arbitrate.js sortiert nach `unchecked`, NIE nach `passing`.
32
+ if (targetValue < 0) {
33
+ return {
34
+ pass: null,
35
+ reasonCode: 'INVALID_MEASUREMENT',
36
+ reasonCategory: 'SPECIFICATION',
37
+ detail: `Negativer Ziel-Abstand (${targetValue}) ist keine messbare Vorgabe — ein Abstand kann nicht < 0 sein.`,
38
+ };
39
+ }
40
+ const expectedDist = targetValue * grid.cellW;
41
+ const pass = actualDist >= expectedDist;
42
+ const shortfall = expectedDist - actualDist;
43
+ if (pass) {
44
+ return { pass: true, detail: null };
45
+ }
46
+ //
47
+ // + F-CODEX-Re-Review-F3 (Patch-2, 2026-05-31):
48
+ // Strukturierte Pixel-Korrektur dx/dy zusaetzlich zur detail-Prosa.
49
+ // REGEL-3 verlangt maschinenlesbare Deltas. INV-3 fordert Korrektheit:
50
+ // nach REALER AABB-Translation MUSS actualDist_after >= expectedDist.
51
+ //
52
+ // Strategie: VECTOR-SCALING (nicht 1D-Dominant-Axis).
53
+ // Begruendung: Euklid-Distanz ist 2D. Die alte 1D-Dominant-Axis-Variante
54
+ // lieferte fuer gapX=gapY=10, expected=20 ein dx=6 → neue Distanz
55
+ // sqrt(16^2+10^2)=18.87 < 20 (Codex-Probe Patch-1). Korrekt ist
56
+ // Skalierung des AABB-Gap-Vektors auf expectedDist via
57
+ // factor = expectedDist/actualDist.
58
+ //
59
+ // Verifikation Codex-Probe (gapX=gapY=10, expected=20):
60
+ // actualDist=14.14, factor=1.414, rawDx=rawDy=4.14
61
+ // nach Apply: gap=(14.14,14.14), dist=19.99 ~ 20 ✓
62
+ //
63
+ // Sonderfaelle (in Reihenfolge):
64
+ // 1) actualDist == 0 (Elemente raeumlich ueberlappend, gapX=gapY=0):
65
+ // Vector-Skalierung undefiniert. Push subj's linke (bzw. rechte) Kante
66
+ // auf ref's gegenueberliegende Kante + expectedDist.
67
+ // F-CODEX-Re-Review-F3-Fix:
68
+ // Patch-1 setzte dx = expectedDist. Bei subj=ref=(0,0,10,10),
69
+ // expected=20 → dx=20, neuer subj.x=20, AABB-gapX=20-10=10 → newDist=10 < 20 ✗
70
+ // Korrekt: targetSubjX = ref.x + ref.w + expectedDist (push rechts)
71
+ // dx = targetSubjX - subj.x = (ref.x + ref.w - subj.x) + expectedDist
72
+ // Verifikation F-3-Probe (subj=ref=(0,0,10,10), expected=20):
73
+ // dx = (0+10-0) + 20 = 30. Neuer subj.x=30. AABB-gapX=30-10=20.
74
+ // newDist=20 >= 20 ✓
75
+ // Sign-Wahl: signX aus centerDx (falls != 0), sonst default +1
76
+ // (push rechts, analog arrange()-Fallback Z.148).
77
+ // 2) centerDx == 0 UND centerDy == 0 (identisch zentriert ABER mit gap>0,
78
+ // z.B. konzentrische Boxen): Sign-Vektor undefiniert. Push in X um
79
+ // shortfall (Konsistenz mit arrange()-Fallback). Hinweis: bei
80
+ // identisch zentrierten Boxen IST actualDist == 0 (gapX=gapY=0
81
+ // geometrisch zwingend), d.h. dieser Pfad ist defensiv und wird
82
+ // vom Sonderfall 1 oben bereits abgedeckt — bleibt nur fuer
83
+ // Floating-Point-Pathologie.
84
+ // 3) Sonst: rawDx/rawDy = (factor-1) * gapX/gapY, Sign aus center-Relativ.
85
+ // Math.ceil fuer Korrektheit. Mindestens 1px nur wenn rawAchse != 0
86
+ // (kein Achsen-Push fuer perfekt-vertikale/horizontale Gaps mit gap=0).
87
+ const sCx = subj.bbox.x + subj.bbox.w / 2;
88
+ const sCy = subj.bbox.y + subj.bbox.h / 2;
89
+ const rCx = ref.bbox.x + ref.bbox.w / 2;
90
+ const rCy = ref.bbox.y + ref.bbox.h / 2;
91
+ const centerDx = sCx - rCx;
92
+ const centerDy = sCy - rCy;
93
+ // G2 (D-006): detail beschreibt NUR das WAS/WIEVIEL-daneben (Ist-Distanz
94
+ // vs. Soll-Distanz, Defizit). Die strukturierte Korrektur lebt
95
+ // ausschliesslich in dx/dy — prose.js/structured.js bauen den
96
+ // Korrektur-Hinweis aus diesen Feldern, nicht mehr aus dem detail-String.
97
+ const detail = `Zu nah dran (${Math.round(actualDist)}px statt ${Math.round(expectedDist)}px, Defizit ~${Math.round(shortfall)}px)`;
98
+ // Sonderfall 1: actualDist == 0 → keine Skalierung moeglich.
99
+ // F-CODEX-Re-Review-F3-Fix: dx MUSS die raeumliche Ueberlappung +
100
+ // expectedDist ueberwinden. Push entlang dominanter Achse (X bei
101
+ // |centerDx|>=|centerDy|, sonst Y). Default +X bei centerDx==centerDy==0.
102
+ if (actualDist === 0) {
103
+ const dominantX = Math.abs(centerDx) >= Math.abs(centerDy);
104
+ if (dominantX) {
105
+ const signX = centerDx >= 0 ? 1 : -1;
106
+ // signX>0: subj nach rechts. targetSubjX = ref.x + ref.w + expectedDist
107
+ // signX<0: subj nach links. targetSubjX = ref.x - subj.w - expectedDist
108
+ const targetSubjX =
109
+ signX > 0
110
+ ? ref.bbox.x + ref.bbox.w + expectedDist
111
+ : ref.bbox.x - subj.bbox.w - expectedDist;
112
+ const rawDx = targetSubjX - subj.bbox.x;
113
+ const dxRounded =
114
+ rawDx >= 0 ? Math.ceil(rawDx) : -Math.ceil(Math.abs(rawDx));
115
+ const dxFinal = dxRounded === 0 ? signX : dxRounded;
116
+ return { pass: false, detail, dx: dxFinal };
117
+ } else {
118
+ const signY = centerDy >= 0 ? 1 : -1;
119
+ const targetSubjY =
120
+ signY > 0
121
+ ? ref.bbox.y + ref.bbox.h + expectedDist
122
+ : ref.bbox.y - subj.bbox.h - expectedDist;
123
+ const rawDy = targetSubjY - subj.bbox.y;
124
+ const dyRounded =
125
+ rawDy >= 0 ? Math.ceil(rawDy) : -Math.ceil(Math.abs(rawDy));
126
+ const dyFinal = dyRounded === 0 ? signY : dyRounded;
127
+ return { pass: false, detail, dy: dyFinal };
128
+ }
129
+ }
130
+ // Sonderfall 2: identisch zentriert (centerDx==0 AND centerDy==0).
131
+ // Defensiv: geometrisch wird das bereits von Sonderfall 1 abgefangen
132
+ // (centers identisch ⇒ gapX=gapY=0 ⇒ actualDist=0), bleibt nur fuer
133
+ // Floating-Point-Pathologie.
134
+ if (centerDx === 0 && centerDy === 0) {
135
+ return {
136
+ pass: false,
137
+ detail,
138
+ dx: Math.max(1, Math.round(shortfall)),
139
+ };
140
+ }
141
+ // Vector-Scaling.
142
+ const factor = expectedDist / actualDist; // > 1 weil pass=false
143
+ const rawDx = (factor - 1) * gapX;
144
+ const rawDy = (factor - 1) * gapY;
145
+ // Sign aus center-Relativ (push-weg-vektor). Bei centerDx==0 (perfekt
146
+ // vertikal ausgerichtet): rawDx ist ohnehin 0 (gapX==0 in dem Setup) →
147
+ // sign-Wahl egal, dx=0.
148
+ const signX = centerDx >= 0 ? 1 : -1;
149
+ const signY = centerDy >= 0 ? 1 : -1;
150
+ // Aufrunden (Math.ceil) statt Math.round gegen Rundungs-Defizit:
151
+ // Math.round(4.142)=4 wuerde fuer Codex-Probe newDist=19.8 < 20 liefern
152
+ // und INV-3-Korrektheit brechen. Math.ceil garantiert
153
+ // newGap_axis >= raw_target → newDist >= expectedDist.
154
+ // Mindest-Push 1px erfolgt nur wenn rawAchse != 0 (sonst wuerde ein
155
+ // 1px-Push auf eine Achse ohne Gap kreiert, was semantisch falsch waere).
156
+ let dx = 0;
157
+ let dy = 0;
158
+ if (rawDx !== 0) {
159
+ const r = Math.ceil(Math.abs(rawDx));
160
+ dx = Math.max(1, r) * signX;
161
+ }
162
+ if (rawDy !== 0) {
163
+ const r = Math.ceil(Math.abs(rawDy));
164
+ dy = Math.max(1, r) * signY;
165
+ }
166
+ // Mindestens eine der beiden Achsen MUSS != 0 sein (REGEL-3-Vertrag).
167
+ // Wenn beide rawDx, rawDy == 0 waeren, waere actualDist == 0 — und das
168
+ // ist oben (Sonderfall 1) bereits abgefangen. Defensiv: wenn doch beide
169
+ // 0 (Floating-Point-Pathologie), fallback dx=1.
170
+ if (dx === 0 && dy === 0) {
171
+ dx = 1;
172
+ }
173
+ // Output: nur gesetzte Achsen aufnehmen (kanonische Form analog
174
+ // centered-in.js — undefined statt 0 ist semantisch "keine Korrektion").
175
+ const out = { pass: false, detail };
176
+ if (dx !== 0) out.dx = dx;
177
+ if (dy !== 0) out.dy = dy;
178
+ return out;
179
+ },
180
+ arrange(subj, ref, { canvas, value }) {
181
+ const cellsX = Math.max(4, Math.min(16, Math.round(canvas.width / 50)));
182
+ const cellW = canvas.width / cellsX;
183
+ const minDist = (value ?? 1) * cellW;
184
+
185
+ // AABB gap metric — identical to check() for consistency (Audit Fix J)
186
+ const gapX = Math.max(
187
+ 0,
188
+ Math.max(subj.bbox.x, ref.bbox.x) -
189
+ Math.min(subj.bbox.x + subj.bbox.w, ref.bbox.x + ref.bbox.w),
190
+ );
191
+ const gapY = Math.max(
192
+ 0,
193
+ Math.max(subj.bbox.y, ref.bbox.y) -
194
+ Math.min(subj.bbox.y + subj.bbox.h, ref.bbox.y + ref.bbox.h),
195
+ );
196
+ const actualDist = Math.sqrt(gapX * gapX + gapY * gapY);
197
+
198
+ // Already far enough apart — no-op
199
+ if (actualDist >= minDist) {
200
+ return { x: subj.bbox.x, y: subj.bbox.y };
201
+ }
202
+
203
+ // Push along dominant axis (center-to-center direction)
204
+ const sCx = subj.bbox.x + subj.bbox.w / 2;
205
+ const sCy = subj.bbox.y + subj.bbox.h / 2;
206
+ const rCx = ref.bbox.x + ref.bbox.w / 2;
207
+ const rCy = ref.bbox.y + ref.bbox.h / 2;
208
+ const dx = sCx - rCx;
209
+ const dy = sCy - rCy;
210
+
211
+ // Deterministic fallback for identical positions: push right (Audit Fix D)
212
+ if (dx === 0 && dy === 0) {
213
+ return { x: ref.bbox.x + ref.bbox.w + minDist, y: subj.bbox.y };
214
+ }
215
+
216
+ // Push along dominant axis so AABB gap = minDist
217
+ if (Math.abs(dx) >= Math.abs(dy)) {
218
+ const sign = dx >= 0 ? 1 : -1;
219
+ const targetX =
220
+ sign > 0
221
+ ? ref.bbox.x + ref.bbox.w + minDist
222
+ : ref.bbox.x - subj.bbox.w - minDist;
223
+ return { x: targetX, y: subj.bbox.y };
224
+ } else {
225
+ const sign = dy >= 0 ? 1 : -1;
226
+ const targetY =
227
+ sign > 0
228
+ ? ref.bbox.y + ref.bbox.h + minDist
229
+ : ref.bbox.y - subj.bbox.h - minDist;
230
+ return { x: subj.bbox.x, y: targetY };
231
+ }
232
+ },
233
+ });
@@ -0,0 +1,22 @@
1
+ /**
2
+ * fill.js - FILL Constraint (arrange-only)
3
+ * Element fills the entire canvas (x:0, y:0, width:W, height:H)
4
+ * Vector Mirror v2.0 Phase 3a
5
+ */
6
+ import { registerConstraint } from './registry.js';
7
+
8
+ registerConstraint('FILL', {
9
+ // FILL braucht keine Referenz (Element fuellt die Leinwand). Ohne diesen
10
+ // Marker wuerde die pipeline-Wache (fail-closed-Default) FILL faelschlich
11
+ // als referenz-pflichtig behandeln.
12
+ requiresReference: false,
13
+ // Uniform dispatch signature (registry.js): check(subj, ref, ctx).
14
+ check(_subj, _ref, _ctx) {
15
+ // arrange-only constraint — check always returns null
16
+ return { pass: null, detail: 'FILL ist ein arrange-only Constraint' };
17
+ },
18
+ // Uniform dispatch signature (registry.js): arrange(subj, ref, ctx).
19
+ arrange(_subj, _ref, { canvas }) {
20
+ return { x: 0, y: 0, width: canvas.width, height: canvas.height };
21
+ },
22
+ });
@@ -0,0 +1,52 @@
1
+ /**
2
+ * inside.js - INSIDE Constraint
3
+ * Migrated from mirror.js:53-62
4
+ * Vector Mirror v2.0
5
+ */
6
+ import { registerConstraint } from './registry.js';
7
+
8
+ registerConstraint('INSIDE', {
9
+ // Uniform dispatch signature (registry.js): check(subj, ref, ctx).
10
+ check(subj, ref, _ctx) {
11
+ const inside =
12
+ subj.bbox.x >= ref.bbox.x &&
13
+ subj.bbox.y >= ref.bbox.y &&
14
+ subj.bbox.x + subj.bbox.w <= ref.bbox.x + ref.bbox.w &&
15
+ subj.bbox.y + subj.bbox.h <= ref.bbox.y + ref.bbox.h;
16
+ if (inside) return { pass: true, detail: null };
17
+ const dx =
18
+ subj.bbox.x < ref.bbox.x
19
+ ? ref.bbox.x - subj.bbox.x
20
+ : subj.bbox.x + subj.bbox.w > ref.bbox.x + ref.bbox.w
21
+ ? ref.bbox.x + ref.bbox.w - (subj.bbox.x + subj.bbox.w)
22
+ : 0;
23
+ const dy =
24
+ subj.bbox.y < ref.bbox.y
25
+ ? ref.bbox.y - subj.bbox.y
26
+ : subj.bbox.y + subj.bbox.h > ref.bbox.y + ref.bbox.h
27
+ ? ref.bbox.y + ref.bbox.h - (subj.bbox.y + subj.bbox.h)
28
+ : 0;
29
+ const cParts = [];
30
+ if (dx !== 0) cParts.push(`dx=${Math.round(dx)}px`);
31
+ if (dy !== 0) cParts.push(`dy=${Math.round(dy)}px`);
32
+ const result = {
33
+ pass: false,
34
+ detail: `Ragt aus #${ref.id} heraus. Korrektur: ${cParts.join(', ')}`,
35
+ };
36
+ if (dx !== 0) result.dx = Math.round(dx);
37
+ if (dy !== 0) result.dy = Math.round(dy);
38
+ return result;
39
+ },
40
+ // Uniform dispatch signature (registry.js): arrange(subj, ref, ctx).
41
+ arrange(subj, ref, _ctx) {
42
+ const x = Math.max(
43
+ ref.bbox.x,
44
+ Math.min(subj.bbox.x, ref.bbox.x + ref.bbox.w - subj.bbox.w),
45
+ );
46
+ const y = Math.max(
47
+ ref.bbox.y,
48
+ Math.min(subj.bbox.y, ref.bbox.y + ref.bbox.h - subj.bbox.h),
49
+ );
50
+ return { x, y };
51
+ },
52
+ });
@@ -0,0 +1,65 @@
1
+ /**
2
+ * loader.js - Side-Effect Imports for all Constraints + REGEL-3 Validation-Wrapper
3
+ * ADR-020: New constraint = 1 file + 1 import here
4
+ * Vector Mirror v2.0
5
+ *
6
+ *
7
+ * Zusaetzlich zum side-effect-Registrieren exportiert loader.js jetzt
8
+ * `validatedCheckConstraint(type, subj, ref, ctx)`. Diese Funktion
9
+ * delegiert an registry.checkConstraint und prueft das Resultat gegen
10
+ * constraintCheckSchema (REGEL-3 Output-Boundary-Gate, WELLE-β-004).
11
+ *
12
+ * pipeline.js bleibt UNVERAENDERT — Edit C ist additive API. Die
13
+ * Drift-Verhinderung greift via test_regel3_invariants.js (Edit H),
14
+ * das validatedCheckConstraint generativ ueber alle registrierten
15
+ * Constraints aufruft. Damit faengt jeder neue Constraint, der REGEL-3
16
+ * bricht (kein dx/dy bei fail), schon im Unit-Test (Fail-Fast).
17
+ *
18
+ * Warum nicht registry.js direkt patchen: registry.js liegt ausserhalb
19
+ * der 8 Patch-Files (H-9). Andon-Cord-#3-Vermeidung — andere Constraints
20
+ * (CENTERED-IN, NO-OVERLAP, etc.) bleiben unangetastet im Pipeline-Pfad,
21
+ * keine Pre-Existing-Drift wird stillschweigend "nebenbei" gefixed.
22
+ */
23
+ import './centered-in.js';
24
+ import './no-overlap.js';
25
+ import './inside.js';
26
+ import './aligned.js';
27
+ import './positional.js';
28
+ import './distance.js';
29
+ import './same-size.js';
30
+ import './color.js';
31
+ import './fill.js';
32
+
33
+ import { constraintCheckSchema } from './_schema.js';
34
+ import { checkConstraint } from './registry.js';
35
+
36
+ /**
37
+ * REGEL-3 Output-Boundary-Gate.
38
+ *
39
+ * Aufruf delegiert an checkConstraint, validiert das Resultat gegen
40
+ * constraintCheckSchema und wirft bei Verletzung einen Error mit
41
+ * Constraint-Typ + Detail. Bei `pass === null` (Unbekannter Constraint)
42
+ * wird das Resultat ohne Validation zurueckgegeben — `null` ist eine
43
+ * gueltige "kein Verdict moeglich"-Antwort und faellt aus REGEL-3 raus.
44
+ *
45
+ * @param {string} type Constraint-Typ (z.B. 'CENTERED-IN', 'DISTANCE-FROM')
46
+ * @param {object} subj Subject-Element aus gridMap
47
+ * @param {object|null} ref Reference-Element aus gridMap
48
+ * @param {object} ctx Kontext { grid, value? }
49
+ * @returns {{ pass: boolean|null, detail: string|null, dx?: number, dy?: number, dw?: number, dh?: number }}
50
+ * @throws Error wenn Constraint REGEL-3 verletzt (fail ohne dx/dy)
51
+ */
52
+ export function validatedCheckConstraint(type, subj, ref, ctx) {
53
+ const result = checkConstraint(type, subj, ref, ctx);
54
+ // pass === null → "Unbekannter Constraint" (registry.js Z.29).
55
+ // Das ist KEIN REGEL-3-Bruch, sondern eine Lookup-Failure — durchreichen.
56
+ if (result.pass === null) return result;
57
+ const parsed = constraintCheckSchema.safeParse(result);
58
+ if (!parsed.success) {
59
+ const reason = parsed.error.issues.map((i) => i.message).join('; ');
60
+ throw new Error(
61
+ `[REGEL-3] Constraint '${type}' verletzt Output-Boundary-Vertrag: ${reason}`,
62
+ );
63
+ }
64
+ return result;
65
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * no-overlap.js - NO-OVERLAP Constraint
3
+ * Migrated from mirror.js:42-52
4
+ * Vector Mirror v2.0
5
+ */
6
+
7
+ import { bboxOverlap } from '../../lib/bbox.js';
8
+ import { registerConstraint } from './registry.js';
9
+
10
+ registerConstraint('NO-OVERLAP', {
11
+ // Uniform dispatch signature (registry.js): check(subj, ref, ctx).
12
+ check(subj, ref, _ctx) {
13
+ const overlaps = bboxOverlap(subj.bbox, ref.bbox);
14
+ if (!overlaps) return { pass: true, detail: null };
15
+ const dxL = ref.bbox.x + ref.bbox.w - subj.bbox.x;
16
+ const dxR = ref.bbox.x - (subj.bbox.x + subj.bbox.w);
17
+ const dyT = ref.bbox.y + ref.bbox.h - subj.bbox.y;
18
+ const dyB = ref.bbox.y - (subj.bbox.y + subj.bbox.h);
19
+ const moves = [
20
+ { v: dxL, type: 'dx', abs: Math.abs(dxL) },
21
+ { v: dxR, type: 'dx', abs: Math.abs(dxR) },
22
+ { v: dyT, type: 'dy', abs: Math.abs(dyT) },
23
+ { v: dyB, type: 'dy', abs: Math.abs(dyB) },
24
+ ];
25
+ const best = moves.reduce((a, b) => (a.abs < b.abs ? a : b));
26
+ const result = {
27
+ pass: false,
28
+ detail: `Überlappt #${ref.id}. Kürzester Fluchtweg: ${best.type}=${Math.round(best.v)}px`,
29
+ };
30
+ result[best.type] = Math.round(best.v);
31
+ return result;
32
+ },
33
+ // Uniform dispatch signature (registry.js): arrange(subj, ref, ctx).
34
+ arrange(subj, ref, _ctx) {
35
+ if (!bboxOverlap(subj.bbox, ref.bbox))
36
+ return { x: subj.bbox.x, y: subj.bbox.y };
37
+ const dxL = ref.bbox.x + ref.bbox.w - subj.bbox.x;
38
+ const dxR = ref.bbox.x - (subj.bbox.x + subj.bbox.w);
39
+ const dyT = ref.bbox.y + ref.bbox.h - subj.bbox.y;
40
+ const dyB = ref.bbox.y - (subj.bbox.y + subj.bbox.h);
41
+ const moves = [
42
+ { dx: dxL, dy: 0, abs: Math.abs(dxL) },
43
+ { dx: dxR, dy: 0, abs: Math.abs(dxR) },
44
+ { dx: 0, dy: dyT, abs: Math.abs(dyT) },
45
+ { dx: 0, dy: dyB, abs: Math.abs(dyB) },
46
+ ];
47
+ const best = moves.reduce((a, b) => (a.abs < b.abs ? a : b));
48
+ return { x: subj.bbox.x + best.dx, y: subj.bbox.y + best.dy };
49
+ },
50
+ });
@@ -0,0 +1,46 @@
1
+ /**
2
+ * positional.js - LEFT-OF + ABOVE Constraints
3
+ * Migrated from mirror.js:80-93
4
+ * Vector Mirror v2.0
5
+ */
6
+ import { registerConstraint } from './registry.js';
7
+
8
+ registerConstraint('LEFT-OF', {
9
+ // Uniform dispatch signature (registry.js): check(subj, ref, ctx).
10
+ check(subj, ref, _ctx) {
11
+ const expectedMaxX = ref.bbox.x;
12
+ const actualMaxX = subj.bbox.x + subj.bbox.w;
13
+ const pass = actualMaxX <= expectedMaxX;
14
+ const dx = actualMaxX - expectedMaxX;
15
+ if (pass) return { pass, detail: null };
16
+ return {
17
+ pass,
18
+ detail: `Nicht links davon. Korrektur: dx=${Math.round(-dx)}px`,
19
+ dx: Math.round(-dx),
20
+ };
21
+ },
22
+ // Uniform dispatch signature (registry.js): arrange(subj, ref, ctx).
23
+ arrange(subj, ref, _ctx) {
24
+ return { x: ref.bbox.x - subj.bbox.w };
25
+ },
26
+ });
27
+
28
+ registerConstraint('ABOVE', {
29
+ // Uniform dispatch signature (registry.js): check(subj, ref, ctx).
30
+ check(subj, ref, _ctx) {
31
+ const expectedMaxY = ref.bbox.y;
32
+ const actualMaxY = subj.bbox.y + subj.bbox.h;
33
+ const pass = actualMaxY <= expectedMaxY;
34
+ const dy = actualMaxY - expectedMaxY;
35
+ if (pass) return { pass, detail: null };
36
+ return {
37
+ pass,
38
+ detail: `Nicht oberhalb. Korrektur: dy=${Math.round(-dy)}px`,
39
+ dy: Math.round(-dy),
40
+ };
41
+ },
42
+ // Uniform dispatch signature (registry.js): arrange(subj, ref, ctx).
43
+ arrange(subj, ref, _ctx) {
44
+ return { y: ref.bbox.y - subj.bbox.h };
45
+ },
46
+ });
@@ -0,0 +1,98 @@
1
+ /**
2
+ * registry.js - Constraint Registry (Plug-In Pattern)
3
+ * ADR-020: Map<type, {check, arrange}>
4
+ * ADR-022: Context-Object Pattern (ctx = { grid, value? })
5
+ * Vector Mirror v2.0
6
+ */
7
+
8
+ const handlers = new Map();
9
+
10
+ /**
11
+ * Registers a constraint handler.
12
+ *
13
+ * `requiresReference` (default true) markiert, ob `check()`/`arrange()` ein
14
+ * Referenz-Element (`ref`) DEREFERENZIEREN — z.B. `ref.bbox`. Der Default ist
15
+ * fail-closed (true): ein neu registrierter, unmarkierter Constraint gilt als
16
+ * referenz-pflichtig, damit eine fehlende Referenz NIE still in einen
17
+ * null-Deref-Crash laeuft (REGEL-8). Nur Constraints, die ohne Referenz
18
+ * arbeiten (COLOR, FILL), setzen `requiresReference: false` explizit.
19
+ *
20
+ * @param {string} type - Constraint type (e.g. 'CENTERED-IN')
21
+ * @param {{ check: Function, arrange?: Function, requiresReference?: boolean }} handler
22
+ */
23
+ export function registerConstraint(
24
+ type,
25
+ { check, arrange, requiresReference },
26
+ ) {
27
+ handlers.set(type, {
28
+ check,
29
+ arrange,
30
+ requiresReference: requiresReference !== false,
31
+ });
32
+ }
33
+
34
+ /**
35
+ * Ob ein Constraint-Typ ueberhaupt registriert (bekannt) ist.
36
+ *
37
+ * Trennt die zwei Diagnosen sauber: ein UNBEKANNTER Typ (Tippfehler/Phantasie)
38
+ * darf NICHT vom ref-Guard maskiert werden — er fliesst weiter zu
39
+ * checkConstraint → CONSTRAINT_TYPE_UNKNOWN (+ Levenshtein-Vorschlag). Nur fuer
40
+ * BEKANNTE, ref-pflichtige Typen ohne ref greift der fail-closed REFERENCE_NOT_FOUND.
41
+ *
42
+ * @param {string} type
43
+ * @returns {boolean}
44
+ */
45
+ export function isRegistered(type) {
46
+ return handlers.has(type);
47
+ }
48
+
49
+ /**
50
+ * Ob ein Constraint-Typ ein Referenz-Element braucht (siehe registerConstraint).
51
+ * Fail-closed: unbekannte Typen gelten als referenz-pflichtig (true), damit ein
52
+ * fehlender ref nie in einen Deref-Crash faellt.
53
+ *
54
+ * @param {string} type
55
+ * @returns {boolean}
56
+ */
57
+ export function requiresReference(type) {
58
+ const h = handlers.get(type);
59
+ return h ? h.requiresReference : true;
60
+ }
61
+
62
+ /**
63
+ * Checks a single constraint.
64
+ * @param {string} type
65
+ * @param {object} subj - Subject element from gridMap
66
+ * @param {object|null} ref - Reference element from gridMap
67
+ * @param {object} ctx - { grid, value? }
68
+ * @returns {{ pass: boolean|null, detail?: string, dx?: number, dy?: number }}
69
+ */
70
+ export function checkConstraint(type, subj, ref, ctx) {
71
+ const h = handlers.get(type);
72
+ if (!h) return { pass: null, detail: `Unbekannter Constraint: ${type}` };
73
+ return h.check(subj, ref, ctx);
74
+ }
75
+
76
+ /**
77
+ * Arranges a single constraint (inverse of check).
78
+ * @param {string} type
79
+ * @param {object} subj - Subject element { bbox }
80
+ * @param {object|null} ref - Reference element { bbox }
81
+ * @param {object} ctx - { canvas, value? }
82
+ * @returns {object|null} Attribute patch or null if no arrange handler
83
+ */
84
+ export function arrangeConstraint(type, subj, ref, ctx) {
85
+ const h = handlers.get(type);
86
+ if (!h?.arrange) return null;
87
+ return h.arrange(subj, ref, ctx);
88
+ }
89
+
90
+ /**
91
+ * Lists all registered constraint types.
92
+ */
93
+ export function listConstraints() {
94
+ return [...handlers.entries()].map(([type, h]) => ({
95
+ type,
96
+ hasArrange: !!h.arrange,
97
+ }));
98
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * same-size.js - SAME-SIZE Constraint
3
+ * Migrated from mirror.js:73-79
4
+ * Vector Mirror v2.0
5
+ */
6
+
7
+ import { getTolX, getTolY } from '../tolerance.js';
8
+ import { registerConstraint } from './registry.js';
9
+
10
+ registerConstraint('SAME-SIZE', {
11
+ check(subj, ref, { grid }) {
12
+ if (ref.bbox.w === 0 || ref.bbox.h === 0)
13
+ return { pass: null, detail: 'Referenz hat Grösse 0' };
14
+ const dw = subj.bbox.w - ref.bbox.w;
15
+ const dh = subj.bbox.h - ref.bbox.h;
16
+ const pass =
17
+ Math.abs(dw) <= getTolX(ref.bbox.w, grid) &&
18
+ Math.abs(dh) <= getTolY(ref.bbox.h, grid);
19
+ if (pass) return { pass, detail: null };
20
+ // G2 (D-006): detail beschreibt NUR das WAS/WIEVIEL-daneben. Die
21
+ // strukturierte Korrektur lebt ausschliesslich in dw/dh — prose.js/
22
+ // structured.js bauen den Hinweis aus diesen Feldern, nicht mehr aus
23
+ // dem detail-String (kein String-Leak, gate-bar in S8).
24
+ return {
25
+ pass,
26
+ detail: `Grösse weicht ab (Δw=${Math.round(dw)}px, Δh=${Math.round(dh)}px)`,
27
+ dw: Math.round(-dw),
28
+ dh: Math.round(-dh),
29
+ };
30
+ },
31
+ // Uniform dispatch signature (registry.js): arrange(subj, ref, ctx).
32
+ arrange(_subj, ref, _ctx) {
33
+ return { width: ref.bbox.w, height: ref.bbox.h };
34
+ },
35
+ });