@viewscript/renderer 0.1.0-202605140639

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.
Files changed (89) hide show
  1. package/dist/ast/types.d.ts +403 -0
  2. package/dist/ast/types.js +33 -0
  3. package/dist/compiler/chunk-splitter.d.ts +98 -0
  4. package/dist/compiler/chunk-splitter.js +361 -0
  5. package/dist/index.d.ts +55 -0
  6. package/dist/index.js +17 -0
  7. package/dist/rasterizer/__tests__/error-distribution.test.d.ts +7 -0
  8. package/dist/rasterizer/__tests__/error-distribution.test.js +322 -0
  9. package/dist/rasterizer/canvas-mapper.d.ts +280 -0
  10. package/dist/rasterizer/canvas-mapper.js +414 -0
  11. package/dist/rasterizer/error-distribution.d.ts +143 -0
  12. package/dist/rasterizer/error-distribution.js +231 -0
  13. package/dist/rasterizer/gradient-mapper.d.ts +223 -0
  14. package/dist/rasterizer/gradient-mapper.js +352 -0
  15. package/dist/rasterizer/topology-rounding.d.ts +151 -0
  16. package/dist/rasterizer/topology-rounding.js +347 -0
  17. package/dist/runtime/__tests__/event-backpressure.test.d.ts +10 -0
  18. package/dist/runtime/__tests__/event-backpressure.test.js +190 -0
  19. package/dist/runtime/event-backpressure.d.ts +393 -0
  20. package/dist/runtime/event-backpressure.js +458 -0
  21. package/dist/runtime/render-loop.d.ts +277 -0
  22. package/dist/runtime/render-loop.js +435 -0
  23. package/dist/runtime/wasm-resource-manager.d.ts +122 -0
  24. package/dist/runtime/wasm-resource-manager.js +253 -0
  25. package/dist/runtime/wgpu-renderer-adapter.d.ts +168 -0
  26. package/dist/runtime/wgpu-renderer-adapter.js +230 -0
  27. package/dist/semantic/__tests__/semantic-translator.test.d.ts +4 -0
  28. package/dist/semantic/__tests__/semantic-translator.test.js +203 -0
  29. package/dist/semantic/semantic-translator.d.ts +229 -0
  30. package/dist/semantic/semantic-translator.js +398 -0
  31. package/package.json +28 -0
  32. package/playwright-report/data/0bafe4e0863f0e244bba68a838f73241f8f2efaa.md +226 -0
  33. package/playwright-report/data/9281aca8abfb06c6cecb35d5ddd13d61f8c752d8.md +226 -0
  34. package/playwright-report/index.html +90 -0
  35. package/playwright.config.ts +160 -0
  36. package/screenshot-chrome.png +0 -0
  37. package/screenshots/visual-demo-verification.png +0 -0
  38. package/screenshots/visual-demo.png +0 -0
  39. package/src/ast/types.ts +473 -0
  40. package/src/compiler/chunk-splitter.ts +534 -0
  41. package/src/index.ts +62 -0
  42. package/src/rasterizer/__tests__/error-distribution.test.ts +382 -0
  43. package/src/rasterizer/canvas-mapper.ts +677 -0
  44. package/src/rasterizer/error-distribution.ts +344 -0
  45. package/src/rasterizer/gradient-mapper.ts +563 -0
  46. package/src/rasterizer/topology-rounding.ts +499 -0
  47. package/src/runtime/__tests__/event-backpressure.test.ts +254 -0
  48. package/src/runtime/event-backpressure.ts +622 -0
  49. package/src/runtime/render-loop.ts +660 -0
  50. package/src/runtime/wasm-resource-manager.ts +349 -0
  51. package/src/runtime/wgpu-renderer-adapter.ts +318 -0
  52. package/src/semantic/__tests__/semantic-translator.test.ts +263 -0
  53. package/src/semantic/semantic-translator.ts +637 -0
  54. package/test-results/.last-run.json +4 -0
  55. package/tests/e2e/async-race.spec.ts +612 -0
  56. package/tests/e2e/bilayer-sync.spec.ts +405 -0
  57. package/tests/e2e/failures/.gitkeep +0 -0
  58. package/tests/e2e/fullstack.spec.ts +681 -0
  59. package/tests/e2e/g1-continuity.spec.ts +703 -0
  60. package/tests/e2e/golden/.gitkeep +0 -0
  61. package/tests/e2e/golden/conic-color-wheel.raw +0 -0
  62. package/tests/e2e/golden/conic-color-wheel.sha256 +1 -0
  63. package/tests/e2e/golden/conic-rotated.raw +0 -0
  64. package/tests/e2e/golden/conic-rotated.sha256 +1 -0
  65. package/tests/e2e/golden/linear-45deg.raw +0 -0
  66. package/tests/e2e/golden/linear-45deg.sha256 +1 -0
  67. package/tests/e2e/golden/linear-horizontal.raw +0 -0
  68. package/tests/e2e/golden/linear-horizontal.sha256 +1 -0
  69. package/tests/e2e/golden/linear-multi-stop.raw +0 -0
  70. package/tests/e2e/golden/linear-multi-stop.sha256 +1 -0
  71. package/tests/e2e/golden/radial-circle-center.raw +0 -0
  72. package/tests/e2e/golden/radial-circle-center.sha256 +1 -0
  73. package/tests/e2e/golden/radial-offset.raw +0 -0
  74. package/tests/e2e/golden/radial-offset.sha256 +1 -0
  75. package/tests/e2e/golden/tile-mirror.raw +0 -0
  76. package/tests/e2e/golden/tile-mirror.sha256 +1 -0
  77. package/tests/e2e/golden/tile-repeat.raw +0 -0
  78. package/tests/e2e/golden/tile-repeat.sha256 +1 -0
  79. package/tests/e2e/gradient-animation.spec.ts +606 -0
  80. package/tests/e2e/memory-stability.spec.ts +396 -0
  81. package/tests/e2e/path-topology.spec.ts +674 -0
  82. package/tests/e2e/performance-profile.spec.ts +501 -0
  83. package/tests/e2e/screenshot.spec.ts +60 -0
  84. package/tests/e2e/test-harness.html +1005 -0
  85. package/tests/e2e/text-layout.spec.ts +451 -0
  86. package/tests/e2e/visual-demo.html +340 -0
  87. package/tests/e2e/visual-regression.spec.ts +335 -0
  88. package/tsconfig.json +12 -0
  89. package/vitest.config.ts +8 -0
@@ -0,0 +1,637 @@
1
+ /**
2
+ * Semantic Translator for P/Q Dimension Bridge (Phase 7: Task 20)
3
+ *
4
+ * This module converts raw solver output (VarId -> Rational maps) into
5
+ * entity-level coordinate descriptions suitable for LLM comprehension.
6
+ *
7
+ * ## Purpose
8
+ *
9
+ * The P-dimension solver produces solutions in the form:
10
+ * { "1:x": "100/1", "1:y": "200/1", "2:x": "300/1", ... }
11
+ *
12
+ * This is not LLM-friendly. The SemanticTranslator converts this to:
13
+ * {
14
+ * entities: [
15
+ * { entityId: 1, coordinates: { x: 100, y: 200 }, description: "Point at (100, 200)" },
16
+ * { entityId: 2, coordinates: { x: 300, y: 150 }, description: "Point at (300, 150)" }
17
+ * ],
18
+ * relationships: [
19
+ * { type: "distance", from: 1, to: 2, value: "~212 units" }
20
+ * ]
21
+ * }
22
+ *
23
+ * ## Axiom 2: Ouroboros Binding
24
+ *
25
+ * This module sits at the P/Q dimension boundary. It translates deterministic
26
+ * solver output (P) into natural language suitable for non-deterministic
27
+ * oracles (Q/LLM).
28
+ *
29
+ * ## Usage
30
+ *
31
+ * ```typescript
32
+ * const translator = new SemanticTranslator(entityRegistry);
33
+ * const semanticSolution = translator.translateSolution(rawSolution);
34
+ * const diff = translator.compareSolutions(solution1, solution2);
35
+ * ```
36
+ */
37
+
38
+ import type { EntityId, Rational } from '../ast/types';
39
+
40
+ // =============================================================================
41
+ // Types for Raw Solver Output
42
+ // =============================================================================
43
+
44
+ /**
45
+ * Raw solution from solver: "entity_id:component" -> "numerator/denominator"
46
+ */
47
+ export type RawSolution = Map<string, string>;
48
+
49
+ /**
50
+ * Parsed VarId key.
51
+ */
52
+ export interface ParsedVarId {
53
+ entityId: EntityId;
54
+ component: VectorComponent;
55
+ }
56
+
57
+ /**
58
+ * Vector components matching Rust VectorComponent enum.
59
+ */
60
+ export type VectorComponent =
61
+ | 'x'
62
+ | 'y'
63
+ | 'z'
64
+ | 't'
65
+ | 'value'
66
+ | 'r'
67
+ | 'g'
68
+ | 'b'
69
+ | 'alpha'
70
+ | 'position';
71
+
72
+ // =============================================================================
73
+ // Entity Metadata Registry
74
+ // =============================================================================
75
+
76
+ /**
77
+ * Entity type classification for semantic descriptions.
78
+ */
79
+ export type EntityType =
80
+ | 'point'
81
+ | 'control_point'
82
+ | 'rect'
83
+ | 'circle'
84
+ | 'text'
85
+ | 'color_stop'
86
+ | 'scalar'
87
+ | 'unknown';
88
+
89
+ /**
90
+ * Metadata about an entity (provided by the constraint graph).
91
+ */
92
+ export interface EntityMetadata {
93
+ entityId: EntityId;
94
+ type: EntityType;
95
+ name?: string;
96
+ parentId?: EntityId;
97
+ }
98
+
99
+ /**
100
+ * Registry of entity metadata for semantic translation.
101
+ */
102
+ export interface EntityRegistry {
103
+ get(entityId: EntityId): EntityMetadata | undefined;
104
+ getAll(): EntityMetadata[];
105
+ }
106
+
107
+ // =============================================================================
108
+ // Semantic Output Types
109
+ // =============================================================================
110
+
111
+ /**
112
+ * Coordinates for a spatial entity.
113
+ */
114
+ export interface EntityCoordinates {
115
+ x?: number;
116
+ y?: number;
117
+ z?: number;
118
+ t?: number;
119
+ }
120
+
121
+ /**
122
+ * Color values for a color stop entity.
123
+ */
124
+ export interface EntityColor {
125
+ r?: number;
126
+ g?: number;
127
+ b?: number;
128
+ alpha?: number;
129
+ position?: number;
130
+ }
131
+
132
+ /**
133
+ * Scalar value for value-only entities.
134
+ */
135
+ export interface EntityScalar {
136
+ value?: number;
137
+ }
138
+
139
+ /**
140
+ * A semantic entity description.
141
+ */
142
+ export interface SemanticEntity {
143
+ entityId: EntityId;
144
+ type: EntityType;
145
+ name?: string;
146
+
147
+ /** Spatial coordinates (for points, rects, etc.) */
148
+ coordinates?: EntityCoordinates;
149
+
150
+ /** Color values (for color stops) */
151
+ color?: EntityColor;
152
+
153
+ /** Scalar value (for radius, angle, etc.) */
154
+ scalar?: EntityScalar;
155
+
156
+ /** Human-readable description for LLM */
157
+ description: string;
158
+ }
159
+
160
+ /**
161
+ * Relationship between entities derived from geometry.
162
+ */
163
+ export interface SemanticRelationship {
164
+ type: 'distance' | 'alignment' | 'containment' | 'collinear';
165
+ fromEntityId: EntityId;
166
+ toEntityId: EntityId;
167
+ description: string;
168
+ value?: string;
169
+ }
170
+
171
+ /**
172
+ * Complete semantic solution.
173
+ */
174
+ export interface SemanticSolution {
175
+ /** Unique identifier for this solution (index in MultipleSolutions) */
176
+ solutionIndex: number;
177
+
178
+ /** All entities with resolved coordinates */
179
+ entities: SemanticEntity[];
180
+
181
+ /** Derived relationships between entities */
182
+ relationships: SemanticRelationship[];
183
+
184
+ /** High-level summary for LLM consumption */
185
+ summary: string;
186
+ }
187
+
188
+ /**
189
+ * Difference between two solutions.
190
+ */
191
+ export interface SolutionDiff {
192
+ /** Entities that differ between solutions */
193
+ differingEntities: EntityDiff[];
194
+
195
+ /** Human-readable diff summary */
196
+ summary: string;
197
+ }
198
+
199
+ export interface EntityDiff {
200
+ entityId: EntityId;
201
+ name?: string;
202
+ solution1: SemanticEntity;
203
+ solution2: SemanticEntity;
204
+ description: string;
205
+ }
206
+
207
+ // =============================================================================
208
+ // SemanticTranslator Implementation
209
+ // =============================================================================
210
+
211
+ /**
212
+ * Translates raw solver output to LLM-friendly semantic descriptions.
213
+ */
214
+ export class SemanticTranslator {
215
+ constructor(private readonly registry: EntityRegistry) {}
216
+
217
+ /**
218
+ * Parse a raw VarId key (e.g., "5:x") into its components.
219
+ */
220
+ parseVarId(key: string): ParsedVarId | null {
221
+ const [entityStr, component] = key.split(':');
222
+ if (!entityStr || !component) return null;
223
+
224
+ const entityId = parseInt(entityStr, 10);
225
+ if (isNaN(entityId)) return null;
226
+
227
+ const validComponents: VectorComponent[] = [
228
+ 'x', 'y', 'z', 't', 'value', 'r', 'g', 'b', 'alpha', 'position',
229
+ ];
230
+ if (!validComponents.includes(component as VectorComponent)) return null;
231
+
232
+ return { entityId, component: component as VectorComponent };
233
+ }
234
+
235
+ /**
236
+ * Parse a rational string (e.g., "100/1" or "100") to a number.
237
+ *
238
+ * WARNING: This converts P-dimension exact rationals to Q-dimension floats.
239
+ * Only use for display/LLM consumption, NEVER for constraint solving.
240
+ */
241
+ parseRationalToFloat(value: string): number {
242
+ const parts = value.split('/');
243
+ if (parts.length === 2) {
244
+ const num = parseFloat(parts[0]);
245
+ const denom = parseFloat(parts[1]);
246
+ if (denom === 0) return NaN;
247
+ return num / denom;
248
+ }
249
+ return parseFloat(value);
250
+ }
251
+
252
+ /**
253
+ * Translate a raw solution to a semantic solution.
254
+ */
255
+ translateSolution(rawSolution: RawSolution, solutionIndex: number = 0): SemanticSolution {
256
+ // Group by entity
257
+ const entityMap = new Map<EntityId, Map<VectorComponent, number>>();
258
+
259
+ for (const [key, value] of rawSolution) {
260
+ const parsed = this.parseVarId(key);
261
+ if (!parsed) continue;
262
+
263
+ let components = entityMap.get(parsed.entityId);
264
+ if (!components) {
265
+ components = new Map();
266
+ entityMap.set(parsed.entityId, components);
267
+ }
268
+ components.set(parsed.component, this.parseRationalToFloat(value));
269
+ }
270
+
271
+ // Build semantic entities
272
+ const entities: SemanticEntity[] = [];
273
+ for (const [entityId, components] of entityMap) {
274
+ const metadata = this.registry.get(entityId);
275
+ const entity = this.buildSemanticEntity(
276
+ entityId,
277
+ metadata?.type ?? 'unknown',
278
+ metadata?.name,
279
+ components,
280
+ );
281
+ entities.push(entity);
282
+ }
283
+
284
+ // Sort by entity ID for consistent output
285
+ entities.sort((a, b) => a.entityId - b.entityId);
286
+
287
+ // Derive relationships
288
+ const relationships = this.deriveRelationships(entities);
289
+
290
+ // Generate summary
291
+ const summary = this.generateSummary(entities, relationships);
292
+
293
+ return {
294
+ solutionIndex,
295
+ entities,
296
+ relationships,
297
+ summary,
298
+ };
299
+ }
300
+
301
+ /**
302
+ * Build a semantic entity from raw components.
303
+ */
304
+ private buildSemanticEntity(
305
+ entityId: EntityId,
306
+ type: EntityType,
307
+ name: string | undefined,
308
+ components: Map<VectorComponent, number>,
309
+ ): SemanticEntity {
310
+ const entity: SemanticEntity = {
311
+ entityId,
312
+ type,
313
+ name,
314
+ description: '',
315
+ };
316
+
317
+ // Build coordinates/color/scalar based on entity type
318
+ if (type === 'color_stop') {
319
+ entity.color = {
320
+ r: components.get('r'),
321
+ g: components.get('g'),
322
+ b: components.get('b'),
323
+ alpha: components.get('alpha'),
324
+ position: components.get('position'),
325
+ };
326
+ entity.description = this.describeColorStop(entity.color, name);
327
+ } else if (type === 'scalar') {
328
+ entity.scalar = {
329
+ value: components.get('value'),
330
+ };
331
+ entity.description = this.describeScalar(entity.scalar, name);
332
+ } else {
333
+ // Spatial entity
334
+ entity.coordinates = {
335
+ x: components.get('x'),
336
+ y: components.get('y'),
337
+ z: components.get('z'),
338
+ t: components.get('t'),
339
+ };
340
+ entity.description = this.describeSpatialEntity(type, entity.coordinates, name);
341
+ }
342
+
343
+ return entity;
344
+ }
345
+
346
+ /**
347
+ * Generate description for a spatial entity.
348
+ */
349
+ private describeSpatialEntity(
350
+ type: EntityType,
351
+ coords: EntityCoordinates,
352
+ name?: string,
353
+ ): string {
354
+ const nameStr = name ? `"${name}" ` : '';
355
+ const x = coords.x !== undefined ? this.formatCoord(coords.x) : '?';
356
+ const y = coords.y !== undefined ? this.formatCoord(coords.y) : '?';
357
+
358
+ switch (type) {
359
+ case 'point':
360
+ case 'control_point':
361
+ return `${nameStr}${type === 'control_point' ? 'Control point' : 'Point'} at (${x}, ${y})`;
362
+ case 'rect':
363
+ return `${nameStr}Rectangle at (${x}, ${y})`;
364
+ case 'circle':
365
+ return `${nameStr}Circle centered at (${x}, ${y})`;
366
+ case 'text':
367
+ return `${nameStr}Text at (${x}, ${y})`;
368
+ default:
369
+ return `${nameStr}Entity ${type} at (${x}, ${y})`;
370
+ }
371
+ }
372
+
373
+ /**
374
+ * Generate description for a color stop.
375
+ */
376
+ private describeColorStop(color: EntityColor, name?: string): string {
377
+ const nameStr = name ? `"${name}" ` : '';
378
+ const r = color.r !== undefined ? Math.round(color.r) : '?';
379
+ const g = color.g !== undefined ? Math.round(color.g) : '?';
380
+ const b = color.b !== undefined ? Math.round(color.b) : '?';
381
+ const pos = color.position !== undefined ? `${(color.position * 100).toFixed(0)}%` : '?';
382
+
383
+ return `${nameStr}Color stop at ${pos}: rgb(${r}, ${g}, ${b})`;
384
+ }
385
+
386
+ /**
387
+ * Generate description for a scalar entity.
388
+ */
389
+ private describeScalar(scalar: EntityScalar, name?: string): string {
390
+ const nameStr = name ? `"${name}"` : 'Scalar';
391
+ const val = scalar.value !== undefined ? this.formatCoord(scalar.value) : '?';
392
+
393
+ return `${nameStr} = ${val}`;
394
+ }
395
+
396
+ /**
397
+ * Format a coordinate value for display.
398
+ */
399
+ private formatCoord(value: number): string {
400
+ // Round to reasonable precision for display
401
+ if (Number.isInteger(value)) {
402
+ return value.toString();
403
+ }
404
+ return value.toFixed(2).replace(/\.?0+$/, '');
405
+ }
406
+
407
+ /**
408
+ * Derive geometric relationships between entities.
409
+ */
410
+ private deriveRelationships(entities: SemanticEntity[]): SemanticRelationship[] {
411
+ const relationships: SemanticRelationship[] = [];
412
+
413
+ // Find horizontally aligned entities (same Y)
414
+ const byY = new Map<number, SemanticEntity[]>();
415
+ for (const entity of entities) {
416
+ if (entity.coordinates?.y !== undefined) {
417
+ const y = Math.round(entity.coordinates.y);
418
+ let list = byY.get(y);
419
+ if (!list) {
420
+ list = [];
421
+ byY.set(y, list);
422
+ }
423
+ list.push(entity);
424
+ }
425
+ }
426
+
427
+ for (const [y, aligned] of byY) {
428
+ if (aligned.length >= 2) {
429
+ const ids = aligned.map(e => e.entityId).join(', ');
430
+ relationships.push({
431
+ type: 'alignment',
432
+ fromEntityId: aligned[0].entityId,
433
+ toEntityId: aligned[aligned.length - 1].entityId,
434
+ description: `Entities ${ids} are horizontally aligned at y=${y}`,
435
+ });
436
+ }
437
+ }
438
+
439
+ // Find vertically aligned entities (same X)
440
+ const byX = new Map<number, SemanticEntity[]>();
441
+ for (const entity of entities) {
442
+ if (entity.coordinates?.x !== undefined) {
443
+ const x = Math.round(entity.coordinates.x);
444
+ let list = byX.get(x);
445
+ if (!list) {
446
+ list = [];
447
+ byX.set(x, list);
448
+ }
449
+ list.push(entity);
450
+ }
451
+ }
452
+
453
+ for (const [x, aligned] of byX) {
454
+ if (aligned.length >= 2) {
455
+ const ids = aligned.map(e => e.entityId).join(', ');
456
+ relationships.push({
457
+ type: 'alignment',
458
+ fromEntityId: aligned[0].entityId,
459
+ toEntityId: aligned[aligned.length - 1].entityId,
460
+ description: `Entities ${ids} are vertically aligned at x=${x}`,
461
+ });
462
+ }
463
+ }
464
+
465
+ return relationships;
466
+ }
467
+
468
+ /**
469
+ * Generate a high-level summary of the solution.
470
+ */
471
+ private generateSummary(
472
+ entities: SemanticEntity[],
473
+ relationships: SemanticRelationship[],
474
+ ): string {
475
+ const entityCounts = new Map<EntityType, number>();
476
+ for (const entity of entities) {
477
+ entityCounts.set(entity.type, (entityCounts.get(entity.type) ?? 0) + 1);
478
+ }
479
+
480
+ const parts: string[] = [];
481
+ for (const [type, count] of entityCounts) {
482
+ parts.push(`${count} ${type}${count > 1 ? 's' : ''}`);
483
+ }
484
+
485
+ let summary = `Solution with ${entities.length} entities: ${parts.join(', ')}.`;
486
+
487
+ if (relationships.length > 0) {
488
+ summary += ` ${relationships.length} geometric relationship${relationships.length > 1 ? 's' : ''} detected.`;
489
+ }
490
+
491
+ return summary;
492
+ }
493
+
494
+ /**
495
+ * Compare two solutions and generate a diff.
496
+ */
497
+ compareSolutions(
498
+ solution1: SemanticSolution,
499
+ solution2: SemanticSolution,
500
+ ): SolutionDiff {
501
+ const diffs: EntityDiff[] = [];
502
+
503
+ const entities1 = new Map(solution1.entities.map(e => [e.entityId, e]));
504
+ const entities2 = new Map(solution2.entities.map(e => [e.entityId, e]));
505
+
506
+ // Find entities that differ
507
+ for (const [entityId, e1] of entities1) {
508
+ const e2 = entities2.get(entityId);
509
+ if (!e2) continue;
510
+
511
+ if (!this.entitiesEqual(e1, e2)) {
512
+ diffs.push({
513
+ entityId,
514
+ name: e1.name ?? e2.name,
515
+ solution1: e1,
516
+ solution2: e2,
517
+ description: this.describeDiff(e1, e2),
518
+ });
519
+ }
520
+ }
521
+
522
+ const summary = diffs.length === 0
523
+ ? 'Solutions are identical'
524
+ : `${diffs.length} entities differ: ${diffs.map(d => d.entityId).join(', ')}`;
525
+
526
+ return { differingEntities: diffs, summary };
527
+ }
528
+
529
+ /**
530
+ * Check if two semantic entities are equal.
531
+ */
532
+ private entitiesEqual(e1: SemanticEntity, e2: SemanticEntity): boolean {
533
+ const tolerance = 1e-6;
534
+
535
+ if (e1.coordinates && e2.coordinates) {
536
+ for (const key of ['x', 'y', 'z', 't'] as const) {
537
+ const v1 = e1.coordinates[key];
538
+ const v2 = e2.coordinates[key];
539
+ if (v1 !== undefined && v2 !== undefined && Math.abs(v1 - v2) > tolerance) {
540
+ return false;
541
+ }
542
+ }
543
+ }
544
+
545
+ if (e1.color && e2.color) {
546
+ for (const key of ['r', 'g', 'b', 'alpha', 'position'] as const) {
547
+ const v1 = e1.color[key];
548
+ const v2 = e2.color[key];
549
+ if (v1 !== undefined && v2 !== undefined && Math.abs(v1 - v2) > tolerance) {
550
+ return false;
551
+ }
552
+ }
553
+ }
554
+
555
+ if (e1.scalar && e2.scalar) {
556
+ const v1 = e1.scalar.value;
557
+ const v2 = e2.scalar.value;
558
+ if (v1 !== undefined && v2 !== undefined && Math.abs(v1 - v2) > tolerance) {
559
+ return false;
560
+ }
561
+ }
562
+
563
+ return true;
564
+ }
565
+
566
+ /**
567
+ * Describe the difference between two entities.
568
+ */
569
+ private describeDiff(e1: SemanticEntity, e2: SemanticEntity): string {
570
+ if (e1.coordinates && e2.coordinates) {
571
+ const changes: string[] = [];
572
+ if (e1.coordinates.x !== e2.coordinates.x) {
573
+ changes.push(`x: ${this.formatCoord(e1.coordinates.x ?? 0)} -> ${this.formatCoord(e2.coordinates.x ?? 0)}`);
574
+ }
575
+ if (e1.coordinates.y !== e2.coordinates.y) {
576
+ changes.push(`y: ${this.formatCoord(e1.coordinates.y ?? 0)} -> ${this.formatCoord(e2.coordinates.y ?? 0)}`);
577
+ }
578
+ return `Position changes: ${changes.join(', ')}`;
579
+ }
580
+
581
+ return 'Values differ';
582
+ }
583
+
584
+ /**
585
+ * Translate multiple solutions and generate comparative descriptions.
586
+ */
587
+ translateMultipleSolutions(rawSolutions: RawSolution[]): {
588
+ solutions: SemanticSolution[];
589
+ comparison: string;
590
+ } {
591
+ const solutions = rawSolutions.map((raw, idx) => this.translateSolution(raw, idx));
592
+
593
+ if (solutions.length < 2) {
594
+ return {
595
+ solutions,
596
+ comparison: 'Only one solution available.',
597
+ };
598
+ }
599
+
600
+ // Generate pairwise comparison for first two solutions
601
+ const diff = this.compareSolutions(solutions[0], solutions[1]);
602
+
603
+ let comparison = `${solutions.length} solutions found.\n\n`;
604
+ comparison += `Solution 0 vs Solution 1:\n${diff.summary}\n\n`;
605
+
606
+ for (const entityDiff of diff.differingEntities) {
607
+ comparison += ` Entity ${entityDiff.entityId}: ${entityDiff.description}\n`;
608
+ }
609
+
610
+ return { solutions, comparison };
611
+ }
612
+ }
613
+
614
+ // =============================================================================
615
+ // Utility: Create Entity Registry from Array
616
+ // =============================================================================
617
+
618
+ /**
619
+ * Create an EntityRegistry from an array of metadata.
620
+ */
621
+ export function createEntityRegistry(entities: EntityMetadata[]): EntityRegistry {
622
+ const map = new Map(entities.map(e => [e.entityId, e]));
623
+ return {
624
+ get: (id: EntityId) => map.get(id),
625
+ getAll: () => entities,
626
+ };
627
+ }
628
+
629
+ /**
630
+ * Create an empty EntityRegistry (all entities will be "unknown" type).
631
+ */
632
+ export function createEmptyRegistry(): EntityRegistry {
633
+ return {
634
+ get: () => undefined,
635
+ getAll: () => [],
636
+ };
637
+ }
@@ -0,0 +1,4 @@
1
+ {
2
+ "status": "passed",
3
+ "failedTests": []
4
+ }