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,266 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* arbitrate.js — Tri-State Validator (P1-04, VM-LC-001)
|
|
3
|
+
* Vector Mirror v2.5
|
|
4
|
+
*
|
|
5
|
+
* Splits constraint results into three buckets — `passing`, `failing`,
|
|
6
|
+
* `unchecked` — and passes the scene `diff` through severity-sorted.
|
|
7
|
+
* No silent cap: max=3 capping has moved to the presentation layer
|
|
8
|
+
* (`adapters/emitter/prose.js`); structured output ships the full set.
|
|
9
|
+
*
|
|
10
|
+
* Tri-state semantics:
|
|
11
|
+
* pass === true → passing[] (constraint evaluated, satisfied)
|
|
12
|
+
* pass === false → failing[] (constraint evaluated, violated, with deltas)
|
|
13
|
+
* pass === null → unchecked[] (could not evaluate, with reasonCode)
|
|
14
|
+
*
|
|
15
|
+
* Each `unchecked` entry carries a structured reason so the LLM (or a
|
|
16
|
+
* downstream emitter) can act on it instead of seeing a phantom "PASS":
|
|
17
|
+
* - reasonCategory: "SPECIFICATION" | "MODEL"
|
|
18
|
+
* - reasonCode: enum (see REASON_CODES)
|
|
19
|
+
* - hint: human-readable line
|
|
20
|
+
* - suggestedCorrection?: string (only when we can offer one)
|
|
21
|
+
*
|
|
22
|
+
* Source-of-truth references:
|
|
23
|
+
* - FIX_PLAN_2026-04-18 §1.2 P1-04
|
|
24
|
+
* - KATALOG VM-LC-001 (Silent-PASS bei pass:null)
|
|
25
|
+
* - KATALOG VM-LC-003 (max=3-Kappung verletzt ADR-024)
|
|
26
|
+
* - RB-01 §6.1, RB-03 §6.1
|
|
27
|
+
*
|
|
28
|
+
* Core module: NO imports from adapters/ or interface/.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Severity ordering for diff entries (lower = more important).
|
|
33
|
+
* `failing` issues are pinned at severity 0; diff types follow.
|
|
34
|
+
*/
|
|
35
|
+
const DIFF_PRIO = {
|
|
36
|
+
VERSCHOBEN: 1,
|
|
37
|
+
FARBÄNDERUNG: 2,
|
|
38
|
+
NEU: 3,
|
|
39
|
+
ENTFERNT: 4,
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Allowed reasonCode values for unchecked entries.
|
|
44
|
+
* @type {readonly string[]}
|
|
45
|
+
*/
|
|
46
|
+
export const REASON_CODES = Object.freeze([
|
|
47
|
+
'CONSTRAINT_TYPE_UNKNOWN', // unbekannter / falsch geschriebener Typ
|
|
48
|
+
'SUBJECT_NOT_FOUND', // subjekt-id existiert nicht im scene
|
|
49
|
+
'REFERENCE_NOT_FOUND', // referenz-id existiert nicht im scene
|
|
50
|
+
'REFERENCE_DEGENERATE', // referenz-bbox 0×0 oder ungueltig
|
|
51
|
+
'MEASUREMENT_AMBIGUOUS', // kein klares messverfahren (z.b. fill auf <g>)
|
|
52
|
+
'INVALID_MEASUREMENT', // mess-vorgabe selbst ungueltig (z.b. negativer ziel-abstand) (D-003)
|
|
53
|
+
'SEMANTIC_SUSPICIOUS', // syntaktisch ok, aber semantisch verdaechtig
|
|
54
|
+
'SCOPE_MISMATCH', // constraint passt nicht zu element-typ
|
|
55
|
+
// §HEAL-5 Verdikt-Wache (pipeline.js#classifySubjectHonesty, F-AT-2-005):
|
|
56
|
+
'SUBJECT_NOT_PAINTED', // subjekt malt 0 pixel (paint_visible:false) — pass:true degradiert
|
|
57
|
+
'SUBJECT_TIME_VARIANT', // subjekt-geometrie zeit-variant (motion_dependent) — geprueft @t0
|
|
58
|
+
// §H10 R11-07 dritte Wache-Klasse (pipeline.js#classifySubjectHonesty):
|
|
59
|
+
'SUBJECT_NOT_MEASURABLE', // subjekt-bbox not_measurable (3d/non-smil-motion) — kein gruenes verdikt ueber misstrauter zahl
|
|
60
|
+
// §H10 R11-21 Parse-Verweigerung (pipeline.js#parseConstraints/checkAllConstraints):
|
|
61
|
+
'CONSTRAINT_UNPARSEABLE', // constraint-string nicht (vollstaendig) gegen die grammatik parsebar — verweigert statt interpretiert
|
|
62
|
+
// §H10 R11-01 Existenz-Register (pipeline.js#checkAllConstraints):
|
|
63
|
+
'SUBJECT_HIDDEN', // subjekt-id existiert im markup, ist aber css-unsichtbar @t0 — nicht gemessen
|
|
64
|
+
'REFERENCE_HIDDEN', // referenz-id existiert im markup, ist aber css-unsichtbar @t0 — symmetrie zu SUBJECT_HIDDEN (P1)
|
|
65
|
+
]);
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Constraint types currently registered in `core/constraints/loader.js`.
|
|
69
|
+
* Used as the dictionary for typo-suggestions in unchecked entries.
|
|
70
|
+
* Mirrors ADR-029 Tier-A (11 types). When loader.js gains new types,
|
|
71
|
+
* extend this list.
|
|
72
|
+
*/
|
|
73
|
+
const KNOWN_CONSTRAINT_TYPES = Object.freeze([
|
|
74
|
+
'ALIGNED-LEFT',
|
|
75
|
+
'ALIGNED-TOP',
|
|
76
|
+
'CENTERED-IN',
|
|
77
|
+
'COLOR',
|
|
78
|
+
'DISTANCE-FROM',
|
|
79
|
+
'FILL',
|
|
80
|
+
'INSIDE',
|
|
81
|
+
'NO-OVERLAP',
|
|
82
|
+
'LEFT-OF',
|
|
83
|
+
'ABOVE',
|
|
84
|
+
'SAME-SIZE',
|
|
85
|
+
]);
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Levenshtein edit distance — small helper, used for typo suggestions.
|
|
89
|
+
* O(m·n) time, O(min(m,n)) space.
|
|
90
|
+
*
|
|
91
|
+
* @param {string} a
|
|
92
|
+
* @param {string} b
|
|
93
|
+
* @returns {number}
|
|
94
|
+
*/
|
|
95
|
+
function levenshtein(a, b) {
|
|
96
|
+
if (a === b) return 0;
|
|
97
|
+
if (!a) return b.length;
|
|
98
|
+
if (!b) return a.length;
|
|
99
|
+
let prev = new Array(b.length + 1);
|
|
100
|
+
let curr = new Array(b.length + 1);
|
|
101
|
+
for (let j = 0; j <= b.length; j++) prev[j] = j;
|
|
102
|
+
for (let i = 1; i <= a.length; i++) {
|
|
103
|
+
curr[0] = i;
|
|
104
|
+
for (let j = 1; j <= b.length; j++) {
|
|
105
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
106
|
+
curr[j] = Math.min(prev[j] + 1, curr[j - 1] + 1, prev[j - 1] + cost);
|
|
107
|
+
}
|
|
108
|
+
[prev, curr] = [curr, prev];
|
|
109
|
+
}
|
|
110
|
+
return prev[b.length];
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Returns the closest known constraint type within Levenshtein ≤ 2,
|
|
115
|
+
* or `undefined` if no candidate is close enough.
|
|
116
|
+
*
|
|
117
|
+
* @param {string} type
|
|
118
|
+
* @returns {string|undefined}
|
|
119
|
+
*/
|
|
120
|
+
function closestKnownType(type) {
|
|
121
|
+
if (!type) return undefined;
|
|
122
|
+
let best;
|
|
123
|
+
let bestDist = 3; // strictly less than this wins; ≤2 by FIX_PLAN
|
|
124
|
+
for (const known of KNOWN_CONSTRAINT_TYPES) {
|
|
125
|
+
const d = levenshtein(type.toUpperCase(), known);
|
|
126
|
+
if (d < bestDist) {
|
|
127
|
+
bestDist = d;
|
|
128
|
+
best = known;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return best;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Builds an `unchecked` entry. If the constraint result already carries
|
|
136
|
+
* a reasonCode (provided by registry / P1-08), passes it through; otherwise
|
|
137
|
+
* applies a heuristic fallback so existing producers keep working.
|
|
138
|
+
*
|
|
139
|
+
* @param {object} cr raw constraint result (`pass: null` shape)
|
|
140
|
+
* @returns {{
|
|
141
|
+
* id?: string,
|
|
142
|
+
* constraintType?: string,
|
|
143
|
+
* reasonCategory: 'SPECIFICATION'|'MODEL',
|
|
144
|
+
* reasonCode: string,
|
|
145
|
+
* hint: string,
|
|
146
|
+
* suggestedCorrection?: string,
|
|
147
|
+
* detail?: string
|
|
148
|
+
* }}
|
|
149
|
+
*/
|
|
150
|
+
function buildUnchecked(cr) {
|
|
151
|
+
const entry = {};
|
|
152
|
+
if (cr.id !== undefined) entry.id = cr.id;
|
|
153
|
+
if (cr.constraintType !== undefined) entry.constraintType = cr.constraintType;
|
|
154
|
+
if (cr.detail !== undefined) entry.detail = cr.detail;
|
|
155
|
+
|
|
156
|
+
// Honor upstream reasonCode (registry / P1-08) if present and valid.
|
|
157
|
+
if (cr.reasonCode && REASON_CODES.includes(cr.reasonCode)) {
|
|
158
|
+
entry.reasonCategory =
|
|
159
|
+
cr.reasonCategory === 'MODEL' ? 'MODEL' : 'SPECIFICATION';
|
|
160
|
+
entry.reasonCode = cr.reasonCode;
|
|
161
|
+
entry.hint = cr.hint || cr.detail || 'Constraint nicht ausgewertet.';
|
|
162
|
+
if (cr.suggestedCorrection)
|
|
163
|
+
entry.suggestedCorrection = cr.suggestedCorrection;
|
|
164
|
+
return entry;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Fallback heuristic: attempt typo-detection on constraintType.
|
|
168
|
+
if (
|
|
169
|
+
cr.constraintType &&
|
|
170
|
+
!KNOWN_CONSTRAINT_TYPES.includes(cr.constraintType)
|
|
171
|
+
) {
|
|
172
|
+
const suggestion = closestKnownType(cr.constraintType);
|
|
173
|
+
entry.reasonCategory = 'SPECIFICATION';
|
|
174
|
+
entry.reasonCode = 'CONSTRAINT_TYPE_UNKNOWN';
|
|
175
|
+
entry.hint = `Constraint-Typ '${cr.constraintType}' ist nicht registriert.`;
|
|
176
|
+
if (suggestion) entry.suggestedCorrection = suggestion;
|
|
177
|
+
return entry;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Generic catch-all: known type but check returned pass:null.
|
|
181
|
+
entry.reasonCategory = 'MODEL';
|
|
182
|
+
entry.reasonCode = 'MEASUREMENT_AMBIGUOUS';
|
|
183
|
+
entry.hint = cr.detail || 'Constraint-Auswertung nicht moeglich.';
|
|
184
|
+
return entry;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Tri-state validation pass over constraint results + scene diff.
|
|
189
|
+
*
|
|
190
|
+
* @param {Array<{pass: boolean|null, [k:string]: any}>} constraintResults
|
|
191
|
+
* @param {Array<{type: string, [k:string]: any}>} diff
|
|
192
|
+
* @returns {{
|
|
193
|
+
* passing: Array<{id?: string, constraintType?: string}>,
|
|
194
|
+
* failing: Array<{type: 'CONSTRAINT_FAIL', severity: 0, [k:string]: any}>,
|
|
195
|
+
* unchecked: Array<{reasonCategory: string, reasonCode: string, hint: string, [k:string]: any}>,
|
|
196
|
+
* diff: Array<{type: string, severity: number, [k:string]: any}>,
|
|
197
|
+
* totals: {
|
|
198
|
+
* total: number,
|
|
199
|
+
* passing_count: number,
|
|
200
|
+
* failing_count: number,
|
|
201
|
+
* unchecked_count: number,
|
|
202
|
+
* diff_count: number
|
|
203
|
+
* }
|
|
204
|
+
* }}
|
|
205
|
+
*/
|
|
206
|
+
/**
|
|
207
|
+
* Copies any defined `keys` from `src` onto `dst`. Helper to keep
|
|
208
|
+
* arbitrate() free of long if-chains.
|
|
209
|
+
*/
|
|
210
|
+
function copyDefined(src, dst, keys) {
|
|
211
|
+
for (const k of keys) if (src[k] !== undefined) dst[k] = src[k];
|
|
212
|
+
return dst;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const PASSING_KEYS = ['id', 'constraintType'];
|
|
216
|
+
const FAILING_KEYS = [
|
|
217
|
+
'detail',
|
|
218
|
+
'id',
|
|
219
|
+
'constraintType',
|
|
220
|
+
'reference',
|
|
221
|
+
'dx',
|
|
222
|
+
'dy',
|
|
223
|
+
'dw',
|
|
224
|
+
'dh',
|
|
225
|
+
];
|
|
226
|
+
|
|
227
|
+
export function arbitrate(constraintResults, diff) {
|
|
228
|
+
const passing = [];
|
|
229
|
+
const failing = [];
|
|
230
|
+
const unchecked = [];
|
|
231
|
+
|
|
232
|
+
for (const cr of constraintResults) {
|
|
233
|
+
if (cr.pass === true) {
|
|
234
|
+
passing.push(copyDefined(cr, {}, PASSING_KEYS));
|
|
235
|
+
} else if (cr.pass === false) {
|
|
236
|
+
failing.push(
|
|
237
|
+
copyDefined(cr, { type: 'CONSTRAINT_FAIL', severity: 0 }, FAILING_KEYS),
|
|
238
|
+
);
|
|
239
|
+
} else {
|
|
240
|
+
unchecked.push(buildUnchecked(cr));
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const diffSorted = diff
|
|
245
|
+
.map((d) => ({ ...d, severity: DIFF_PRIO[d.type] ?? 5 }))
|
|
246
|
+
.sort((a, b) => a.severity - b.severity);
|
|
247
|
+
|
|
248
|
+
const passing_count = passing.length;
|
|
249
|
+
const failing_count = failing.length;
|
|
250
|
+
const unchecked_count = unchecked.length;
|
|
251
|
+
const diff_count = diffSorted.length;
|
|
252
|
+
|
|
253
|
+
return {
|
|
254
|
+
passing,
|
|
255
|
+
failing,
|
|
256
|
+
unchecked,
|
|
257
|
+
diff: diffSorted,
|
|
258
|
+
totals: {
|
|
259
|
+
total: failing_count + unchecked_count + diff_count,
|
|
260
|
+
passing_count,
|
|
261
|
+
failing_count,
|
|
262
|
+
unchecked_count,
|
|
263
|
+
diff_count,
|
|
264
|
+
},
|
|
265
|
+
};
|
|
266
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* _schema.js — Constraint Output Contract (REGEL-3 Caller-Boundary Gate)
|
|
3
|
+
*
|
|
4
|
+
*
|
|
5
|
+
* Zweck:
|
|
6
|
+
* REGEL-3 "Spotter-Anti-Luege" (CONSTANTS.md Z.116) maschinen-mässig
|
|
7
|
+
* erzwingen. Jeder Constraint, dessen check() ein fail-Verdict liefert,
|
|
8
|
+
* MUSS strukturierte Pixel-Korrekturen (dx oder dy) zurueckgeben — nicht
|
|
9
|
+
* nur Prosa. Bis Sprint-β3 war REGEL-3 textuelle Konvention; jetzt ist
|
|
10
|
+
* sie via Zod-Schema kodiert und durch loader.validatedCheckConstraint
|
|
11
|
+
* testbar.
|
|
12
|
+
*
|
|
13
|
+
* Vertrag:
|
|
14
|
+
* - pass=true → keine Pixel-Korrektur erwartet (detail darf null sein)
|
|
15
|
+
* - pass=false → MUSS mindestens dx ODER dy enthalten (oder beide)
|
|
16
|
+
*
|
|
17
|
+
* REGEL-4 Hexagonal:
|
|
18
|
+
* - Modul ist Layer "core/" — darf NUR aus core/ oder Pure-Libraries
|
|
19
|
+
* importieren. Hier: ausschliesslich `zod` (Pure-Library, transport-
|
|
20
|
+
* agnostisch). KEIN Import aus adapters/ oder interface/.
|
|
21
|
+
*
|
|
22
|
+
* Adressiert Audit-Welle-β:
|
|
23
|
+
* WELLE-β-004 (DISTANCE-FROM ohne dx/dy — REGEL-3-Asymmetrie zu
|
|
24
|
+
* kanonischen Constraints CENTERED-IN/ALIGNED-LEFT).
|
|
25
|
+
*
|
|
26
|
+
* DEPENDS: zod
|
|
27
|
+
*/
|
|
28
|
+
import { z } from 'zod';
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* constraintCheckSchema — Zod-Output-Vertrag fuer alle Constraint .check()-Resultate.
|
|
32
|
+
*
|
|
33
|
+
* Aufbau:
|
|
34
|
+
* - Basis (z.object): pass:boolean, detail:string|null.
|
|
35
|
+
* - .and(z.union(...)): diskriminiert nach pass-Wert.
|
|
36
|
+
* Branch 1 (pass=true): KEINE Pflicht zu dx/dy.
|
|
37
|
+
* Branch 2 (pass=false): refine erzwingt dx ODER dy (optional beide,
|
|
38
|
+
* aber mindestens eins).
|
|
39
|
+
*
|
|
40
|
+
* REGEL-3-Pflicht bei pass=false (TYP-EHRLICH, maintainer decision S2b
|
|
41
|
+
* Step-0, D-007):
|
|
42
|
+
* - spatial-Constraints (CENTERED-IN, ALIGNED-*, DISTANCE-FROM, …):
|
|
43
|
+
* mindestens `dx` ODER `dy`.
|
|
44
|
+
* - Resize-Constraints (SAME-SIZE): erfuellen REGEL-3 mit `dw` ODER `dh`
|
|
45
|
+
* (Groessen-Delta ist ihre kanonische strukturierte Korrektur).
|
|
46
|
+
* - non-spatial-Constraints (COLOR): haben KEINEN Pixel-Delta. Sie tragen
|
|
47
|
+
* einen expliziten `non_spatial: true`-Marker und sind damit von der
|
|
48
|
+
* Delta-Pflicht ausgenommen. Der Marker ist self-describing (lokal vom
|
|
49
|
+
* Constraint deklariert, kein zentraler Typ-Katalog im Schema → keine
|
|
50
|
+
* Drift). OHNE Marker bleibt die Delta-Pflicht streng: ein spatial/resize-
|
|
51
|
+
* Constraint, der weder Delta noch Marker liefert, wirft weiterhin
|
|
52
|
+
* (keine Prosa-only-Antwort fuer mess-bare Constraints).
|
|
53
|
+
*/
|
|
54
|
+
export const constraintCheckSchema = z
|
|
55
|
+
.object({
|
|
56
|
+
pass: z.boolean(),
|
|
57
|
+
detail: z.string().nullable(),
|
|
58
|
+
})
|
|
59
|
+
.and(
|
|
60
|
+
z.union([
|
|
61
|
+
// pass=true: keine Korrektur-Daten erwartet
|
|
62
|
+
z.object({ pass: z.literal(true) }),
|
|
63
|
+
// pass=false: REGEL-3 verlangt strukturierte Korrektur — (dx|dy) ODER
|
|
64
|
+
// (dw|dh) — ausser der Constraint markiert sich als non_spatial.
|
|
65
|
+
z
|
|
66
|
+
.object({
|
|
67
|
+
pass: z.literal(false),
|
|
68
|
+
dx: z.number().optional(),
|
|
69
|
+
dy: z.number().optional(),
|
|
70
|
+
dw: z.number().optional(),
|
|
71
|
+
dh: z.number().optional(),
|
|
72
|
+
// non-spatial-Marker (COLOR): nimmt den Constraint von der
|
|
73
|
+
// Pixel-Delta-Pflicht aus. Muss explizit `true` sein.
|
|
74
|
+
non_spatial: z.literal(true).optional(),
|
|
75
|
+
})
|
|
76
|
+
.refine(
|
|
77
|
+
(v) =>
|
|
78
|
+
v.non_spatial === true ||
|
|
79
|
+
v.dx !== undefined ||
|
|
80
|
+
v.dy !== undefined ||
|
|
81
|
+
v.dw !== undefined ||
|
|
82
|
+
v.dh !== undefined,
|
|
83
|
+
{
|
|
84
|
+
message:
|
|
85
|
+
'REGEL-3: fail-Verdict MUSS strukturierte Korrektur liefern — dx/dy (spatial) ODER dw/dh (resize) ODER non_spatial:true (z.B. COLOR). Keine Prosa-only-Antwort fuer mess-bare Constraints.',
|
|
86
|
+
},
|
|
87
|
+
),
|
|
88
|
+
]),
|
|
89
|
+
);
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* aligned.js - ALIGNED-LEFT + ALIGNED-TOP Constraints
|
|
3
|
+
* Migrated from mirror.js:63-71
|
|
4
|
+
* Vector Mirror v2.0
|
|
5
|
+
*/
|
|
6
|
+
import { registerConstraint } from './registry.js';
|
|
7
|
+
|
|
8
|
+
registerConstraint('ALIGNED-LEFT', {
|
|
9
|
+
// Uniform dispatch signature (registry.js): check(subj, ref, ctx).
|
|
10
|
+
check(subj, ref, _ctx) {
|
|
11
|
+
const dx = subj.bbox.x - ref.bbox.x;
|
|
12
|
+
const pass = Math.abs(dx) <= 2; // Hard 2px spirit level
|
|
13
|
+
if (pass) return { pass, detail: null };
|
|
14
|
+
return {
|
|
15
|
+
pass,
|
|
16
|
+
detail: `Nicht bündig. Korrektur: dx=${Math.round(-dx)}px`,
|
|
17
|
+
dx: Math.round(-dx),
|
|
18
|
+
};
|
|
19
|
+
},
|
|
20
|
+
// Uniform dispatch signature (registry.js): arrange(subj, ref, ctx).
|
|
21
|
+
arrange(_subj, ref, _ctx) {
|
|
22
|
+
return { x: ref.bbox.x };
|
|
23
|
+
},
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
registerConstraint('ALIGNED-TOP', {
|
|
27
|
+
// Uniform dispatch signature (registry.js): check(subj, ref, ctx).
|
|
28
|
+
check(subj, ref, _ctx) {
|
|
29
|
+
const dy = subj.bbox.y - ref.bbox.y;
|
|
30
|
+
const pass = Math.abs(dy) <= 2;
|
|
31
|
+
if (pass) return { pass, detail: null };
|
|
32
|
+
return {
|
|
33
|
+
pass,
|
|
34
|
+
detail: `Nicht bündig. Korrektur: dy=${Math.round(-dy)}px`,
|
|
35
|
+
dy: Math.round(-dy),
|
|
36
|
+
};
|
|
37
|
+
},
|
|
38
|
+
// Uniform dispatch signature (registry.js): arrange(subj, ref, ctx).
|
|
39
|
+
arrange(_subj, ref, _ctx) {
|
|
40
|
+
return { y: ref.bbox.y };
|
|
41
|
+
},
|
|
42
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* centered-in.js - CENTERED-IN Constraint
|
|
3
|
+
* Migrated from mirror.js:36-41
|
|
4
|
+
* Vector Mirror v2.0
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { getTolX, getTolY } from '../tolerance.js';
|
|
8
|
+
import { registerConstraint } from './registry.js';
|
|
9
|
+
|
|
10
|
+
registerConstraint('CENTERED-IN', {
|
|
11
|
+
check(subj, ref, { grid }) {
|
|
12
|
+
const dx = subj.bbox.x + subj.bbox.w / 2 - (ref.bbox.x + ref.bbox.w / 2);
|
|
13
|
+
const dy = subj.bbox.y + subj.bbox.h / 2 - (ref.bbox.y + ref.bbox.h / 2);
|
|
14
|
+
const pass =
|
|
15
|
+
Math.abs(dx) <= getTolX(ref.bbox.w, grid) &&
|
|
16
|
+
Math.abs(dy) <= getTolY(ref.bbox.h, grid);
|
|
17
|
+
if (pass) return { pass, detail: null };
|
|
18
|
+
return {
|
|
19
|
+
pass,
|
|
20
|
+
detail: `Verfehlt Zentrum. Korrektur: dx=${Math.round(-dx)}px, dy=${Math.round(-dy)}px`,
|
|
21
|
+
dx: Math.round(-dx),
|
|
22
|
+
dy: Math.round(-dy),
|
|
23
|
+
};
|
|
24
|
+
},
|
|
25
|
+
// Uniform dispatch signature (registry.js): arrange(subj, ref, ctx).
|
|
26
|
+
arrange(_subj, ref, _ctx) {
|
|
27
|
+
return { cx: ref.bbox.x + ref.bbox.w / 2, cy: ref.bbox.y + ref.bbox.h / 2 };
|
|
28
|
+
},
|
|
29
|
+
});
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* color.js - COLOR Constraint
|
|
3
|
+
* Migrated from mirror.js:104-107
|
|
4
|
+
* Vector Mirror v2.0
|
|
5
|
+
* Uses ctx.value (target color name)
|
|
6
|
+
*/
|
|
7
|
+
import { registerConstraint } from './registry.js';
|
|
8
|
+
import { parseColor } from '../../lib/palette.js';
|
|
9
|
+
|
|
10
|
+
registerConstraint('COLOR', {
|
|
11
|
+
// COLOR braucht keine Referenz (non-spatial, Ziel-Farbe steckt in ctx.value).
|
|
12
|
+
// Ohne diesen Marker wuerde die pipeline-Wache (fail-closed-Default) COLOR
|
|
13
|
+
// faelschlich als referenz-pflichtig behandeln.
|
|
14
|
+
requiresReference: false,
|
|
15
|
+
// Uniform dispatch signature (registry.js): check(subj, ref, ctx).
|
|
16
|
+
check(subj, _ref, { value }) {
|
|
17
|
+
// COLOR ist non-spatial (D-007): kein Pixel-Delta. Der non_spatial-Marker
|
|
18
|
+
// nimmt fail-Verdicts von der REGEL-3-Delta-Pflicht aus (_schema.js).
|
|
19
|
+
// Der spaetere COLOR-attribute_fix ist C-04/L-003-Arbeit, NICHT hier.
|
|
20
|
+
if (value === undefined || value === null) {
|
|
21
|
+
return {
|
|
22
|
+
pass: false,
|
|
23
|
+
detail: `Kein Farbwert angegeben fuer #${subj.id}.`,
|
|
24
|
+
non_spatial: true,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
// §HEAL3b: eine nicht messbare Farbe (use-Instanz → 'indeterminate', §HEAL3b
|
|
28
|
+
// im Renderer) kann nicht GEGEN ein Ziel geprüft werden. Ein pass:false meldete
|
|
29
|
+
// eine FALSCHE Verletzung und triggerte eine SCHÄDLICHE Korrektur auf eine
|
|
30
|
+
// womöglich schon korrekte Farbe (Blind-Trust-Gesetz). Unmessbar → UNCHECKED.
|
|
31
|
+
if (subj.color === 'indeterminate') {
|
|
32
|
+
return {
|
|
33
|
+
pass: null,
|
|
34
|
+
reasonCode: 'MEASUREMENT_AMBIGUOUS',
|
|
35
|
+
reasonCategory: 'MODEL',
|
|
36
|
+
detail: `Farbe von #${subj.id} ist nicht messbar (use-Instanz) — COLOR ${value} nicht prüfbar.`,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
// §H9 K-08a: subj.color ist IMMER der quantisierte Name (grid.js →
|
|
40
|
+
// parseColor/CIELAB). Der Soll-Token MUSS durch DENSELBEN Quantisierer,
|
|
41
|
+
// sonst vergleicht der Check zwei Repräsentationen derselben Farbe
|
|
42
|
+
// (#ff0000 vs red = False Negative). Unbekannte Token fallen in
|
|
43
|
+
// parseColor unverändert durch → ehrlicher FAIL bleibt.
|
|
44
|
+
//
|
|
45
|
+
// §H9 P3 MESS-GRANULARITÄT (ausgesprochen, bewusst): `COLOR <hex>`
|
|
46
|
+
// bedeutet nearest-named-color im Mess-Farbraum (140 W3C-Namen, CIELAB —
|
|
47
|
+
// parseColor/rgbToColorName). Zwei Hexes, die in DIESELBE Palette-Zelle
|
|
48
|
+
// quantisieren (z.B. #ff0000 und #ff0001 → beide 'red'), gelten als
|
|
49
|
+
// GLEICH — das ist die Granularität der Messung, kein Bug. Hexes mit
|
|
50
|
+
// VERSCHIEDENEM quantisierten Namen (#ff0000 'red' vs #ff6347 'tomato')
|
|
51
|
+
// bleiben verschieden → FAIL (Pin: tests/h9_red/test_h9_k08a.mjs).
|
|
52
|
+
// Feinere Unterscheidung (Hex-exakter Kanal) wäre eine Design-Runde,
|
|
53
|
+
// keine stille Verfeinerung hier.
|
|
54
|
+
const pass = subj.color === parseColor(value);
|
|
55
|
+
if (pass) return { pass, detail: null };
|
|
56
|
+
return {
|
|
57
|
+
pass,
|
|
58
|
+
detail: `Farbe ist ${subj.color}, soll ${value} sein.`,
|
|
59
|
+
non_spatial: true,
|
|
60
|
+
};
|
|
61
|
+
},
|
|
62
|
+
// No arrange — color cannot be spatially computed
|
|
63
|
+
});
|