@holoscript/framework 6.0.3 → 6.0.4

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 (160) hide show
  1. package/CHANGELOG.md +1 -2
  2. package/ROADMAP.md +68 -66
  3. package/dist/{InvisibleWallet-BB6tFvRA.d.cts → InvisibleWallet-EFiuaLn3.d.cts} +1 -1
  4. package/dist/{OrchestratorAgent-BvWgf9uw.d.cts → OrchestratorAgent-CrLDGNL6.d.cts} +1 -1
  5. package/dist/agents/index.cjs +11 -10
  6. package/dist/agents/index.d.cts +4 -16
  7. package/dist/ai/index.cjs +2 -2
  8. package/dist/behavior.cjs +10 -0
  9. package/dist/economy/index.cjs +4 -4
  10. package/dist/economy/index.d.cts +2 -2
  11. package/dist/index.cjs +33 -11
  12. package/dist/index.d.cts +3 -3
  13. package/dist/swarm/index.cjs +3 -0
  14. package/package.json +14 -9
  15. package/src/__tests__/bounty-marketplace.test.ts +53 -21
  16. package/src/__tests__/delegation.test.ts +1 -4
  17. package/src/__tests__/done-log-audit.test.ts +38 -46
  18. package/src/__tests__/framework.test.ts +172 -53
  19. package/src/__tests__/goal-synthesizer.test.ts +9 -6
  20. package/src/__tests__/presence.test.ts +1 -1
  21. package/src/__tests__/protocol-agent.test.ts +12 -11
  22. package/src/__tests__/revenue-splitter.test.ts +22 -15
  23. package/src/__tests__/scenario-driven-todo.test.ts +55 -35
  24. package/src/__tests__/self-improve.test.ts +28 -9
  25. package/src/__tests__/service-lifecycle.test.ts +9 -3
  26. package/src/__tests__/skill-router.test.ts +3 -3
  27. package/src/agents/CulturalMemory.ts +6 -6
  28. package/src/agents/DelegationTraceHooks.ts +560 -0
  29. package/src/agents/FederatedRegistryAdapter.ts +1 -1
  30. package/src/agents/NormEngine.ts +3 -8
  31. package/src/agents/OrchestratorAgent.ts +1 -1
  32. package/src/agents/TaskDelegationService.ts +5 -9
  33. package/src/agents/__tests__/AgentWalletRegistry.test.ts +5 -4
  34. package/src/agents/__tests__/CrossRealityHandoff.test.ts +9 -3
  35. package/src/agents/__tests__/DelegationTraceHooks.test.ts +390 -0
  36. package/src/agents/__tests__/TaskDelegationService.test.ts +4 -2
  37. package/src/agents/spatial-comms/Layer1RealTime.ts +36 -19
  38. package/src/agents/spatial-comms/Layer2A2A.ts +1 -3
  39. package/src/agents/spatial-comms/Layer3MCP.ts +13 -4
  40. package/src/agents/spatial-comms/ProtocolTypes.ts +5 -2
  41. package/src/agents/spatial-comms/examples/multi-agent-world-creation.ts +2 -2
  42. package/src/ai/HoloScriptGenerator.ts +2 -2
  43. package/src/ai/__tests__/PerceptionSystem.prod.test.ts +1 -1
  44. package/src/ai/__tests__/PerceptionSystem.test.ts +14 -14
  45. package/src/ai/__tests__/SteeringBehaviors.prod.test.ts +1 -1
  46. package/src/ai/index.ts +5 -1
  47. package/src/board/audit.ts +17 -6
  48. package/src/board/board-ops.ts +45 -15
  49. package/src/board/board-types.ts +94 -20
  50. package/src/delegation.ts +5 -3
  51. package/src/distributed-claimer.ts +13 -2
  52. package/src/economy/BountyManager.ts +40 -18
  53. package/src/economy/KnowledgeMarketplace.ts +27 -8
  54. package/src/economy/PaymentWebhookService.ts +0 -1
  55. package/src/economy/RevenueSplitter.ts +2 -4
  56. package/src/economy/UnifiedBudgetOptimizer.ts +8 -9
  57. package/src/economy/_core-stubs.ts +1 -1
  58. package/src/economy/x402-facilitator.ts +17 -8
  59. package/src/index.ts +16 -12
  60. package/src/knowledge/__tests__/knowledge-consolidator.test.ts +138 -89
  61. package/src/knowledge/__tests__/knowledge-store-vector.test.ts +59 -16
  62. package/src/knowledge/brain.ts +7 -7
  63. package/src/knowledge/consolidation.ts +16 -16
  64. package/src/knowledge/knowledge-consolidator.ts +60 -30
  65. package/src/knowledge/knowledge-store.ts +83 -45
  66. package/src/learning/ProceduralCompiler.ts +6 -1
  67. package/src/learning/learning/MemoryConsolidator.ts +102 -0
  68. package/src/learning/learning/MemoryScorer.ts +69 -0
  69. package/src/learning/learning/ProceduralCompiler.ts +45 -0
  70. package/src/learning/learning/SemanticClusterer.ts +66 -0
  71. package/src/llm/llm-adapter.ts +24 -10
  72. package/src/mesh/index.ts +37 -17
  73. package/src/protocol/goal-synthesizer.ts +24 -34
  74. package/src/protocol/implementations.ts +91 -22
  75. package/src/protocol/micro-phase-decomposer.ts +25 -17
  76. package/src/protocol/micro-step-decomposer.test.ts +104 -39
  77. package/src/protocol-agent.test.ts +17 -7
  78. package/src/protocol-agent.ts +45 -42
  79. package/src/self-improve/absorb-scanner.ts +9 -6
  80. package/src/self-improve/evolution-engine.ts +36 -18
  81. package/src/self-improve/framework-absorber.ts +21 -16
  82. package/src/self-improve/index.ts +2 -10
  83. package/src/self-improve/prompt-optimizer.ts +31 -19
  84. package/src/self-improve/test-generator.ts +16 -12
  85. package/src/skill-router.ts +7 -6
  86. package/src/swarm/messaging/GossipProtocol.ts +1 -1
  87. package/src/swarm/messaging/__tests__/BroadcastChannel.prod.test.ts +31 -9
  88. package/src/swarm/messaging/__tests__/GossipProtocol.prod.test.ts +21 -7
  89. package/src/swarm/messaging/__tests__/SwarmEventBus.prod.test.ts +24 -8
  90. package/src/swarm/messaging/__tests__/SwarmEventBus.test.ts +6 -2
  91. package/src/team.ts +277 -122
  92. package/src/training/scripts/generate-spatial-dataset.ts +1 -1
  93. package/src/training/training/LRScheduler.ts +377 -0
  94. package/src/training/training/QualityScoringPipeline.ts +139 -0
  95. package/src/training/training/SoftDedup.ts +461 -0
  96. package/src/training/training/SparsityMonitor.ts +685 -0
  97. package/src/training/training/SparsityMonitorTypes.ts +209 -0
  98. package/src/training/training/SpatialTrainingDataGenerator.ts +1526 -0
  99. package/src/training/training/SpatialTrainingDataTypes.ts +216 -0
  100. package/src/training/training/TrainingPipelineConfig.ts +215 -0
  101. package/src/training/training/__tests__/CorpusValidation.test.ts +87 -0
  102. package/src/training/training/__tests__/LRScheduler.test.ts +592 -0
  103. package/src/training/training/__tests__/SoftDedup.test.ts +415 -0
  104. package/src/training/training/__tests__/SparsityMonitor.test.ts +1623 -0
  105. package/src/training/training/__tests__/SpatialCorpusValidation.test.ts +72 -0
  106. package/src/training/training/__tests__/SpatialTrainingDataGenerator.test.ts +1244 -0
  107. package/src/training/training/__tests__/TrainingMonkeyIntegration.test.ts +897 -0
  108. package/src/training/training/__tests__/TrainingPipelineConfig.test.ts +202 -0
  109. package/src/training/training/__tests__/schema.test.ts +72 -0
  110. package/src/training/training/__tests__/training-constants.test.ts +106 -0
  111. package/src/training/training/__tests__/trait-mappings.test.ts +81 -0
  112. package/src/training/training/constants.ts +94 -0
  113. package/src/training/training/index.ts +17 -0
  114. package/src/training/training/schema.ts +147 -0
  115. package/src/training/training/scripts/generate-novel-use-cases-dataset.ts +272 -0
  116. package/src/training/training/scripts/generate-spatial-dataset.ts +521 -0
  117. package/src/training/training/trainingmonkey/TrainingMonkeyIntegration.ts +477 -0
  118. package/src/training/training/trainingmonkey/TrainingMonkeyTypes.ts +230 -0
  119. package/src/training/training/trainingmonkey/index.ts +26 -0
  120. package/src/training/training/trait-mappings.ts +157 -0
  121. package/src/types.ts +2 -7
  122. package/ALL-test-results.json +0 -1
  123. package/LICENSE +0 -21
  124. package/dist/AgentManifest-CB4xM-Ma.d.ts +0 -704
  125. package/dist/BehaviorTree-BrBFECv5.d.ts +0 -103
  126. package/dist/InvisibleWallet-rtRrBOA8.d.ts +0 -1732
  127. package/dist/OrchestratorAgent-Q_CbVTmO.d.ts +0 -798
  128. package/dist/agents/index.d.ts +0 -1788
  129. package/dist/agents/index.js +0 -4695
  130. package/dist/ai/index.d.ts +0 -1753
  131. package/dist/ai/index.js +0 -5244
  132. package/dist/behavior.d.ts +0 -130
  133. package/dist/behavior.js +0 -407
  134. package/dist/economy/index.d.ts +0 -747
  135. package/dist/economy/index.js +0 -3617
  136. package/dist/implementations-D9T3un9D.d.ts +0 -236
  137. package/dist/index.d.ts +0 -1729
  138. package/dist/index.js +0 -24277
  139. package/dist/learning/index.d.ts +0 -104
  140. package/dist/learning/index.js +0 -189
  141. package/dist/negotiation/index.d.ts +0 -610
  142. package/dist/negotiation/index.js +0 -931
  143. package/dist/skills/index.d.ts +0 -289
  144. package/dist/skills/index.js +0 -1079
  145. package/dist/swarm/index.d.ts +0 -2433
  146. package/dist/swarm/index.js +0 -5221
  147. package/dist/training/index.d.ts +0 -1734
  148. package/dist/training/index.js +0 -2687
  149. package/extract-failures.js +0 -10
  150. package/src/training/training/data/novel-use-cases.jsonl +0 -153
  151. package/src/training/training/data/spatial-reasoning-10k.jsonl +0 -9354
  152. package/src/types/core-stubs.d.ts +0 -113
  153. package/test-output.txt +0 -0
  154. package/test-result.json +0 -1
  155. package/tsc-errors.txt +0 -4
  156. package/tsc_output.txt +0 -0
  157. package/typescript-errors-2.txt +0 -0
  158. package/typescript-errors.txt +0 -22
  159. package/vitest-log-utf8.txt +0 -268
  160. package/vitest-log.txt +0 -0
@@ -0,0 +1,1244 @@
1
+ import { describe, it, expect, beforeEach } from 'vitest';
2
+ import {
3
+ SpatialTrainingDataGenerator,
4
+ createSpatialTrainingDataGenerator,
5
+ } from '../SpatialTrainingDataGenerator';
6
+ import type {
7
+ SpatialTrainingExample,
8
+ SpatialGeneratorConfig,
9
+ SpatialDifficulty,
10
+ SpatialRelationshipType,
11
+ SpatialTrainingJSONLEntry,
12
+ } from '../SpatialTrainingDataTypes';
13
+
14
+ // =============================================================================
15
+ // HELPERS
16
+ // =============================================================================
17
+
18
+ const FIXED_SEED = 42;
19
+
20
+ function createSeededGenerator(
21
+ overrides: Partial<SpatialGeneratorConfig> = {}
22
+ ): SpatialTrainingDataGenerator {
23
+ return new SpatialTrainingDataGenerator({
24
+ seed: FIXED_SEED,
25
+ ...overrides,
26
+ });
27
+ }
28
+
29
+ // =============================================================================
30
+ // TESTS
31
+ // =============================================================================
32
+
33
+ describe('SpatialTrainingDataGenerator', () => {
34
+ // ---------------------------------------------------------------------------
35
+ // Initialization
36
+ // ---------------------------------------------------------------------------
37
+
38
+ describe('initialization', () => {
39
+ it('should create a generator with default config', () => {
40
+ const gen = new SpatialTrainingDataGenerator();
41
+ expect(gen).toBeDefined();
42
+ });
43
+
44
+ it('should create a generator via factory function', () => {
45
+ const gen = createSpatialTrainingDataGenerator();
46
+ expect(gen).toBeDefined();
47
+ });
48
+
49
+ it('should create a generator with custom config', () => {
50
+ const gen = new SpatialTrainingDataGenerator({
51
+ examplesPerCategory: 5,
52
+ seed: 123,
53
+ positiveRatio: 0.7,
54
+ });
55
+ expect(gen).toBeDefined();
56
+ });
57
+
58
+ it('should accept all configuration options', () => {
59
+ const config: SpatialGeneratorConfig = {
60
+ examplesPerCategory: 3,
61
+ relationshipTypes: ['spatial_adjacent'],
62
+ difficultyLevels: ['basic'],
63
+ positiveRatio: 0.6,
64
+ seed: 99,
65
+ includeContext: false,
66
+ };
67
+ const gen = new SpatialTrainingDataGenerator(config);
68
+ expect(gen).toBeDefined();
69
+ });
70
+ });
71
+
72
+ // ---------------------------------------------------------------------------
73
+ // Generation: basic output structure
74
+ // ---------------------------------------------------------------------------
75
+
76
+ describe('generate()', () => {
77
+ let gen: SpatialTrainingDataGenerator;
78
+
79
+ beforeEach(() => {
80
+ gen = createSeededGenerator({ examplesPerCategory: 3 });
81
+ });
82
+
83
+ it('should generate examples', () => {
84
+ const examples = gen.generate();
85
+ expect(examples.length).toBeGreaterThan(0);
86
+ });
87
+
88
+ it('should generate expected number of examples', () => {
89
+ // 3 relationship types x 3 difficulty levels x 3 examples per category = 27
90
+ const examples = gen.generate();
91
+ expect(examples.length).toBe(27);
92
+ });
93
+
94
+ it('should generate examples with required fields', () => {
95
+ const examples = gen.generate();
96
+ for (const ex of examples) {
97
+ expect(ex.id).toBeDefined();
98
+ expect(ex.id.length).toBeGreaterThan(0);
99
+ expect(ex.instruction).toBeDefined();
100
+ expect(ex.instruction.length).toBeGreaterThan(0);
101
+ expect(ex.response).toBeDefined();
102
+ expect(ex.response.length).toBeGreaterThan(0);
103
+ expect(ex.context).toBeDefined();
104
+ expect(ex.context.length).toBeGreaterThan(0);
105
+ expect(ex.relationshipType).toBeDefined();
106
+ expect(ex.difficulty).toBeDefined();
107
+ expect(ex.tags).toBeDefined();
108
+ expect(ex.tags.length).toBeGreaterThan(0);
109
+ expect(typeof ex.isPositive).toBe('boolean');
110
+ }
111
+ });
112
+
113
+ it('should produce unique IDs for all examples', () => {
114
+ const examples = gen.generate();
115
+ const ids = examples.map((ex) => ex.id);
116
+ const uniqueIds = new Set(ids);
117
+ expect(uniqueIds.size).toBe(ids.length);
118
+ });
119
+
120
+ it('should produce valid HoloScript in context', () => {
121
+ const examples = gen.generate();
122
+ for (const ex of examples) {
123
+ expect(ex.context).toContain('composition "SpatialScene"');
124
+ expect(ex.context).toContain('{');
125
+ expect(ex.context).toContain('}');
126
+ }
127
+ });
128
+ });
129
+
130
+ // ---------------------------------------------------------------------------
131
+ // Reproducibility (seeded RNG)
132
+ // ---------------------------------------------------------------------------
133
+
134
+ describe('reproducibility', () => {
135
+ it('should produce identical output with same seed', () => {
136
+ const gen1 = createSeededGenerator({ examplesPerCategory: 5 });
137
+ const gen2 = createSeededGenerator({ examplesPerCategory: 5 });
138
+
139
+ const examples1 = gen1.generate();
140
+ const examples2 = gen2.generate();
141
+
142
+ expect(examples1.length).toBe(examples2.length);
143
+ for (let i = 0; i < examples1.length; i++) {
144
+ expect(examples1[i].instruction).toBe(examples2[i].instruction);
145
+ expect(examples1[i].response).toBe(examples2[i].response);
146
+ expect(examples1[i].context).toBe(examples2[i].context);
147
+ }
148
+ });
149
+
150
+ it('should produce different output with different seeds', () => {
151
+ const gen1 = new SpatialTrainingDataGenerator({ seed: 1, examplesPerCategory: 5 });
152
+ const gen2 = new SpatialTrainingDataGenerator({ seed: 2, examplesPerCategory: 5 });
153
+
154
+ const examples1 = gen1.generate();
155
+ const examples2 = gen2.generate();
156
+
157
+ // At least some examples should differ
158
+ let diffCount = 0;
159
+ for (let i = 0; i < Math.min(examples1.length, examples2.length); i++) {
160
+ if (examples1[i].instruction !== examples2[i].instruction) {
161
+ diffCount++;
162
+ }
163
+ }
164
+ expect(diffCount).toBeGreaterThan(0);
165
+ });
166
+
167
+ it('should allow reseed for fresh generation', () => {
168
+ const gen = createSeededGenerator({ examplesPerCategory: 3 });
169
+ const first = gen.generate();
170
+
171
+ gen.reseed(FIXED_SEED);
172
+ const second = gen.generate();
173
+
174
+ expect(first.length).toBe(second.length);
175
+ for (let i = 0; i < first.length; i++) {
176
+ expect(first[i].instruction).toBe(second[i].instruction);
177
+ }
178
+ });
179
+ });
180
+
181
+ // ---------------------------------------------------------------------------
182
+ // Relationship type coverage
183
+ // ---------------------------------------------------------------------------
184
+
185
+ describe('relationship types', () => {
186
+ it('should generate spatial_adjacent examples', () => {
187
+ const gen = createSeededGenerator({
188
+ relationshipTypes: ['spatial_adjacent'],
189
+ examplesPerCategory: 5,
190
+ });
191
+ const examples = gen.generate();
192
+
193
+ expect(examples.length).toBeGreaterThan(0);
194
+ for (const ex of examples) {
195
+ expect(ex.relationshipType).toBe('spatial_adjacent');
196
+ }
197
+ });
198
+
199
+ it('should generate spatial_contains examples', () => {
200
+ const gen = createSeededGenerator({
201
+ relationshipTypes: ['spatial_contains'],
202
+ examplesPerCategory: 5,
203
+ });
204
+ const examples = gen.generate();
205
+
206
+ expect(examples.length).toBeGreaterThan(0);
207
+ for (const ex of examples) {
208
+ expect(ex.relationshipType).toBe('spatial_contains');
209
+ }
210
+ });
211
+
212
+ it('should generate spatial_reachable examples', () => {
213
+ const gen = createSeededGenerator({
214
+ relationshipTypes: ['spatial_reachable'],
215
+ examplesPerCategory: 5,
216
+ });
217
+ const examples = gen.generate();
218
+
219
+ expect(examples.length).toBeGreaterThan(0);
220
+ for (const ex of examples) {
221
+ expect(ex.relationshipType).toBe('spatial_reachable');
222
+ }
223
+ });
224
+
225
+ it('should include spatial constraint traits in HoloScript context', () => {
226
+ const gen = createSeededGenerator({ examplesPerCategory: 5 });
227
+ const examples = gen.generate();
228
+
229
+ const adjacentExamples = examples.filter((e) => e.relationshipType === 'spatial_adjacent');
230
+ for (const ex of adjacentExamples) {
231
+ expect(ex.context).toContain('@spatial_adjacent');
232
+ }
233
+
234
+ const containsExamples = examples.filter((e) => e.relationshipType === 'spatial_contains');
235
+ for (const ex of containsExamples) {
236
+ expect(ex.context).toContain('@spatial_contains');
237
+ }
238
+
239
+ const reachableExamples = examples.filter((e) => e.relationshipType === 'spatial_reachable');
240
+ for (const ex of reachableExamples) {
241
+ expect(ex.context).toContain('@spatial_reachable');
242
+ }
243
+ });
244
+
245
+ it('should generate for specific relationship type', () => {
246
+ const gen = createSeededGenerator({ examplesPerCategory: 3 });
247
+ const examples = gen.generateForRelationship('spatial_contains');
248
+
249
+ expect(examples.length).toBe(9); // 3 difficulties x 3 examples
250
+ for (const ex of examples) {
251
+ expect(ex.relationshipType).toBe('spatial_contains');
252
+ }
253
+ });
254
+ });
255
+
256
+ // ---------------------------------------------------------------------------
257
+ // Positive / Negative examples
258
+ // ---------------------------------------------------------------------------
259
+
260
+ describe('positive and negative examples', () => {
261
+ it('should generate both positive and negative examples', () => {
262
+ const gen = createSeededGenerator({ examplesPerCategory: 20 });
263
+ const examples = gen.generate();
264
+
265
+ const positive = examples.filter((e) => e.isPositive);
266
+ const negative = examples.filter((e) => !e.isPositive);
267
+
268
+ expect(positive.length).toBeGreaterThan(0);
269
+ expect(negative.length).toBeGreaterThan(0);
270
+ });
271
+
272
+ it('should respect positiveRatio configuration', () => {
273
+ const gen = new SpatialTrainingDataGenerator({
274
+ seed: FIXED_SEED,
275
+ examplesPerCategory: 100,
276
+ relationshipTypes: ['spatial_adjacent'],
277
+ difficultyLevels: ['basic'],
278
+ positiveRatio: 0.7,
279
+ });
280
+ const examples = gen.generate();
281
+
282
+ const positiveCount = examples.filter((e) => e.isPositive).length;
283
+ const ratio = positiveCount / examples.length;
284
+
285
+ // Allow some variance due to randomness
286
+ expect(ratio).toBeGreaterThan(0.5);
287
+ expect(ratio).toBeLessThan(0.9);
288
+ });
289
+
290
+ it('should generate positive adjacent examples with objects within maxDistance', () => {
291
+ const gen = createSeededGenerator({
292
+ relationshipTypes: ['spatial_adjacent'],
293
+ examplesPerCategory: 20,
294
+ difficultyLevels: ['basic'],
295
+ positiveRatio: 1.0,
296
+ });
297
+ const examples = gen.generate();
298
+
299
+ for (const ex of examples) {
300
+ expect(ex.isPositive).toBe(true);
301
+ // Positive adjacent responses should indicate objects are close enough
302
+ expect(ex.response.toLowerCase()).toMatch(
303
+ /yes|satisf|pass|within|close|proper|no violation/i
304
+ );
305
+ }
306
+ });
307
+
308
+ it('should generate negative adjacent examples with objects beyond maxDistance', () => {
309
+ const gen = createSeededGenerator({
310
+ relationshipTypes: ['spatial_adjacent'],
311
+ examplesPerCategory: 20,
312
+ difficultyLevels: ['basic'],
313
+ positiveRatio: 0.0,
314
+ });
315
+ const examples = gen.generate();
316
+
317
+ for (const ex of examples) {
318
+ expect(ex.isPositive).toBe(false);
319
+ // Negative adjacent responses should indicate objects are too far
320
+ expect(ex.response.toLowerCase()).toMatch(
321
+ /no|exceed|fail|violat|not|outside|too far|improper|break/i
322
+ );
323
+ }
324
+ });
325
+
326
+ it('should generate positive contains examples with objects inside bounds', () => {
327
+ const gen = createSeededGenerator({
328
+ relationshipTypes: ['spatial_contains'],
329
+ examplesPerCategory: 10,
330
+ difficultyLevels: ['basic'],
331
+ positiveRatio: 1.0,
332
+ });
333
+ const examples = gen.generate();
334
+
335
+ for (const ex of examples) {
336
+ expect(ex.isPositive).toBe(true);
337
+ }
338
+ });
339
+
340
+ it('should generate negative contains examples with objects outside bounds', () => {
341
+ const gen = createSeededGenerator({
342
+ relationshipTypes: ['spatial_contains'],
343
+ examplesPerCategory: 10,
344
+ difficultyLevels: ['basic'],
345
+ positiveRatio: 0.0,
346
+ });
347
+ const examples = gen.generate();
348
+
349
+ for (const ex of examples) {
350
+ expect(ex.isPositive).toBe(false);
351
+ }
352
+ });
353
+
354
+ it('should generate positive reachable examples with clear paths', () => {
355
+ const gen = createSeededGenerator({
356
+ relationshipTypes: ['spatial_reachable'],
357
+ examplesPerCategory: 10,
358
+ difficultyLevels: ['basic'],
359
+ positiveRatio: 1.0,
360
+ });
361
+ const examples = gen.generate();
362
+
363
+ for (const ex of examples) {
364
+ expect(ex.isPositive).toBe(true);
365
+ }
366
+ });
367
+
368
+ it('should generate negative reachable examples with blocked paths', () => {
369
+ const gen = createSeededGenerator({
370
+ relationshipTypes: ['spatial_reachable'],
371
+ examplesPerCategory: 10,
372
+ difficultyLevels: ['intermediate', 'advanced'],
373
+ positiveRatio: 0.0,
374
+ });
375
+ const examples = gen.generate();
376
+
377
+ for (const ex of examples) {
378
+ expect(ex.isPositive).toBe(false);
379
+ }
380
+ });
381
+
382
+ it('should tag positive and negative examples correctly', () => {
383
+ const gen = createSeededGenerator({ examplesPerCategory: 10 });
384
+ const examples = gen.generate();
385
+
386
+ for (const ex of examples) {
387
+ if (ex.isPositive) {
388
+ expect(ex.tags).toContain('positive');
389
+ expect(ex.tags).not.toContain('negative');
390
+ } else {
391
+ expect(ex.tags).toContain('negative');
392
+ expect(ex.tags).not.toContain('positive');
393
+ }
394
+ }
395
+ });
396
+ });
397
+
398
+ // ---------------------------------------------------------------------------
399
+ // Difficulty levels
400
+ // ---------------------------------------------------------------------------
401
+
402
+ describe('difficulty levels', () => {
403
+ it('should generate basic difficulty examples (2 objects)', () => {
404
+ const gen = createSeededGenerator({
405
+ difficultyLevels: ['basic'],
406
+ examplesPerCategory: 5,
407
+ });
408
+ const examples = gen.generate();
409
+
410
+ for (const ex of examples) {
411
+ expect(ex.difficulty).toBe('basic');
412
+ }
413
+ });
414
+
415
+ it('should generate intermediate difficulty examples (3-5 objects)', () => {
416
+ const gen = createSeededGenerator({
417
+ difficultyLevels: ['intermediate'],
418
+ examplesPerCategory: 5,
419
+ });
420
+ const examples = gen.generate();
421
+
422
+ for (const ex of examples) {
423
+ expect(ex.difficulty).toBe('intermediate');
424
+ }
425
+ });
426
+
427
+ it('should generate advanced difficulty examples (6+ objects)', () => {
428
+ const gen = createSeededGenerator({
429
+ difficultyLevels: ['advanced'],
430
+ examplesPerCategory: 5,
431
+ });
432
+ const examples = gen.generate();
433
+
434
+ for (const ex of examples) {
435
+ expect(ex.difficulty).toBe('advanced');
436
+ }
437
+ });
438
+
439
+ it('should generate for specific difficulty level', () => {
440
+ const gen = createSeededGenerator({ examplesPerCategory: 3 });
441
+ const examples = gen.generateForDifficulty('intermediate');
442
+
443
+ expect(examples.length).toBe(9); // 3 rel types x 3 examples
444
+ for (const ex of examples) {
445
+ expect(ex.difficulty).toBe('intermediate');
446
+ }
447
+ });
448
+
449
+ it('should include more objects in advanced scenes', () => {
450
+ const gen = createSeededGenerator({
451
+ difficultyLevels: ['advanced'],
452
+ relationshipTypes: ['spatial_adjacent'],
453
+ examplesPerCategory: 5,
454
+ });
455
+ const examples = gen.generate();
456
+
457
+ for (const ex of examples) {
458
+ // Advanced scenes should have more object declarations
459
+ const objectMatches = ex.context.match(/object\s+"/g);
460
+ expect(objectMatches).toBeDefined();
461
+ expect(objectMatches!.length).toBeGreaterThanOrEqual(4);
462
+ }
463
+ });
464
+
465
+ it('should have basic scenes with minimal objects', () => {
466
+ const gen = createSeededGenerator({
467
+ difficultyLevels: ['basic'],
468
+ relationshipTypes: ['spatial_adjacent'],
469
+ examplesPerCategory: 5,
470
+ });
471
+ const examples = gen.generate();
472
+
473
+ for (const ex of examples) {
474
+ const objectMatches = ex.context.match(/object\s+"/g);
475
+ expect(objectMatches).toBeDefined();
476
+ expect(objectMatches!.length).toBe(2);
477
+ }
478
+ });
479
+
480
+ it('should include nested containment in advanced contains scenes', () => {
481
+ const gen = createSeededGenerator({
482
+ difficultyLevels: ['advanced'],
483
+ relationshipTypes: ['spatial_contains'],
484
+ examplesPerCategory: 5,
485
+ });
486
+ const examples = gen.generate();
487
+
488
+ // At least some advanced contains scenes should have nested zones
489
+ const scenesWithMultipleZones = examples.filter((ex) => {
490
+ const zoneMatches = ex.context.match(/zone\s+"/g);
491
+ return zoneMatches && zoneMatches.length >= 2;
492
+ });
493
+ expect(scenesWithMultipleZones.length).toBeGreaterThan(0);
494
+ });
495
+
496
+ it('should include obstacles in advanced reachable scenes', () => {
497
+ const gen = createSeededGenerator({
498
+ difficultyLevels: ['advanced'],
499
+ relationshipTypes: ['spatial_reachable'],
500
+ examplesPerCategory: 5,
501
+ });
502
+ const examples = gen.generate();
503
+
504
+ // Advanced reachable scenes should have obstacle objects
505
+ const scenesWithObstacles = examples.filter((ex) => {
506
+ return ex.context.includes('@static') || ex.context.includes('@collidable');
507
+ });
508
+ expect(scenesWithObstacles.length).toBeGreaterThan(0);
509
+ });
510
+ });
511
+
512
+ // ---------------------------------------------------------------------------
513
+ // Template diversity (G.002 mandate: 10+ templates)
514
+ // ---------------------------------------------------------------------------
515
+
516
+ describe('template diversity (G.002 compliance)', () => {
517
+ it('should use 10+ templates for spatial_adjacent', () => {
518
+ const gen = new SpatialTrainingDataGenerator({
519
+ seed: FIXED_SEED,
520
+ examplesPerCategory: 50,
521
+ relationshipTypes: ['spatial_adjacent'],
522
+ difficultyLevels: ['basic'],
523
+ });
524
+ const examples = gen.generate();
525
+
526
+ const templateTags = new Set<string>();
527
+ for (const ex of examples) {
528
+ const tplTag = ex.tags.find((t) => t.startsWith('template:'));
529
+ if (tplTag) templateTags.add(tplTag);
530
+ }
531
+
532
+ expect(templateTags.size).toBeGreaterThanOrEqual(10);
533
+ });
534
+
535
+ it('should use 10+ templates for spatial_contains', () => {
536
+ const gen = new SpatialTrainingDataGenerator({
537
+ seed: FIXED_SEED,
538
+ examplesPerCategory: 50,
539
+ relationshipTypes: ['spatial_contains'],
540
+ difficultyLevels: ['basic'],
541
+ });
542
+ const examples = gen.generate();
543
+
544
+ const templateTags = new Set<string>();
545
+ for (const ex of examples) {
546
+ const tplTag = ex.tags.find((t) => t.startsWith('template:'));
547
+ if (tplTag) templateTags.add(tplTag);
548
+ }
549
+
550
+ expect(templateTags.size).toBeGreaterThanOrEqual(10);
551
+ });
552
+
553
+ it('should use 10+ templates for spatial_reachable', () => {
554
+ const gen = new SpatialTrainingDataGenerator({
555
+ seed: FIXED_SEED,
556
+ examplesPerCategory: 50,
557
+ relationshipTypes: ['spatial_reachable'],
558
+ difficultyLevels: ['basic'],
559
+ });
560
+ const examples = gen.generate();
561
+
562
+ const templateTags = new Set<string>();
563
+ for (const ex of examples) {
564
+ const tplTag = ex.tags.find((t) => t.startsWith('template:'));
565
+ if (tplTag) templateTags.add(tplTag);
566
+ }
567
+
568
+ expect(templateTags.size).toBeGreaterThanOrEqual(10);
569
+ });
570
+
571
+ it('should produce diverse instruction texts (no single template dominates)', () => {
572
+ const gen = new SpatialTrainingDataGenerator({
573
+ seed: FIXED_SEED,
574
+ examplesPerCategory: 30,
575
+ relationshipTypes: ['spatial_adjacent'],
576
+ difficultyLevels: ['basic'],
577
+ });
578
+ const examples = gen.generate();
579
+
580
+ // Count template usage
581
+ const templateCounts = new Map<string, number>();
582
+ for (const ex of examples) {
583
+ const tplTag = ex.tags.find((t) => t.startsWith('template:')) ?? 'unknown';
584
+ templateCounts.set(tplTag, (templateCounts.get(tplTag) ?? 0) + 1);
585
+ }
586
+
587
+ // No single template should account for more than 40% of examples
588
+ for (const [, count] of templateCounts) {
589
+ expect(count / examples.length).toBeLessThan(0.4);
590
+ }
591
+ });
592
+ });
593
+
594
+ // ---------------------------------------------------------------------------
595
+ // HoloScript source generation
596
+ // ---------------------------------------------------------------------------
597
+
598
+ describe('HoloScript source generation', () => {
599
+ it('should generate valid composition blocks', () => {
600
+ const gen = createSeededGenerator({ examplesPerCategory: 5 });
601
+ const examples = gen.generate();
602
+
603
+ for (const ex of examples) {
604
+ expect(ex.context).toMatch(/^composition\s+"SpatialScene"\s+\{/);
605
+ expect(ex.context.endsWith('}')).toBe(true);
606
+ }
607
+ });
608
+
609
+ it('should include object positions in HoloScript', () => {
610
+ const gen = createSeededGenerator({ examplesPerCategory: 5 });
611
+ const examples = gen.generate();
612
+
613
+ for (const ex of examples) {
614
+ expect(ex.context).toMatch(/position:\s+\[[\d\-.,\s]+\]/);
615
+ }
616
+ });
617
+
618
+ it('should include geometry types in object blocks', () => {
619
+ const gen = createSeededGenerator({
620
+ examplesPerCategory: 5,
621
+ relationshipTypes: ['spatial_adjacent'],
622
+ });
623
+ const examples = gen.generate();
624
+
625
+ for (const ex of examples) {
626
+ expect(ex.context).toMatch(/geometry:\s+"\w+"/);
627
+ }
628
+ });
629
+
630
+ it('should include spatial_adjacent trait with parameters', () => {
631
+ const gen = createSeededGenerator({
632
+ examplesPerCategory: 5,
633
+ relationshipTypes: ['spatial_adjacent'],
634
+ });
635
+ const examples = gen.generate();
636
+
637
+ for (const ex of examples) {
638
+ expect(ex.context).toMatch(
639
+ /@spatial_adjacent\(target:\s+"[^"]+",\s+maxDistance:\s+[\d.]+m\)/
640
+ );
641
+ }
642
+ });
643
+
644
+ it('should include spatial_contains trait with parameters', () => {
645
+ const gen = createSeededGenerator({
646
+ examplesPerCategory: 5,
647
+ relationshipTypes: ['spatial_contains'],
648
+ });
649
+ const examples = gen.generate();
650
+
651
+ for (const ex of examples) {
652
+ expect(ex.context).toMatch(/@spatial_contains\(target:\s+"[^"]+"/);
653
+ }
654
+ });
655
+
656
+ it('should include spatial_reachable trait with parameters', () => {
657
+ const gen = createSeededGenerator({
658
+ examplesPerCategory: 5,
659
+ relationshipTypes: ['spatial_reachable'],
660
+ });
661
+ const examples = gen.generate();
662
+
663
+ for (const ex of examples) {
664
+ expect(ex.context).toMatch(/@spatial_reachable\(target:\s+"[^"]+"/);
665
+ }
666
+ });
667
+
668
+ it('should include zone blocks for containment scenes', () => {
669
+ const gen = createSeededGenerator({
670
+ examplesPerCategory: 5,
671
+ relationshipTypes: ['spatial_contains'],
672
+ });
673
+ const examples = gen.generate();
674
+
675
+ for (const ex of examples) {
676
+ expect(ex.context).toContain('zone "');
677
+ expect(ex.context).toContain('shape: "box"');
678
+ expect(ex.context).toContain('size:');
679
+ }
680
+ });
681
+
682
+ it('should include obstacle markers in reachable scenes', () => {
683
+ const gen = createSeededGenerator({
684
+ examplesPerCategory: 10,
685
+ relationshipTypes: ['spatial_reachable'],
686
+ difficultyLevels: ['intermediate', 'advanced'],
687
+ });
688
+ const examples = gen.generate();
689
+
690
+ const withObstacles = examples.filter(
691
+ (ex) => ex.context.includes('@static') || ex.context.includes('@collidable')
692
+ );
693
+ expect(withObstacles.length).toBeGreaterThan(0);
694
+ });
695
+ });
696
+
697
+ // ---------------------------------------------------------------------------
698
+ // Scene generation
699
+ // ---------------------------------------------------------------------------
700
+
701
+ describe('scene generation', () => {
702
+ it('should generate adjacent scenes', () => {
703
+ const gen = createSeededGenerator();
704
+ const scene = gen.generateScene('spatial_adjacent', 'basic', true);
705
+
706
+ expect(scene.name).toContain('Adjacent');
707
+ expect(scene.objects.length).toBe(2);
708
+ expect(scene.relationships.length).toBe(1);
709
+ expect(scene.relationships[0].type).toBe('spatial_adjacent');
710
+ expect(scene.difficulty).toBe('basic');
711
+ });
712
+
713
+ it('should generate contains scenes', () => {
714
+ const gen = createSeededGenerator();
715
+ const scene = gen.generateScene('spatial_contains', 'basic', true);
716
+
717
+ expect(scene.name).toContain('Contains');
718
+ expect(scene.objects.length).toBeGreaterThanOrEqual(2);
719
+ expect(scene.relationships.length).toBe(1);
720
+ expect(scene.relationships[0].type).toBe('spatial_contains');
721
+ });
722
+
723
+ it('should generate reachable scenes', () => {
724
+ const gen = createSeededGenerator();
725
+ const scene = gen.generateScene('spatial_reachable', 'basic', true);
726
+
727
+ expect(scene.name).toContain('Reachable');
728
+ expect(scene.objects.length).toBeGreaterThanOrEqual(2);
729
+ expect(scene.relationships.length).toBe(1);
730
+ expect(scene.relationships[0].type).toBe('spatial_reachable');
731
+ });
732
+
733
+ it('should include HoloScript source in scene', () => {
734
+ const gen = createSeededGenerator();
735
+ const scene = gen.generateScene('spatial_adjacent', 'basic', true);
736
+
737
+ expect(scene.holoScriptSource).toBeDefined();
738
+ expect(scene.holoScriptSource.length).toBeGreaterThan(0);
739
+ expect(scene.holoScriptSource).toContain('composition');
740
+ });
741
+
742
+ it('should generate positive adjacent scenes with satisfied constraint', () => {
743
+ const gen = createSeededGenerator();
744
+ const scene = gen.generateScene('spatial_adjacent', 'basic', true);
745
+
746
+ expect(scene.relationships[0].satisfied).toBe(true);
747
+ });
748
+
749
+ it('should generate negative adjacent scenes with violated constraint', () => {
750
+ const gen = createSeededGenerator();
751
+ const scene = gen.generateScene('spatial_adjacent', 'basic', false);
752
+
753
+ expect(scene.relationships[0].satisfied).toBe(false);
754
+ });
755
+
756
+ it('should generate positive contains scenes with object inside bounds', () => {
757
+ const gen = createSeededGenerator();
758
+ const scene = gen.generateScene('spatial_contains', 'basic', true);
759
+
760
+ expect(scene.relationships[0].satisfied).toBe(true);
761
+ });
762
+
763
+ it('should generate negative contains scenes with object outside bounds', () => {
764
+ const gen = createSeededGenerator();
765
+ const scene = gen.generateScene('spatial_contains', 'basic', false);
766
+
767
+ expect(scene.relationships[0].satisfied).toBe(false);
768
+ });
769
+ });
770
+
771
+ // ---------------------------------------------------------------------------
772
+ // JSONL export
773
+ // ---------------------------------------------------------------------------
774
+
775
+ describe('JSONL export', () => {
776
+ it('should export valid JSONL', () => {
777
+ const gen = createSeededGenerator({ examplesPerCategory: 3 });
778
+ const examples = gen.generate();
779
+ const jsonl = gen.exportJSONL(examples);
780
+
781
+ expect(jsonl).toBeDefined();
782
+ expect(jsonl.length).toBeGreaterThan(0);
783
+
784
+ const lines = jsonl.split('\n');
785
+ expect(lines.length).toBe(examples.length);
786
+
787
+ for (const line of lines) {
788
+ expect(() => JSON.parse(line)).not.toThrow();
789
+ }
790
+ });
791
+
792
+ it('should include instruction and response fields in each JSONL entry', () => {
793
+ const gen = createSeededGenerator({ examplesPerCategory: 3 });
794
+ const examples = gen.generate();
795
+ const jsonl = gen.exportJSONL(examples);
796
+
797
+ const lines = jsonl.split('\n');
798
+ for (const line of lines) {
799
+ const entry: SpatialTrainingJSONLEntry = JSON.parse(line);
800
+ expect(entry.instruction).toBeDefined();
801
+ expect(entry.instruction.length).toBeGreaterThan(0);
802
+ expect(entry.response).toBeDefined();
803
+ expect(entry.response.length).toBeGreaterThan(0);
804
+ }
805
+ });
806
+
807
+ it('should include metadata in JSONL entries', () => {
808
+ const gen = createSeededGenerator({ examplesPerCategory: 3 });
809
+ const examples = gen.generate();
810
+ const jsonl = gen.exportJSONL(examples);
811
+
812
+ const lines = jsonl.split('\n');
813
+ for (const line of lines) {
814
+ const entry: SpatialTrainingJSONLEntry = JSON.parse(line);
815
+ expect(entry.metadata).toBeDefined();
816
+ expect(entry.metadata.id).toBeDefined();
817
+ expect(entry.metadata.relationship_type).toBeDefined();
818
+ expect(typeof entry.metadata.is_positive).toBe('boolean');
819
+ expect(entry.metadata.difficulty).toBeDefined();
820
+ expect(entry.metadata.tags).toBeDefined();
821
+ }
822
+ });
823
+
824
+ it('should include HoloScript context when includeContext is true', () => {
825
+ const gen = new SpatialTrainingDataGenerator({
826
+ seed: FIXED_SEED,
827
+ examplesPerCategory: 3,
828
+ includeContext: true,
829
+ });
830
+ const examples = gen.generate();
831
+ const jsonl = gen.exportJSONL(examples);
832
+
833
+ const lines = jsonl.split('\n');
834
+ for (const line of lines) {
835
+ const entry: SpatialTrainingJSONLEntry = JSON.parse(line);
836
+ expect(entry.instruction).toContain('HoloScript Scene:');
837
+ expect(entry.instruction).toContain('```holoscript');
838
+ }
839
+ });
840
+
841
+ it('should exclude HoloScript context when includeContext is false', () => {
842
+ const gen = new SpatialTrainingDataGenerator({
843
+ seed: FIXED_SEED,
844
+ examplesPerCategory: 3,
845
+ includeContext: false,
846
+ });
847
+ const examples = gen.generate();
848
+ const jsonl = gen.exportJSONL(examples);
849
+
850
+ const lines = jsonl.split('\n');
851
+ for (const line of lines) {
852
+ const entry: SpatialTrainingJSONLEntry = JSON.parse(line);
853
+ expect(entry.instruction).not.toContain('HoloScript Scene:');
854
+ }
855
+ });
856
+ });
857
+
858
+ // ---------------------------------------------------------------------------
859
+ // JSON export
860
+ // ---------------------------------------------------------------------------
861
+
862
+ describe('JSON export', () => {
863
+ it('should export valid JSON', () => {
864
+ const gen = createSeededGenerator({ examplesPerCategory: 3 });
865
+ const examples = gen.generate();
866
+ const json = gen.exportJSON(examples);
867
+
868
+ expect(json).toBeDefined();
869
+ const parsed = JSON.parse(json);
870
+ expect(Array.isArray(parsed)).toBe(true);
871
+ expect(parsed.length).toBe(examples.length);
872
+ });
873
+
874
+ it('should preserve all example fields in JSON export', () => {
875
+ const gen = createSeededGenerator({ examplesPerCategory: 3 });
876
+ const examples = gen.generate();
877
+ const json = gen.exportJSON(examples);
878
+ const parsed: SpatialTrainingExample[] = JSON.parse(json);
879
+
880
+ for (let i = 0; i < examples.length; i++) {
881
+ expect(parsed[i].id).toBe(examples[i].id);
882
+ expect(parsed[i].instruction).toBe(examples[i].instruction);
883
+ expect(parsed[i].response).toBe(examples[i].response);
884
+ expect(parsed[i].relationshipType).toBe(examples[i].relationshipType);
885
+ expect(parsed[i].isPositive).toBe(examples[i].isPositive);
886
+ expect(parsed[i].difficulty).toBe(examples[i].difficulty);
887
+ }
888
+ });
889
+ });
890
+
891
+ // ---------------------------------------------------------------------------
892
+ // Statistics
893
+ // ---------------------------------------------------------------------------
894
+
895
+ describe('getStats()', () => {
896
+ it('should return correct total count', () => {
897
+ const gen = createSeededGenerator({ examplesPerCategory: 3 });
898
+ const examples = gen.generate();
899
+ const stats = gen.getStats(examples);
900
+
901
+ expect(stats.totalExamples).toBe(examples.length);
902
+ });
903
+
904
+ it('should break down by relationship type', () => {
905
+ const gen = createSeededGenerator({ examplesPerCategory: 3 });
906
+ const examples = gen.generate();
907
+ const stats = gen.getStats(examples);
908
+
909
+ expect(stats.byRelationship.spatial_adjacent).toBeGreaterThan(0);
910
+ expect(stats.byRelationship.spatial_contains).toBeGreaterThan(0);
911
+ expect(stats.byRelationship.spatial_reachable).toBeGreaterThan(0);
912
+
913
+ const relTotal =
914
+ stats.byRelationship.spatial_adjacent +
915
+ stats.byRelationship.spatial_contains +
916
+ stats.byRelationship.spatial_reachable;
917
+ expect(relTotal).toBe(stats.totalExamples);
918
+ });
919
+
920
+ it('should break down by difficulty', () => {
921
+ const gen = createSeededGenerator({ examplesPerCategory: 3 });
922
+ const examples = gen.generate();
923
+ const stats = gen.getStats(examples);
924
+
925
+ expect(stats.byDifficulty.basic).toBeGreaterThan(0);
926
+ expect(stats.byDifficulty.intermediate).toBeGreaterThan(0);
927
+ expect(stats.byDifficulty.advanced).toBeGreaterThan(0);
928
+
929
+ const diffTotal =
930
+ stats.byDifficulty.basic + stats.byDifficulty.intermediate + stats.byDifficulty.advanced;
931
+ expect(diffTotal).toBe(stats.totalExamples);
932
+ });
933
+
934
+ it('should count positive and negative examples', () => {
935
+ const gen = createSeededGenerator({ examplesPerCategory: 10 });
936
+ const examples = gen.generate();
937
+ const stats = gen.getStats(examples);
938
+
939
+ expect(stats.positiveCount + stats.negativeCount).toBe(stats.totalExamples);
940
+ expect(stats.positiveCount).toBeGreaterThan(0);
941
+ expect(stats.negativeCount).toBeGreaterThan(0);
942
+ });
943
+
944
+ it('should track unique templates used', () => {
945
+ const gen = createSeededGenerator({ examplesPerCategory: 20 });
946
+ const examples = gen.generate();
947
+ const stats = gen.getStats(examples);
948
+
949
+ expect(stats.uniqueTemplatesUsed).toBeGreaterThan(0);
950
+ });
951
+ });
952
+
953
+ // ---------------------------------------------------------------------------
954
+ // Randomized scene parameters
955
+ // ---------------------------------------------------------------------------
956
+
957
+ describe('randomized scene parameters', () => {
958
+ it('should randomize object positions', () => {
959
+ const gen = new SpatialTrainingDataGenerator({
960
+ seed: FIXED_SEED,
961
+ examplesPerCategory: 10,
962
+ relationshipTypes: ['spatial_adjacent'],
963
+ difficultyLevels: ['basic'],
964
+ });
965
+ const examples = gen.generate();
966
+
967
+ // Extract positions from HoloScript source
968
+ const positions = new Set<string>();
969
+ for (const ex of examples) {
970
+ const matches = ex.context.match(/position:\s+\[([^\]]+)\]/g);
971
+ if (matches) {
972
+ for (const m of matches) {
973
+ positions.add(m);
974
+ }
975
+ }
976
+ }
977
+
978
+ // Should have diverse positions (not all the same)
979
+ expect(positions.size).toBeGreaterThan(5);
980
+ });
981
+
982
+ it('should randomize object scales', () => {
983
+ const gen = new SpatialTrainingDataGenerator({
984
+ seed: FIXED_SEED,
985
+ examplesPerCategory: 10,
986
+ relationshipTypes: ['spatial_adjacent'],
987
+ difficultyLevels: ['intermediate'],
988
+ });
989
+ const examples = gen.generate();
990
+
991
+ const scales = new Set<string>();
992
+ for (const ex of examples) {
993
+ const matches = ex.context.match(/scale:\s+\[([^\]]+)\]/g);
994
+ if (matches) {
995
+ for (const m of matches) {
996
+ scales.add(m);
997
+ }
998
+ }
999
+ }
1000
+
1001
+ expect(scales.size).toBeGreaterThan(3);
1002
+ });
1003
+
1004
+ it('should randomize maxDistance for adjacent constraints', () => {
1005
+ const gen = new SpatialTrainingDataGenerator({
1006
+ seed: FIXED_SEED,
1007
+ examplesPerCategory: 10,
1008
+ relationshipTypes: ['spatial_adjacent'],
1009
+ difficultyLevels: ['basic'],
1010
+ });
1011
+ const examples = gen.generate();
1012
+
1013
+ const distances = new Set<string>();
1014
+ for (const ex of examples) {
1015
+ const match = ex.context.match(/maxDistance:\s+([\d.]+)m/);
1016
+ if (match) {
1017
+ distances.add(match[1]);
1018
+ }
1019
+ }
1020
+
1021
+ expect(distances.size).toBeGreaterThan(3);
1022
+ });
1023
+
1024
+ it('should randomize container sizes for contains constraints', () => {
1025
+ const gen = new SpatialTrainingDataGenerator({
1026
+ seed: FIXED_SEED,
1027
+ examplesPerCategory: 10,
1028
+ relationshipTypes: ['spatial_contains'],
1029
+ difficultyLevels: ['basic'],
1030
+ });
1031
+ const examples = gen.generate();
1032
+
1033
+ const sizes = new Set<string>();
1034
+ for (const ex of examples) {
1035
+ const match = ex.context.match(/size:\s+\[([^\]]+)\]/);
1036
+ if (match) {
1037
+ sizes.add(match[1]);
1038
+ }
1039
+ }
1040
+
1041
+ expect(sizes.size).toBeGreaterThan(3);
1042
+ });
1043
+
1044
+ it('should use diverse object names', () => {
1045
+ const gen = new SpatialTrainingDataGenerator({
1046
+ seed: FIXED_SEED,
1047
+ examplesPerCategory: 10,
1048
+ relationshipTypes: ['spatial_adjacent'],
1049
+ difficultyLevels: ['basic'],
1050
+ });
1051
+ const examples = gen.generate();
1052
+
1053
+ const names = new Set<string>();
1054
+ for (const ex of examples) {
1055
+ const matches = ex.context.match(/object\s+"([^"]+)"/g);
1056
+ if (matches) {
1057
+ for (const m of matches) {
1058
+ names.add(m);
1059
+ }
1060
+ }
1061
+ }
1062
+
1063
+ expect(names.size).toBeGreaterThan(5);
1064
+ });
1065
+
1066
+ it('should use diverse geometry types', () => {
1067
+ const gen = new SpatialTrainingDataGenerator({
1068
+ seed: FIXED_SEED,
1069
+ examplesPerCategory: 20,
1070
+ relationshipTypes: ['spatial_adjacent'],
1071
+ difficultyLevels: ['basic'],
1072
+ });
1073
+ const examples = gen.generate();
1074
+
1075
+ const geoTypes = new Set<string>();
1076
+ for (const ex of examples) {
1077
+ const matches = ex.context.match(/geometry:\s+"(\w+)"/g);
1078
+ if (matches) {
1079
+ for (const m of matches) {
1080
+ geoTypes.add(m);
1081
+ }
1082
+ }
1083
+ }
1084
+
1085
+ expect(geoTypes.size).toBeGreaterThan(3);
1086
+ });
1087
+ });
1088
+
1089
+ // ---------------------------------------------------------------------------
1090
+ // Edge cases
1091
+ // ---------------------------------------------------------------------------
1092
+
1093
+ describe('edge cases', () => {
1094
+ it('should handle single relationship type', () => {
1095
+ const gen = createSeededGenerator({
1096
+ relationshipTypes: ['spatial_adjacent'],
1097
+ examplesPerCategory: 3,
1098
+ });
1099
+ const examples = gen.generate();
1100
+
1101
+ expect(examples.length).toBe(9); // 1 type x 3 difficulties x 3 examples
1102
+ for (const ex of examples) {
1103
+ expect(ex.relationshipType).toBe('spatial_adjacent');
1104
+ }
1105
+ });
1106
+
1107
+ it('should handle single difficulty level', () => {
1108
+ const gen = createSeededGenerator({
1109
+ difficultyLevels: ['basic'],
1110
+ examplesPerCategory: 3,
1111
+ });
1112
+ const examples = gen.generate();
1113
+
1114
+ expect(examples.length).toBe(9); // 3 types x 1 difficulty x 3 examples
1115
+ for (const ex of examples) {
1116
+ expect(ex.difficulty).toBe('basic');
1117
+ }
1118
+ });
1119
+
1120
+ it('should handle examplesPerCategory = 1', () => {
1121
+ const gen = createSeededGenerator({ examplesPerCategory: 1 });
1122
+ const examples = gen.generate();
1123
+
1124
+ expect(examples.length).toBe(9); // 3 types x 3 difficulties x 1
1125
+ });
1126
+
1127
+ it('should handle positiveRatio = 1.0 (all positive)', () => {
1128
+ const gen = createSeededGenerator({
1129
+ positiveRatio: 1.0,
1130
+ examplesPerCategory: 10,
1131
+ });
1132
+ const examples = gen.generate();
1133
+
1134
+ for (const ex of examples) {
1135
+ expect(ex.isPositive).toBe(true);
1136
+ }
1137
+ });
1138
+
1139
+ it('should handle positiveRatio = 0.0 (all negative)', () => {
1140
+ const gen = createSeededGenerator({
1141
+ positiveRatio: 0.0,
1142
+ examplesPerCategory: 10,
1143
+ });
1144
+ const examples = gen.generate();
1145
+
1146
+ for (const ex of examples) {
1147
+ expect(ex.isPositive).toBe(false);
1148
+ }
1149
+ });
1150
+
1151
+ it('should handle large generation counts', () => {
1152
+ const gen = createSeededGenerator({
1153
+ examplesPerCategory: 100,
1154
+ relationshipTypes: ['spatial_adjacent'],
1155
+ difficultyLevels: ['basic'],
1156
+ });
1157
+ const examples = gen.generate();
1158
+
1159
+ expect(examples.length).toBe(100);
1160
+ // All IDs should still be unique
1161
+ const ids = new Set(examples.map((e) => e.id));
1162
+ expect(ids.size).toBe(100);
1163
+ });
1164
+
1165
+ it('should not crash with zero examples per category', () => {
1166
+ const gen = createSeededGenerator({ examplesPerCategory: 0 });
1167
+ const examples = gen.generate();
1168
+
1169
+ expect(examples.length).toBe(0);
1170
+ });
1171
+ });
1172
+
1173
+ // ---------------------------------------------------------------------------
1174
+ // Integration: full pipeline
1175
+ // ---------------------------------------------------------------------------
1176
+
1177
+ describe('full pipeline integration', () => {
1178
+ it('should generate, export, and parse a complete JSONL dataset', () => {
1179
+ const gen = new SpatialTrainingDataGenerator({
1180
+ seed: 12345,
1181
+ examplesPerCategory: 5,
1182
+ relationshipTypes: ['spatial_adjacent', 'spatial_contains', 'spatial_reachable'],
1183
+ difficultyLevels: ['basic', 'intermediate', 'advanced'],
1184
+ positiveRatio: 0.5,
1185
+ includeContext: true,
1186
+ });
1187
+
1188
+ // Generate
1189
+ const examples = gen.generate();
1190
+ expect(examples.length).toBe(45); // 3 types x 3 difficulties x 5 examples
1191
+
1192
+ // Export JSONL
1193
+ const jsonl = gen.exportJSONL(examples);
1194
+ const lines = jsonl.split('\n');
1195
+ expect(lines.length).toBe(45);
1196
+
1197
+ // Verify each line is valid
1198
+ for (const line of lines) {
1199
+ const entry = JSON.parse(line) as SpatialTrainingJSONLEntry;
1200
+ expect(entry.instruction.length).toBeGreaterThan(10);
1201
+ expect(entry.response.length).toBeGreaterThan(10);
1202
+ expect(['spatial_adjacent', 'spatial_contains', 'spatial_reachable']).toContain(
1203
+ entry.metadata.relationship_type
1204
+ );
1205
+ expect(['basic', 'intermediate', 'advanced']).toContain(entry.metadata.difficulty);
1206
+ }
1207
+
1208
+ // Get stats
1209
+ const stats = gen.getStats(examples);
1210
+ expect(stats.totalExamples).toBe(45);
1211
+ expect(stats.byRelationship.spatial_adjacent).toBe(15);
1212
+ expect(stats.byRelationship.spatial_contains).toBe(15);
1213
+ expect(stats.byRelationship.spatial_reachable).toBe(15);
1214
+ expect(stats.byDifficulty.basic).toBe(15);
1215
+ expect(stats.byDifficulty.intermediate).toBe(15);
1216
+ expect(stats.byDifficulty.advanced).toBe(15);
1217
+ });
1218
+
1219
+ it('should produce training-quality instruction-response pairs', () => {
1220
+ const gen = createSeededGenerator({
1221
+ examplesPerCategory: 5,
1222
+ includeContext: true,
1223
+ });
1224
+
1225
+ const examples = gen.generate();
1226
+
1227
+ for (const ex of examples) {
1228
+ // Instructions should be questions or directives
1229
+ expect(ex.instruction.length).toBeGreaterThan(10);
1230
+ // Responses should be informative answers
1231
+ expect(ex.response.length).toBeGreaterThan(10);
1232
+ // Context should be valid HoloScript
1233
+ expect(ex.context).toContain('composition');
1234
+
1235
+ // Responses for positive examples should indicate success/satisfaction
1236
+ if (ex.isPositive) {
1237
+ expect(ex.response.toLowerCase()).toMatch(
1238
+ /yes|satisf|pass|within|clear|contain|inside|proper|reach|no violation|maintain|unobstructed|unblocked|direct|measur/i
1239
+ );
1240
+ }
1241
+ }
1242
+ });
1243
+ });
1244
+ });