@viewscript/renderer 0.1.0-20260515014709 → 0.1.0-20260515015849

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 (34) hide show
  1. package/dist/compiler/chunk-splitter.d.ts +1 -1
  2. package/dist/rasterizer/canvas-mapper.d.ts +1 -1
  3. package/dist/rasterizer/error-distribution.d.ts +1 -1
  4. package/dist/rasterizer/gradient-mapper.d.ts +1 -1
  5. package/dist/rasterizer/topology-rounding.d.ts +1 -1
  6. package/dist/runtime/event-backpressure.d.ts +1 -1
  7. package/dist/runtime/render-loop.d.ts +1 -1
  8. package/dist/semantic/semantic-translator.d.ts +1 -1
  9. package/package.json +1 -1
  10. package/src/compiler/chunk-splitter.ts +1 -1
  11. package/src/rasterizer/__tests__/error-distribution.test.ts +11 -11
  12. package/src/rasterizer/canvas-mapper.ts +1 -1
  13. package/src/rasterizer/error-distribution.ts +1 -1
  14. package/src/rasterizer/gradient-mapper.ts +1 -1
  15. package/src/rasterizer/topology-rounding.ts +1 -1
  16. package/src/runtime/__tests__/event-backpressure.test.ts +3 -3
  17. package/src/runtime/__tests__/ffi-integration.test.ts +8 -0
  18. package/src/runtime/event-backpressure.ts +1 -1
  19. package/src/runtime/render-loop.ts +1 -1
  20. package/src/semantic/__tests__/semantic-translator.test.ts +2 -2
  21. package/src/semantic/semantic-translator.ts +1 -1
  22. package/tsconfig.json +2 -1
  23. package/dist/rasterizer/__tests__/error-distribution.test.d.ts +0 -7
  24. package/dist/rasterizer/__tests__/error-distribution.test.js +0 -322
  25. package/dist/runtime/__tests__/event-backpressure.test.d.ts +0 -10
  26. package/dist/runtime/__tests__/event-backpressure.test.js +0 -190
  27. package/dist/runtime/__tests__/ffi-dispatcher.test.d.ts +0 -11
  28. package/dist/runtime/__tests__/ffi-dispatcher.test.js +0 -408
  29. package/dist/runtime/__tests__/ffi-integration.test.d.ts +0 -9
  30. package/dist/runtime/__tests__/ffi-integration.test.js +0 -514
  31. package/dist/runtime/__tests__/wasm-solver-bridge.test.d.ts +0 -9
  32. package/dist/runtime/__tests__/wasm-solver-bridge.test.js +0 -214
  33. package/dist/semantic/__tests__/semantic-translator.test.d.ts +0 -4
  34. package/dist/semantic/__tests__/semantic-translator.test.js +0 -203
@@ -1,214 +0,0 @@
1
- /**
2
- * Tests for WASM Solver Bridge
3
- *
4
- * Validates:
5
- * 1. QSnapshot construction from mutations + FFI results
6
- * 2. WASM tick() delegation and result parsing
7
- * 3. FFI result injection lifecycle
8
- */
9
- import { describe, it, expect, beforeEach, vi } from 'vitest';
10
- import { WasmSolverBridge, createWasmSolverBridge, } from '../wasm-solver-bridge.js';
11
- // =============================================================================
12
- // Mock WASM Engine
13
- // =============================================================================
14
- function createMockEngine() {
15
- return {
16
- lastInput: null,
17
- tick(inputJson) {
18
- this.lastInput = inputJson;
19
- return JSON.stringify({ pending_ffi_calls: [] });
20
- },
21
- };
22
- }
23
- function createMockEngineWithCalls(calls) {
24
- return {
25
- tick() {
26
- return JSON.stringify({ pending_ffi_calls: calls });
27
- },
28
- };
29
- }
30
- // =============================================================================
31
- // Tests
32
- // =============================================================================
33
- describe('WasmSolverBridge', () => {
34
- let bridge;
35
- let mockEngine;
36
- beforeEach(() => {
37
- mockEngine = createMockEngine();
38
- bridge = createWasmSolverBridge(mockEngine);
39
- });
40
- // ===========================================================================
41
- // Basic Evaluation
42
- // ===========================================================================
43
- describe('evaluate', () => {
44
- it('calls engine.tick() with QSnapshot JSON', () => {
45
- const mutations = [];
46
- bridge.evaluate(mutations);
47
- expect(mockEngine.lastInput).not.toBeNull();
48
- const snapshot = JSON.parse(mockEngine.lastInput);
49
- expect(snapshot).toHaveProperty('values');
50
- expect(snapshot).toHaveProperty('mutations');
51
- });
52
- it('returns empty bounds and pendingFfiCalls by default', () => {
53
- const result = bridge.evaluate([]);
54
- expect(result.bounds.size).toBe(0);
55
- expect(result.pendingFfiCalls).toEqual([]);
56
- });
57
- it('extracts pendingFfiCalls from tick result', () => {
58
- const calls = [{ ffi_id: 1, args: [1, 2, 3] }];
59
- bridge = createWasmSolverBridge(createMockEngineWithCalls(calls));
60
- const result = bridge.evaluate([]);
61
- expect(result.pendingFfiCalls).toEqual(calls);
62
- });
63
- });
64
- // ===========================================================================
65
- // Mutation Conversion
66
- // ===========================================================================
67
- describe('mutation conversion', () => {
68
- it('converts TStateMutation to QSnapshot values', () => {
69
- const mutations = [
70
- { entityId: 1, state: 'hover', value: 1, timestamp: 100 },
71
- { entityId: 2, state: 'scroll_y', value: 0.5, timestamp: 100 },
72
- ];
73
- bridge.evaluate(mutations);
74
- const snapshot = JSON.parse(mockEngine.lastInput);
75
- expect(snapshot.values['1_hover']).toEqual({ type: 'float', value: 1 });
76
- expect(snapshot.values['2_scroll_y']).toEqual({ type: 'float', value: 0.5 });
77
- });
78
- it('includes mutations array for legacy format', () => {
79
- const mutations = [
80
- { entityId: 1, state: 'pressed', value: 1, timestamp: 100 },
81
- ];
82
- bridge.evaluate(mutations);
83
- const snapshot = JSON.parse(mockEngine.lastInput);
84
- expect(snapshot.mutations).toHaveLength(1);
85
- expect(snapshot.mutations[0]).toMatchObject({
86
- entity_id: 1,
87
- state: 'pressed',
88
- value: 1,
89
- });
90
- });
91
- });
92
- // ===========================================================================
93
- // FFI Result Injection
94
- // ===========================================================================
95
- describe('injectFfiResults', () => {
96
- it('includes injected FFI results in QSnapshot values', () => {
97
- const ffiResults = [
98
- { name: 'clamped_opacity', value: 0.75, timestamp: 100 },
99
- { name: 'distance', value: 42, timestamp: 100 },
100
- ];
101
- bridge.injectFfiResults(ffiResults);
102
- bridge.evaluate([]);
103
- const snapshot = JSON.parse(mockEngine.lastInput);
104
- expect(snapshot.values['clamped_opacity']).toEqual({ type: 'float', value: 0.75 });
105
- expect(snapshot.values['distance']).toEqual({ type: 'float', value: 42 });
106
- });
107
- it('clears FFI results after evaluate()', () => {
108
- bridge.injectFfiResults([{ name: 'test', value: 1, timestamp: 100 }]);
109
- // First evaluate includes FFI result
110
- bridge.evaluate([]);
111
- let snapshot = JSON.parse(mockEngine.lastInput);
112
- expect(snapshot.values['test']).toBeDefined();
113
- // Second evaluate should NOT include FFI result
114
- bridge.evaluate([]);
115
- snapshot = JSON.parse(mockEngine.lastInput);
116
- expect(snapshot.values['test']).toBeUndefined();
117
- });
118
- it('FFI results override mutation values with same name', () => {
119
- // This tests the merge order: FFI results come first, then mutations
120
- // If there's a naming collision, the mutation should win
121
- const ffiResults = [
122
- { name: '1_hover', value: 0.5, timestamp: 100 },
123
- ];
124
- const mutations = [
125
- { entityId: 1, state: 'hover', value: 1, timestamp: 100 },
126
- ];
127
- bridge.injectFfiResults(ffiResults);
128
- bridge.evaluate(mutations);
129
- const snapshot = JSON.parse(mockEngine.lastInput);
130
- // Mutation overwrites FFI result (processed later)
131
- expect(snapshot.values['1_hover'].value).toBe(1);
132
- });
133
- });
134
- // ===========================================================================
135
- // Error Handling
136
- // ===========================================================================
137
- describe('error handling', () => {
138
- it('handles engine.tick() throwing', () => {
139
- const errorEngine = {
140
- tick() {
141
- throw new Error('WASM panic');
142
- },
143
- };
144
- bridge = createWasmSolverBridge(errorEngine);
145
- const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => { });
146
- const result = bridge.evaluate([]);
147
- expect(result.bounds.size).toBe(0);
148
- expect(result.pendingFfiCalls).toEqual([]);
149
- expect(errorSpy).toHaveBeenCalled();
150
- errorSpy.mockRestore();
151
- });
152
- it('preserves FFI results when tick() throws for next frame retry', () => {
153
- let callCount = 0;
154
- const flakyEngine = {
155
- lastInput: null,
156
- tick(inputJson) {
157
- callCount++;
158
- if (callCount === 1) {
159
- throw new Error('Transient WASM error');
160
- }
161
- this.lastInput = inputJson;
162
- return JSON.stringify({ pending_ffi_calls: [] });
163
- },
164
- };
165
- bridge = createWasmSolverBridge(flakyEngine);
166
- const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => { });
167
- // Inject FFI results
168
- bridge.injectFfiResults([{ name: 'important_value', value: 42, timestamp: 100 }]);
169
- // First evaluate fails - FFI results should be preserved
170
- bridge.evaluate([]);
171
- expect(errorSpy).toHaveBeenCalled();
172
- // Second evaluate succeeds - FFI results should still be included
173
- bridge.evaluate([]);
174
- const snapshot = JSON.parse(flakyEngine.lastInput);
175
- expect(snapshot.values['important_value']).toEqual({ type: 'float', value: 42 });
176
- errorSpy.mockRestore();
177
- });
178
- it('handles invalid JSON from tick()', () => {
179
- const badEngine = {
180
- tick() {
181
- return 'not valid json';
182
- },
183
- };
184
- bridge = createWasmSolverBridge(badEngine);
185
- const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => { });
186
- const result = bridge.evaluate([]);
187
- expect(result.pendingFfiCalls).toEqual([]);
188
- expect(errorSpy).toHaveBeenCalled();
189
- errorSpy.mockRestore();
190
- });
191
- it('handles missing pending_ffi_calls in result', () => {
192
- const partialEngine = {
193
- tick() {
194
- return JSON.stringify({}); // No pending_ffi_calls field
195
- },
196
- };
197
- bridge = createWasmSolverBridge(partialEngine);
198
- const result = bridge.evaluate([]);
199
- expect(result.pendingFfiCalls).toEqual([]);
200
- });
201
- });
202
- // ===========================================================================
203
- // Factory Function
204
- // ===========================================================================
205
- describe('createWasmSolverBridge', () => {
206
- it('creates a new instance', () => {
207
- const engine = createMockEngine();
208
- const b1 = createWasmSolverBridge(engine);
209
- const b2 = createWasmSolverBridge(engine);
210
- expect(b1).not.toBe(b2);
211
- expect(b1).toBeInstanceOf(WasmSolverBridge);
212
- });
213
- });
214
- });
@@ -1,4 +0,0 @@
1
- /**
2
- * Tests for SemanticTranslator (Phase 7: Task 20)
3
- */
4
- export {};
@@ -1,203 +0,0 @@
1
- /**
2
- * Tests for SemanticTranslator (Phase 7: Task 20)
3
- */
4
- import { describe, it, expect } from 'vitest';
5
- import { SemanticTranslator, createEntityRegistry, createEmptyRegistry, } from '../semantic-translator';
6
- describe('SemanticTranslator', () => {
7
- describe('parseVarId', () => {
8
- it('should parse valid VarId keys', () => {
9
- const translator = new SemanticTranslator(createEmptyRegistry());
10
- expect(translator.parseVarId('1:x')).toEqual({ entityId: 1, component: 'x' });
11
- expect(translator.parseVarId('42:y')).toEqual({ entityId: 42, component: 'y' });
12
- expect(translator.parseVarId('100:value')).toEqual({ entityId: 100, component: 'value' });
13
- expect(translator.parseVarId('5:r')).toEqual({ entityId: 5, component: 'r' });
14
- expect(translator.parseVarId('5:alpha')).toEqual({ entityId: 5, component: 'alpha' });
15
- });
16
- it('should return null for invalid keys', () => {
17
- const translator = new SemanticTranslator(createEmptyRegistry());
18
- expect(translator.parseVarId('')).toBeNull();
19
- expect(translator.parseVarId('invalid')).toBeNull();
20
- expect(translator.parseVarId(':x')).toBeNull();
21
- expect(translator.parseVarId('1:')).toBeNull();
22
- expect(translator.parseVarId('1:unknown_component')).toBeNull();
23
- expect(translator.parseVarId('abc:x')).toBeNull();
24
- });
25
- });
26
- describe('parseRationalToFloat', () => {
27
- it('should parse rational strings', () => {
28
- const translator = new SemanticTranslator(createEmptyRegistry());
29
- expect(translator.parseRationalToFloat('100/1')).toBe(100);
30
- expect(translator.parseRationalToFloat('1/2')).toBe(0.5);
31
- expect(translator.parseRationalToFloat('100')).toBe(100);
32
- expect(translator.parseRationalToFloat('-50/1')).toBe(-50);
33
- expect(translator.parseRationalToFloat('1/3')).toBeCloseTo(0.333, 2);
34
- });
35
- it('should handle zero denominator', () => {
36
- const translator = new SemanticTranslator(createEmptyRegistry());
37
- expect(translator.parseRationalToFloat('1/0')).toBeNaN();
38
- });
39
- });
40
- describe('translateSolution', () => {
41
- it('should translate a simple point solution', () => {
42
- const registry = createEntityRegistry([
43
- { entityId: 1, type: 'point', name: 'P1' },
44
- ]);
45
- const translator = new SemanticTranslator(registry);
46
- const rawSolution = new Map([
47
- ['1:x', '100/1'],
48
- ['1:y', '200/1'],
49
- ]);
50
- const result = translator.translateSolution(rawSolution, 0);
51
- expect(result.solutionIndex).toBe(0);
52
- expect(result.entities).toHaveLength(1);
53
- expect(result.entities[0].entityId).toBe(1);
54
- expect(result.entities[0].type).toBe('point');
55
- expect(result.entities[0].coordinates?.x).toBe(100);
56
- expect(result.entities[0].coordinates?.y).toBe(200);
57
- expect(result.entities[0].description).toContain('P1');
58
- expect(result.entities[0].description).toContain('100');
59
- expect(result.entities[0].description).toContain('200');
60
- });
61
- it('should translate multiple entities', () => {
62
- const registry = createEntityRegistry([
63
- { entityId: 1, type: 'point', name: 'Start' },
64
- { entityId: 2, type: 'point', name: 'End' },
65
- { entityId: 3, type: 'control_point', name: 'CP1' },
66
- ]);
67
- const translator = new SemanticTranslator(registry);
68
- const rawSolution = new Map([
69
- ['1:x', '0/1'],
70
- ['1:y', '0/1'],
71
- ['2:x', '100/1'],
72
- ['2:y', '0/1'],
73
- ['3:x', '50/1'],
74
- ['3:y', '50/1'],
75
- ]);
76
- const result = translator.translateSolution(rawSolution, 0);
77
- expect(result.entities).toHaveLength(3);
78
- expect(result.summary).toContain('3 entities');
79
- });
80
- it('should translate color stop entities', () => {
81
- const registry = createEntityRegistry([
82
- { entityId: 10, type: 'color_stop', name: 'Stop1' },
83
- ]);
84
- const translator = new SemanticTranslator(registry);
85
- const rawSolution = new Map([
86
- ['10:r', '255/1'],
87
- ['10:g', '128/1'],
88
- ['10:b', '0/1'],
89
- ['10:alpha', '1/1'],
90
- ['10:position', '1/2'],
91
- ]);
92
- const result = translator.translateSolution(rawSolution, 0);
93
- expect(result.entities).toHaveLength(1);
94
- expect(result.entities[0].type).toBe('color_stop');
95
- expect(result.entities[0].color?.r).toBe(255);
96
- expect(result.entities[0].color?.g).toBe(128);
97
- expect(result.entities[0].color?.b).toBe(0);
98
- expect(result.entities[0].color?.position).toBe(0.5);
99
- expect(result.entities[0].description).toContain('50%');
100
- expect(result.entities[0].description).toContain('rgb(255, 128, 0)');
101
- });
102
- it('should handle unknown entity types', () => {
103
- const translator = new SemanticTranslator(createEmptyRegistry());
104
- const rawSolution = new Map([
105
- ['99:x', '50/1'],
106
- ['99:y', '75/1'],
107
- ]);
108
- const result = translator.translateSolution(rawSolution, 0);
109
- expect(result.entities).toHaveLength(1);
110
- expect(result.entities[0].type).toBe('unknown');
111
- expect(result.entities[0].coordinates?.x).toBe(50);
112
- });
113
- it('should detect horizontal alignment relationships', () => {
114
- const registry = createEntityRegistry([
115
- { entityId: 1, type: 'point' },
116
- { entityId: 2, type: 'point' },
117
- { entityId: 3, type: 'point' },
118
- ]);
119
- const translator = new SemanticTranslator(registry);
120
- // Three points on the same horizontal line (y=100)
121
- const rawSolution = new Map([
122
- ['1:x', '0/1'],
123
- ['1:y', '100/1'],
124
- ['2:x', '50/1'],
125
- ['2:y', '100/1'],
126
- ['3:x', '100/1'],
127
- ['3:y', '100/1'],
128
- ]);
129
- const result = translator.translateSolution(rawSolution, 0);
130
- expect(result.relationships.length).toBeGreaterThan(0);
131
- const alignment = result.relationships.find(r => r.type === 'alignment');
132
- expect(alignment).toBeDefined();
133
- expect(alignment?.description).toContain('horizontally aligned');
134
- });
135
- });
136
- describe('compareSolutions', () => {
137
- it('should detect identical solutions', () => {
138
- const registry = createEntityRegistry([
139
- { entityId: 1, type: 'point', name: 'P1' },
140
- ]);
141
- const translator = new SemanticTranslator(registry);
142
- const rawSolution = new Map([
143
- ['1:x', '100/1'],
144
- ['1:y', '200/1'],
145
- ]);
146
- const solution1 = translator.translateSolution(rawSolution, 0);
147
- const solution2 = translator.translateSolution(rawSolution, 1);
148
- const diff = translator.compareSolutions(solution1, solution2);
149
- expect(diff.differingEntities).toHaveLength(0);
150
- expect(diff.summary).toContain('identical');
151
- });
152
- it('should detect differing solutions', () => {
153
- const registry = createEntityRegistry([
154
- { entityId: 1, type: 'point', name: 'P1' },
155
- ]);
156
- const translator = new SemanticTranslator(registry);
157
- const solution1 = translator.translateSolution(new Map([['1:x', '100/1'], ['1:y', '200/1']]), 0);
158
- const solution2 = translator.translateSolution(new Map([['1:x', '150/1'], ['1:y', '250/1']]), 1);
159
- const diff = translator.compareSolutions(solution1, solution2);
160
- expect(diff.differingEntities).toHaveLength(1);
161
- expect(diff.differingEntities[0].entityId).toBe(1);
162
- expect(diff.summary).toContain('1 entities differ');
163
- });
164
- });
165
- describe('translateMultipleSolutions', () => {
166
- it('should translate and compare multiple solutions', () => {
167
- const registry = createEntityRegistry([
168
- { entityId: 1, type: 'point', name: 'P1' },
169
- ]);
170
- const translator = new SemanticTranslator(registry);
171
- const rawSolutions = [
172
- new Map([['1:x', '100/1'], ['1:y', '0/1']]),
173
- new Map([['1:x', '-100/1'], ['1:y', '0/1']]),
174
- ];
175
- const result = translator.translateMultipleSolutions(rawSolutions);
176
- expect(result.solutions).toHaveLength(2);
177
- expect(result.comparison).toContain('2 solutions found');
178
- expect(result.comparison).toContain('differ');
179
- });
180
- it('should handle single solution', () => {
181
- const translator = new SemanticTranslator(createEmptyRegistry());
182
- const rawSolutions = [
183
- new Map([['1:x', '100/1']]),
184
- ];
185
- const result = translator.translateMultipleSolutions(rawSolutions);
186
- expect(result.solutions).toHaveLength(1);
187
- expect(result.comparison).toContain('Only one solution');
188
- });
189
- });
190
- });
191
- describe('createEntityRegistry', () => {
192
- it('should create a working registry', () => {
193
- const entities = [
194
- { entityId: 1, type: 'point', name: 'P1' },
195
- { entityId: 2, type: 'rect', name: 'R1' },
196
- ];
197
- const registry = createEntityRegistry(entities);
198
- expect(registry.get(1)?.type).toBe('point');
199
- expect(registry.get(2)?.name).toBe('R1');
200
- expect(registry.get(999)).toBeUndefined();
201
- expect(registry.getAll()).toHaveLength(2);
202
- });
203
- });