gridstamp 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. package/.cursorrules +74 -0
  2. package/CLAUDE.md +61 -0
  3. package/LICENSE +190 -0
  4. package/README.md +107 -0
  5. package/dist/index.js +194 -0
  6. package/package.json +84 -0
  7. package/src/antispoofing/detector.ts +509 -0
  8. package/src/antispoofing/index.ts +7 -0
  9. package/src/gamification/badges.ts +429 -0
  10. package/src/gamification/fleet-leaderboard.ts +293 -0
  11. package/src/gamification/index.ts +44 -0
  12. package/src/gamification/streaks.ts +243 -0
  13. package/src/gamification/trust-tiers.ts +393 -0
  14. package/src/gamification/zone-mastery.ts +256 -0
  15. package/src/index.ts +341 -0
  16. package/src/memory/index.ts +9 -0
  17. package/src/memory/place-cells.ts +279 -0
  18. package/src/memory/spatial-memory.ts +375 -0
  19. package/src/navigation/index.ts +1 -0
  20. package/src/navigation/pathfinding.ts +403 -0
  21. package/src/perception/camera.ts +249 -0
  22. package/src/perception/index.ts +2 -0
  23. package/src/types/index.ts +416 -0
  24. package/src/utils/crypto.ts +94 -0
  25. package/src/utils/index.ts +2 -0
  26. package/src/utils/math.ts +204 -0
  27. package/src/verification/index.ts +9 -0
  28. package/src/verification/spatial-proof.ts +442 -0
  29. package/tests/antispoofing/detector.test.ts +196 -0
  30. package/tests/gamification/badges.test.ts +163 -0
  31. package/tests/gamification/fleet-leaderboard.test.ts +181 -0
  32. package/tests/gamification/streaks.test.ts +158 -0
  33. package/tests/gamification/trust-tiers.test.ts +165 -0
  34. package/tests/gamification/zone-mastery.test.ts +143 -0
  35. package/tests/memory/place-cells.test.ts +128 -0
  36. package/tests/stress/load.test.ts +499 -0
  37. package/tests/stress/security.test.ts +378 -0
  38. package/tests/stress/simulation.test.ts +361 -0
  39. package/tests/utils/crypto.test.ts +115 -0
  40. package/tests/utils/math.test.ts +195 -0
  41. package/tests/verification/spatial-proof.test.ts +299 -0
  42. package/tsconfig.json +26 -0
@@ -0,0 +1,361 @@
1
+ /**
2
+ * Integration Simulation — Full robot lifecycle from registration to elite status
3
+ *
4
+ * Simulates a realistic fleet scenario:
5
+ * - Fleet of 20 delivery robots
6
+ * - 90-day operation period
7
+ * - Varying success rates, zone visits, incidents
8
+ * - One attacker bot attempting spoofing
9
+ * - Tracks all metrics for competitive analysis output
10
+ */
11
+
12
+ import { describe, it, expect } from 'vitest';
13
+ import {
14
+ TrustTierSystem,
15
+ TrustTier,
16
+ BadgeSystem,
17
+ StreakSystem,
18
+ ZoneMasterySystem,
19
+ FleetLeaderboard,
20
+ LeaderboardMetric,
21
+ type RobotMetrics,
22
+ } from '../../src/gamification/index.js';
23
+ import {
24
+ generateSpatialProof,
25
+ verifySpatialProofIntegrity,
26
+ createSettlement,
27
+ } from '../../src/verification/spatial-proof.js';
28
+ import { signFrame, generateNonce } from '../../src/utils/crypto.js';
29
+ import { PlaceCellPopulation, GridCellSystem, computeSpatialCode } from '../../src/memory/place-cells.js';
30
+ import type { CameraFrame, Pose, RenderedView } from '../../src/types/index.js';
31
+
32
+ const SECRET = 'z'.repeat(32);
33
+ const FLEET_ID = 'dallas-delivery-fleet';
34
+ const NUM_ROBOTS = 20;
35
+ const SIMULATION_DAYS = 90;
36
+ const OPS_PER_DAY = 8; // 8 deliveries per robot per day
37
+
38
+ function makePose(x: number, y: number): Pose {
39
+ return { position: { x, y, z: 0 }, orientation: { w: 1, x: 0, y: 0, z: 0 }, timestamp: Date.now() };
40
+ }
41
+
42
+ function makeFrame(seed: number, seq: number): CameraFrame {
43
+ const rgb = new Uint8Array(32 * 32 * 3);
44
+ for (let i = 0; i < rgb.length; i++) rgb[i] = (i * 17 + seed * 31) % 256;
45
+ const ts = Date.now();
46
+ return {
47
+ id: generateNonce(8), timestamp: ts, rgb, width: 32, height: 32,
48
+ depth: new Float32Array(32 * 32).fill(2.5),
49
+ pose: makePose(seed, seed),
50
+ hmac: signFrame(rgb, ts, seq, SECRET),
51
+ sequenceNumber: seq,
52
+ };
53
+ }
54
+
55
+ describe('Fleet Simulation: 20 robots × 90 days', () => {
56
+ // Shared state across the simulation
57
+ const trustSystem = new TrustTierSystem(SECRET);
58
+ const badgeSystem = new BadgeSystem(SECRET);
59
+ const streakSystem = new StreakSystem(SECRET);
60
+ const zoneMastery = new ZoneMasterySystem(SECRET, 5.0);
61
+ const leaderboard = new FleetLeaderboard(SECRET);
62
+
63
+ // Define realistic delivery zones
64
+ const zones = [
65
+ { name: 'Downtown Dallas', min: { x: 0, y: 0, z: 0 }, max: { x: 50, y: 50, z: 3 } },
66
+ { name: 'Deep Ellum', min: { x: 50, y: 0, z: 0 }, max: { x: 100, y: 30, z: 3 } },
67
+ { name: 'Uptown', min: { x: 0, y: 50, z: 0 }, max: { x: 40, y: 100, z: 3 } },
68
+ { name: 'Bishop Arts', min: { x: -30, y: -30, z: 0 }, max: { x: 0, y: 0, z: 3 } },
69
+ { name: 'Oak Lawn', min: { x: -30, y: 50, z: 0 }, max: { x: 0, y: 100, z: 3 } },
70
+ { name: 'Fair Park', min: { x: 60, y: 30, z: 0 }, max: { x: 100, y: 60, z: 3 } },
71
+ { name: 'Greenville Ave', min: { x: 30, y: 70, z: 0 }, max: { x: 60, y: 100, z: 3 } },
72
+ { name: 'Design District', min: { x: -50, y: 20, z: 0 }, max: { x: -20, y: 50, z: 3 } },
73
+ ];
74
+
75
+ // Robot personality profiles (different success rates simulate real fleet variance)
76
+ const robotProfiles = Array.from({ length: NUM_ROBOTS }, (_, i) => ({
77
+ id: `DLV-${String(i + 1).padStart(3, '0')}`,
78
+ successRate: i === 19 ? 0.3 : 0.85 + Math.random() * 0.14, // Robot 20 is an attacker (30%)
79
+ isAttacker: i === 19,
80
+ preferredZones: [i % zones.length, (i + 3) % zones.length, (i + 5) % zones.length],
81
+ nightOperations: Math.floor(Math.random() * 40),
82
+ fastOps: Math.floor(Math.random() * 15),
83
+ }));
84
+
85
+ it('Phase 1: Registration', () => {
86
+ for (const robot of robotProfiles) {
87
+ trustSystem.register(robot.id);
88
+ streakSystem.register(robot.id);
89
+ }
90
+ for (const zone of zones) {
91
+ zoneMastery.defineZone(zone.name, { min: zone.min, max: zone.max });
92
+ }
93
+ expect(trustSystem.robotCount).toBe(NUM_ROBOTS);
94
+ expect(streakSystem.robotCount).toBe(NUM_ROBOTS);
95
+ expect(zoneMastery.totalZones).toBe(8);
96
+ });
97
+
98
+ it('Phase 2: 90-day operation simulation', () => {
99
+ const baseDate = new Date('2025-06-01T08:00:00Z').getTime();
100
+ const simStats: Record<string, {
101
+ totalOps: number;
102
+ successes: number;
103
+ failures: number;
104
+ spoofAttempts: number;
105
+ pointsEarned: number;
106
+ }> = {};
107
+
108
+ for (const robot of robotProfiles) {
109
+ simStats[robot.id] = { totalOps: 0, successes: 0, failures: 0, spoofAttempts: 0, pointsEarned: 0 };
110
+ }
111
+
112
+ for (let day = 0; day < SIMULATION_DAYS; day++) {
113
+ const dayTimestamp = baseDate + day * 86400000;
114
+
115
+ for (const robot of robotProfiles) {
116
+ const stats = simStats[robot.id]!;
117
+ let daySuccess = true;
118
+
119
+ for (let op = 0; op < OPS_PER_DAY; op++) {
120
+ stats.totalOps++;
121
+ const isSuccess = Math.random() < robot.successRate;
122
+ const isSpoofAttempt = robot.isAttacker && Math.random() < 0.2;
123
+
124
+ if (isSpoofAttempt) {
125
+ stats.spoofAttempts++;
126
+ trustSystem.recordFailure(robot.id, true);
127
+ streakSystem.breakStreak(robot.id);
128
+ daySuccess = false;
129
+ } else if (isSuccess) {
130
+ stats.successes++;
131
+ const basePoints = 50;
132
+ const streakResult = streakSystem.recordActivity(robot.id, basePoints, dayTimestamp);
133
+ const totalEarned = streakResult.totalPoints;
134
+ stats.pointsEarned += totalEarned;
135
+ trustSystem.recordSuccess(robot.id, totalEarned);
136
+
137
+ // Visit a zone
138
+ const zoneIdx = robot.preferredZones[op % robot.preferredZones.length]!;
139
+ const zone = zones[zoneIdx]!;
140
+ const pos = {
141
+ x: zone.min.x + Math.random() * (zone.max.x - zone.min.x),
142
+ y: zone.min.y + Math.random() * (zone.max.y - zone.min.y),
143
+ z: 1,
144
+ };
145
+ zoneMastery.recordVisit(robot.id, pos, true, totalEarned);
146
+ } else {
147
+ stats.failures++;
148
+ trustSystem.recordFailure(robot.id, false);
149
+ daySuccess = false;
150
+ }
151
+ }
152
+ }
153
+ }
154
+
155
+ // Verify simulation completed
156
+ const totalOps = Object.values(simStats).reduce((s, v) => s + v.totalOps, 0);
157
+ expect(totalOps).toBe(NUM_ROBOTS * SIMULATION_DAYS * OPS_PER_DAY); // 14,400 operations
158
+ });
159
+
160
+ it('Phase 3: Badge evaluation', () => {
161
+ for (const robot of robotProfiles) {
162
+ const profile = trustSystem.getProfile(robot.id)!;
163
+ const streakRecord = streakSystem.getRecord(robot.id)!;
164
+ const metrics: RobotMetrics = {
165
+ successful_verifications: profile.successfulVerifications,
166
+ spoofing_incidents: profile.spoofingIncidents,
167
+ threats_detected: 0,
168
+ zones_mapped: zoneMastery.getZoneCount(robot.id),
169
+ max_zone_mastery: zoneMastery.getMaxMastery(robot.id),
170
+ unique_routes: Math.floor(profile.successfulVerifications * 0.3),
171
+ consecutive_days: streakRecord.longestStreak,
172
+ night_operations: robot.nightOperations,
173
+ total_verifications: profile.totalVerifications,
174
+ success_rate: profile.totalVerifications > 0
175
+ ? (profile.successfulVerifications / profile.totalVerifications) * 100
176
+ : 0,
177
+ max_ssim: 0.85 + Math.random() * 0.14,
178
+ max_zones_per_trip: robot.preferredZones.length,
179
+ fast_operations: robot.fastOps,
180
+ };
181
+ badgeSystem.evaluate(robot.id, metrics);
182
+ }
183
+
184
+ // Top performers should have many badges
185
+ const topRobot = robotProfiles[0]!;
186
+ const topBadges = badgeSystem.getBadgeCount(topRobot.id);
187
+ expect(topBadges).toBeGreaterThan(5);
188
+
189
+ // Attacker should have fewer badges
190
+ const attacker = robotProfiles.find(r => r.isAttacker)!;
191
+ const attackerBadges = badgeSystem.getBadgeCount(attacker.id);
192
+ expect(attackerBadges).toBeLessThan(topBadges);
193
+ });
194
+
195
+ it('Phase 4: Leaderboard generation', () => {
196
+ for (const robot of robotProfiles) {
197
+ const profile = trustSystem.getProfile(robot.id)!;
198
+ const streakRecord = streakSystem.getRecord(robot.id)!;
199
+ leaderboard.updateStats({
200
+ robotId: robot.id,
201
+ fleetId: FLEET_ID,
202
+ trustTier: profile.currentTier,
203
+ points: profile.points,
204
+ totalVerifications: profile.totalVerifications,
205
+ successfulVerifications: profile.successfulVerifications,
206
+ zonesExplored: zoneMastery.getZoneCount(robot.id),
207
+ badgeCount: badgeSystem.getBadgeCount(robot.id),
208
+ streakDays: streakRecord.currentStreak,
209
+ maxZoneMastery: zoneMastery.getMaxMastery(robot.id),
210
+ spoofingIncidents: profile.spoofingIncidents,
211
+ });
212
+ }
213
+
214
+ const board = leaderboard.getFleetLeaderboard(FLEET_ID, LeaderboardMetric.COMPOSITE);
215
+ expect(board).toHaveLength(NUM_ROBOTS);
216
+
217
+ // Attacker should be near bottom
218
+ const attacker = robotProfiles.find(r => r.isAttacker)!;
219
+ const attackerEntry = board.find(e => e.robotId === attacker.id)!;
220
+ expect(attackerEntry.rank).toBeGreaterThan(NUM_ROBOTS / 2);
221
+
222
+ // Top robot should be a high-performer
223
+ const topEntry = board[0]!;
224
+ expect(topEntry.score).toBeGreaterThan(0);
225
+ expect(topEntry.spoofingIncidents).toBe(0);
226
+ });
227
+
228
+ it('Phase 5: Trust tier distribution analysis', () => {
229
+ const tierCounts: Record<TrustTier, number> = {
230
+ [TrustTier.UNTRUSTED]: 0,
231
+ [TrustTier.PROBATION]: 0,
232
+ [TrustTier.VERIFIED]: 0,
233
+ [TrustTier.TRUSTED]: 0,
234
+ [TrustTier.ELITE]: 0,
235
+ [TrustTier.AUTONOMOUS]: 0,
236
+ };
237
+
238
+ for (const robot of robotProfiles) {
239
+ const profile = trustSystem.getProfile(robot.id)!;
240
+ tierCounts[profile.currentTier]++;
241
+ }
242
+
243
+ // After 90 days of mostly-successful operations:
244
+ // Most robots should be at Verified or above
245
+ const advancedCount = tierCounts[TrustTier.VERIFIED]
246
+ + tierCounts[TrustTier.TRUSTED]
247
+ + tierCounts[TrustTier.ELITE]
248
+ + tierCounts[TrustTier.AUTONOMOUS];
249
+ expect(advancedCount).toBeGreaterThan(NUM_ROBOTS / 2);
250
+
251
+ // Attacker should be at low tier
252
+ const attacker = robotProfiles.find(r => r.isAttacker)!;
253
+ const attackerProfile = trustSystem.getProfile(attacker.id)!;
254
+ expect(attackerProfile.currentTier).toBeLessThanOrEqual(TrustTier.PROBATION);
255
+ });
256
+
257
+ it('Phase 6: Fleet summary health check', () => {
258
+ const summary = leaderboard.getFleetSummary(FLEET_ID);
259
+ expect(summary.robotCount).toBe(NUM_ROBOTS);
260
+ expect(summary.totalDeliveries).toBeGreaterThan(0);
261
+ // Average success rate should reflect mostly-good fleet
262
+ expect(summary.averageSuccessRate).toBeGreaterThan(0.7);
263
+ expect(summary.topRobotId).toBeTruthy();
264
+ expect(summary.totalZonesExplored).toBeGreaterThan(0);
265
+ });
266
+
267
+ it('Phase 7: Integrity verification', () => {
268
+ // All tier histories should be valid
269
+ for (const robot of robotProfiles) {
270
+ const result = trustSystem.verifyHistory(robot.id);
271
+ expect(result.valid).toBe(true);
272
+ }
273
+
274
+ // All badges should be authentic
275
+ for (const robot of robotProfiles) {
276
+ const result = badgeSystem.verifyAllBadges(robot.id);
277
+ expect(result.valid).toBe(true);
278
+ expect(result.forged).toHaveLength(0);
279
+ }
280
+
281
+ // All streak records should verify
282
+ for (const robot of robotProfiles) {
283
+ expect(streakSystem.verifyRecord(robot.id)).toBe(true);
284
+ }
285
+
286
+ // Leaderboard entries should be signed
287
+ const board = leaderboard.getFleetLeaderboard(FLEET_ID);
288
+ for (const entry of board) {
289
+ expect(entry.signature).toHaveLength(64);
290
+ }
291
+ });
292
+
293
+ it('Phase 8: Spatial verification integration', () => {
294
+ // Run 10 real spatial verifications to ensure the pipeline works
295
+ let passed = 0;
296
+ let failed = 0;
297
+
298
+ for (let i = 0; i < 10; i++) {
299
+ const frame = makeFrame(i, i + 1);
300
+ const identicalRender: RenderedView = {
301
+ rgb: frame.rgb, depth: frame.depth!, width: 32, height: 32,
302
+ pose: makePose(i, i), renderTimeMs: 1,
303
+ };
304
+ const proof = generateSpatialProof(
305
+ 'DLV-001', makePose(i, i), frame, identicalRender,
306
+ 'merkle-root', [], SECRET,
307
+ );
308
+ const integrity = verifySpatialProofIntegrity(proof, SECRET);
309
+ expect(integrity.valid).toBe(true);
310
+
311
+ if (proof.passed) {
312
+ const settlement = createSettlement(proof, 15.00, 'USD', 'merchant-1', SECRET);
313
+ expect(settlement.status).toBe('verified');
314
+ passed++;
315
+ } else {
316
+ failed++;
317
+ }
318
+ }
319
+ // With identical render/frame, all should pass
320
+ expect(passed).toBe(10);
321
+ });
322
+
323
+ it('Phase 9: Performance benchmarks', () => {
324
+ // Benchmark: badge evaluation for 20 robots
325
+ const start1 = performance.now();
326
+ for (let round = 0; round < 100; round++) {
327
+ for (const robot of robotProfiles) {
328
+ const profile = trustSystem.getProfile(robot.id)!;
329
+ badgeSystem.evaluate(robot.id, {
330
+ successful_verifications: profile.successfulVerifications,
331
+ spoofing_incidents: profile.spoofingIncidents,
332
+ threats_detected: 0, zones_mapped: 3, max_zone_mastery: 0.5,
333
+ unique_routes: 10, consecutive_days: 30, night_operations: 5,
334
+ total_verifications: profile.totalVerifications, success_rate: 90,
335
+ max_ssim: 0.9, max_zones_per_trip: 2, fast_operations: 3,
336
+ });
337
+ }
338
+ }
339
+ const badgeEvalMs = performance.now() - start1;
340
+ expect(badgeEvalMs).toBeLessThan(3000); // 2000 evaluations under 3s
341
+
342
+ // Benchmark: leaderboard generation
343
+ const start2 = performance.now();
344
+ for (let round = 0; round < 100; round++) {
345
+ leaderboard.getFleetLeaderboard(FLEET_ID, LeaderboardMetric.COMPOSITE);
346
+ }
347
+ const leaderboardMs = performance.now() - start2;
348
+ expect(leaderboardMs).toBeLessThan(2000); // 100 leaderboard generations under 2s
349
+
350
+ // Benchmark: spatial code computation
351
+ const placeCells = new PlaceCellPopulation(2.0);
352
+ placeCells.coverRegion({ x: 0, y: 0, z: 0 }, { x: 50, y: 50, z: 0 }, 3.0);
353
+ const gridCells = new GridCellSystem(0.5, Math.SQRT2, 4, 16);
354
+ const start3 = performance.now();
355
+ for (let i = 0; i < 1000; i++) {
356
+ computeSpatialCode({ x: Math.random() * 50, y: Math.random() * 50, z: 0 }, placeCells, gridCells);
357
+ }
358
+ const spatialCodeMs = performance.now() - start3;
359
+ expect(spatialCodeMs).toBeLessThan(5000); // 1000 spatial codes under 5s
360
+ });
361
+ });
@@ -0,0 +1,115 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import {
3
+ hmacSign,
4
+ hmacVerify,
5
+ signFrame,
6
+ verifyFrame,
7
+ generateNonce,
8
+ sha256,
9
+ deriveKey,
10
+ } from '../../src/utils/crypto.js';
11
+
12
+ const TEST_SECRET = 'a'.repeat(32); // minimum length secret for testing
13
+
14
+ describe('Crypto Utilities', () => {
15
+ describe('hmacSign / hmacVerify', () => {
16
+ it('signs and verifies data correctly', () => {
17
+ const data = Buffer.from('hello world');
18
+ const sig = hmacSign(data, TEST_SECRET);
19
+ expect(sig).toHaveLength(64); // SHA-256 hex = 64 chars
20
+ expect(hmacVerify(data, sig, TEST_SECRET)).toBe(true);
21
+ });
22
+
23
+ it('rejects tampered data', () => {
24
+ const data = Buffer.from('hello world');
25
+ const sig = hmacSign(data, TEST_SECRET);
26
+ const tampered = Buffer.from('hello World'); // capital W
27
+ expect(hmacVerify(tampered, sig, TEST_SECRET)).toBe(false);
28
+ });
29
+
30
+ it('rejects wrong secret', () => {
31
+ const data = Buffer.from('hello world');
32
+ const sig = hmacSign(data, TEST_SECRET);
33
+ const wrongSecret = 'b'.repeat(32);
34
+ expect(hmacVerify(data, sig, wrongSecret)).toBe(false);
35
+ });
36
+
37
+ it('throws on short secret', () => {
38
+ expect(() => hmacSign(Buffer.from('test'), 'short')).toThrow('at least 32');
39
+ });
40
+
41
+ it('rejects empty secret', () => {
42
+ expect(() => hmacSign(Buffer.from('test'), '')).toThrow('at least 32');
43
+ });
44
+ });
45
+
46
+ describe('signFrame / verifyFrame', () => {
47
+ it('signs and verifies frame data', () => {
48
+ const rgb = new Uint8Array([255, 0, 0, 0, 255, 0]);
49
+ const timestamp = 1712600000000;
50
+ const seq = 42;
51
+ const sig = signFrame(rgb, timestamp, seq, TEST_SECRET);
52
+ expect(verifyFrame(rgb, timestamp, seq, sig, TEST_SECRET)).toBe(true);
53
+ });
54
+
55
+ it('rejects altered pixel data', () => {
56
+ const rgb = new Uint8Array([255, 0, 0, 0, 255, 0]);
57
+ const sig = signFrame(rgb, 1000, 1, TEST_SECRET);
58
+ const altered = new Uint8Array([255, 0, 0, 0, 255, 1]); // 1 pixel changed
59
+ expect(verifyFrame(altered, 1000, 1, sig, TEST_SECRET)).toBe(false);
60
+ });
61
+
62
+ it('rejects altered timestamp', () => {
63
+ const rgb = new Uint8Array([255, 0, 0]);
64
+ const sig = signFrame(rgb, 1000, 1, TEST_SECRET);
65
+ expect(verifyFrame(rgb, 1001, 1, sig, TEST_SECRET)).toBe(false);
66
+ });
67
+
68
+ it('rejects altered sequence number', () => {
69
+ const rgb = new Uint8Array([255, 0, 0]);
70
+ const sig = signFrame(rgb, 1000, 1, TEST_SECRET);
71
+ expect(verifyFrame(rgb, 1000, 2, sig, TEST_SECRET)).toBe(false);
72
+ });
73
+ });
74
+
75
+ describe('generateNonce', () => {
76
+ it('generates hex string of correct length', () => {
77
+ const nonce = generateNonce(16);
78
+ expect(nonce).toHaveLength(32); // 16 bytes = 32 hex chars
79
+ });
80
+
81
+ it('generates unique values', () => {
82
+ const a = generateNonce();
83
+ const b = generateNonce();
84
+ expect(a).not.toBe(b);
85
+ });
86
+ });
87
+
88
+ describe('sha256', () => {
89
+ it('hashes strings correctly', () => {
90
+ const hash = sha256('hello');
91
+ expect(hash).toHaveLength(64);
92
+ expect(hash).toBe('2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824');
93
+ });
94
+
95
+ it('hashes buffers correctly', () => {
96
+ const hash = sha256(Buffer.from('hello'));
97
+ expect(hash).toBe('2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e73043362938b9824');
98
+ });
99
+ });
100
+
101
+ describe('deriveKey', () => {
102
+ it('derives different keys for different contexts', () => {
103
+ const key1 = deriveKey(TEST_SECRET, 'frame-signing');
104
+ const key2 = deriveKey(TEST_SECRET, 'memory-signing');
105
+ expect(key1).not.toBe(key2);
106
+ expect(key1).toHaveLength(64);
107
+ });
108
+
109
+ it('derives same key for same inputs', () => {
110
+ const key1 = deriveKey(TEST_SECRET, 'test');
111
+ const key2 = deriveKey(TEST_SECRET, 'test');
112
+ expect(key1).toBe(key2);
113
+ });
114
+ });
115
+ });
@@ -0,0 +1,195 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import {
3
+ vec3Add, vec3Sub, vec3Scale, vec3Dot, vec3Length, vec3Normalize,
4
+ vec3Distance, vec3Cross, vec3Lerp,
5
+ quatMultiply, quatConjugate, quatNormalize, quatRotateVec3,
6
+ quatFromAxisAngle, quatSlerp, QUAT_IDENTITY,
7
+ poseToMat4, egoToAllo, alloToEgo, stereoDepth,
8
+ gaussian, gaussian3D, clamp, meanAbsoluteError,
9
+ } from '../../src/utils/math.js';
10
+
11
+ describe('Vector Operations', () => {
12
+ it('adds vectors', () => {
13
+ const result = vec3Add({ x: 1, y: 2, z: 3 }, { x: 4, y: 5, z: 6 });
14
+ expect(result).toEqual({ x: 5, y: 7, z: 9 });
15
+ });
16
+
17
+ it('subtracts vectors', () => {
18
+ const result = vec3Sub({ x: 5, y: 7, z: 9 }, { x: 4, y: 5, z: 6 });
19
+ expect(result).toEqual({ x: 1, y: 2, z: 3 });
20
+ });
21
+
22
+ it('scales vectors', () => {
23
+ expect(vec3Scale({ x: 1, y: 2, z: 3 }, 2)).toEqual({ x: 2, y: 4, z: 6 });
24
+ });
25
+
26
+ it('computes dot product', () => {
27
+ expect(vec3Dot({ x: 1, y: 0, z: 0 }, { x: 0, y: 1, z: 0 })).toBe(0);
28
+ expect(vec3Dot({ x: 1, y: 2, z: 3 }, { x: 1, y: 2, z: 3 })).toBe(14);
29
+ });
30
+
31
+ it('computes length', () => {
32
+ expect(vec3Length({ x: 3, y: 4, z: 0 })).toBe(5);
33
+ });
34
+
35
+ it('normalizes vectors', () => {
36
+ const n = vec3Normalize({ x: 3, y: 4, z: 0 });
37
+ expect(n.x).toBeCloseTo(0.6);
38
+ expect(n.y).toBeCloseTo(0.8);
39
+ expect(vec3Length(n)).toBeCloseTo(1);
40
+ });
41
+
42
+ it('handles zero vector normalization', () => {
43
+ const n = vec3Normalize({ x: 0, y: 0, z: 0 });
44
+ expect(n).toEqual({ x: 0, y: 0, z: 0 });
45
+ });
46
+
47
+ it('computes distance', () => {
48
+ expect(vec3Distance({ x: 0, y: 0, z: 0 }, { x: 3, y: 4, z: 0 })).toBe(5);
49
+ });
50
+
51
+ it('computes cross product', () => {
52
+ const result = vec3Cross({ x: 1, y: 0, z: 0 }, { x: 0, y: 1, z: 0 });
53
+ expect(result).toEqual({ x: 0, y: 0, z: 1 });
54
+ });
55
+
56
+ it('lerps between vectors', () => {
57
+ const a = { x: 0, y: 0, z: 0 };
58
+ const b = { x: 10, y: 10, z: 10 };
59
+ const mid = vec3Lerp(a, b, 0.5);
60
+ expect(mid).toEqual({ x: 5, y: 5, z: 5 });
61
+ });
62
+
63
+ it('clamps lerp parameter', () => {
64
+ const a = { x: 0, y: 0, z: 0 };
65
+ const b = { x: 10, y: 10, z: 10 };
66
+ expect(vec3Lerp(a, b, -1)).toEqual(a);
67
+ expect(vec3Lerp(a, b, 2)).toEqual(b);
68
+ });
69
+ });
70
+
71
+ describe('Quaternion Operations', () => {
72
+ it('identity quaternion preserves rotation', () => {
73
+ const v = { x: 1, y: 2, z: 3 };
74
+ const rotated = quatRotateVec3(QUAT_IDENTITY, v);
75
+ expect(rotated.x).toBeCloseTo(1);
76
+ expect(rotated.y).toBeCloseTo(2);
77
+ expect(rotated.z).toBeCloseTo(3);
78
+ });
79
+
80
+ it('rotates 90 degrees around Z axis', () => {
81
+ const q = quatFromAxisAngle({ x: 0, y: 0, z: 1 }, Math.PI / 2);
82
+ const v = { x: 1, y: 0, z: 0 };
83
+ const rotated = quatRotateVec3(q, v);
84
+ expect(rotated.x).toBeCloseTo(0);
85
+ expect(rotated.y).toBeCloseTo(1);
86
+ expect(rotated.z).toBeCloseTo(0);
87
+ });
88
+
89
+ it('conjugate reverses rotation', () => {
90
+ const q = quatFromAxisAngle({ x: 0, y: 0, z: 1 }, Math.PI / 4);
91
+ const v = { x: 1, y: 0, z: 0 };
92
+ const rotated = quatRotateVec3(q, v);
93
+ const unrotated = quatRotateVec3(quatConjugate(q), rotated);
94
+ expect(unrotated.x).toBeCloseTo(1);
95
+ expect(unrotated.y).toBeCloseTo(0);
96
+ expect(unrotated.z).toBeCloseTo(0);
97
+ });
98
+
99
+ it('slerp interpolates between quaternions', () => {
100
+ const a = QUAT_IDENTITY;
101
+ const b = quatFromAxisAngle({ x: 0, y: 0, z: 1 }, Math.PI);
102
+ const mid = quatSlerp(a, b, 0.5);
103
+ // At t=0.5, should be 90 degrees rotation
104
+ const v = quatRotateVec3(mid, { x: 1, y: 0, z: 0 });
105
+ expect(v.x).toBeCloseTo(0, 1);
106
+ expect(Math.abs(v.y)).toBeCloseTo(1, 1);
107
+ });
108
+ });
109
+
110
+ describe('Coordinate Transforms', () => {
111
+ it('ego to allo with identity pose is identity', () => {
112
+ const pose = {
113
+ position: { x: 0, y: 0, z: 0 },
114
+ orientation: QUAT_IDENTITY,
115
+ timestamp: 0,
116
+ };
117
+ const result = egoToAllo({ x: 1, y: 2, z: 3 }, pose);
118
+ expect(result.x).toBeCloseTo(1);
119
+ expect(result.y).toBeCloseTo(2);
120
+ expect(result.z).toBeCloseTo(3);
121
+ });
122
+
123
+ it('ego to allo applies translation', () => {
124
+ const pose = {
125
+ position: { x: 10, y: 20, z: 30 },
126
+ orientation: QUAT_IDENTITY,
127
+ timestamp: 0,
128
+ };
129
+ const result = egoToAllo({ x: 1, y: 2, z: 3 }, pose);
130
+ expect(result.x).toBeCloseTo(11);
131
+ expect(result.y).toBeCloseTo(22);
132
+ expect(result.z).toBeCloseTo(33);
133
+ });
134
+
135
+ it('allo to ego reverses ego to allo', () => {
136
+ const pose = {
137
+ position: { x: 5, y: 10, z: 15 },
138
+ orientation: quatFromAxisAngle({ x: 0, y: 0, z: 1 }, Math.PI / 4),
139
+ timestamp: 0,
140
+ };
141
+ const worldPoint = { x: 7, y: 12, z: 18 };
142
+ const ego = alloToEgo(worldPoint, pose);
143
+ const back = egoToAllo(ego, pose);
144
+ expect(back.x).toBeCloseTo(worldPoint.x);
145
+ expect(back.y).toBeCloseTo(worldPoint.y);
146
+ expect(back.z).toBeCloseTo(worldPoint.z);
147
+ });
148
+
149
+ it('computes stereo depth correctly', () => {
150
+ // Z = f*B/d
151
+ expect(stereoDepth(500, 0.1, 10)).toBeCloseTo(5); // 500*0.1/10 = 5m
152
+ expect(stereoDepth(500, 0.1, 0)).toBe(Infinity); // zero disparity = infinity
153
+ });
154
+ });
155
+
156
+ describe('Statistical Functions', () => {
157
+ it('gaussian peaks at center', () => {
158
+ expect(gaussian(0, 0, 1)).toBeCloseTo(1);
159
+ expect(gaussian(0, 0, 1, 2)).toBeCloseTo(2);
160
+ });
161
+
162
+ it('gaussian falls off with distance', () => {
163
+ const atCenter = gaussian(0, 0, 1);
164
+ const at1Sigma = gaussian(1, 0, 1);
165
+ const at2Sigma = gaussian(2, 0, 1);
166
+ expect(atCenter).toBeGreaterThan(at1Sigma);
167
+ expect(at1Sigma).toBeGreaterThan(at2Sigma);
168
+ });
169
+
170
+ it('3D gaussian works in 3D', () => {
171
+ const center = { x: 0, y: 0, z: 0 };
172
+ expect(gaussian3D(center, center, 1)).toBeCloseTo(1);
173
+ expect(gaussian3D({ x: 1, y: 0, z: 0 }, center, 1)).toBeLessThan(1);
174
+ });
175
+
176
+ it('clamp works', () => {
177
+ expect(clamp(5, 0, 10)).toBe(5);
178
+ expect(clamp(-5, 0, 10)).toBe(0);
179
+ expect(clamp(15, 0, 10)).toBe(10);
180
+ });
181
+
182
+ it('meanAbsoluteError computes correctly', () => {
183
+ const a = new Float32Array([1, 2, 3]);
184
+ const b = new Float32Array([1, 2, 3]);
185
+ expect(meanAbsoluteError(a, b)).toBe(0);
186
+
187
+ const c = new Float32Array([1, 2, 3]);
188
+ const d = new Float32Array([2, 3, 4]);
189
+ expect(meanAbsoluteError(c, d)).toBe(1);
190
+ });
191
+
192
+ it('meanAbsoluteError throws on mismatch', () => {
193
+ expect(() => meanAbsoluteError(new Float32Array(3), new Float32Array(4))).toThrow('mismatch');
194
+ });
195
+ });