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,429 @@
1
+ /**
2
+ * Capability Badge System — Achievement tracking for robots
3
+ *
4
+ * Maps WeMeetWeMet's 25+ badges to robot operational achievements.
5
+ * Badges are HMAC-signed to prevent forgery.
6
+ * Categories: operational, safety, navigation, endurance, special
7
+ */
8
+
9
+ import { hmacSign, hmacVerify } from '../utils/crypto.js';
10
+
11
+ export enum BadgeCategory {
12
+ OPERATIONAL = 'operational',
13
+ SAFETY = 'safety',
14
+ NAVIGATION = 'navigation',
15
+ ENDURANCE = 'endurance',
16
+ SPECIAL = 'special',
17
+ }
18
+
19
+ export enum BadgeRarity {
20
+ COMMON = 'common', // easy to earn
21
+ UNCOMMON = 'uncommon', // requires consistent performance
22
+ RARE = 'rare', // significant achievement
23
+ EPIC = 'epic', // exceptional performance
24
+ LEGENDARY = 'legendary', // near-impossible feats
25
+ }
26
+
27
+ export interface BadgeDefinition {
28
+ readonly id: string;
29
+ readonly name: string;
30
+ readonly description: string;
31
+ readonly category: BadgeCategory;
32
+ readonly rarity: BadgeRarity;
33
+ readonly criteria: BadgeCriteria;
34
+ readonly pointValue: number; // points awarded when earned
35
+ }
36
+
37
+ export interface BadgeCriteria {
38
+ readonly type: 'count' | 'streak' | 'rate' | 'threshold' | 'compound';
39
+ readonly metric: string;
40
+ readonly target: number;
41
+ readonly secondaryMetric?: string;
42
+ readonly secondaryTarget?: number;
43
+ }
44
+
45
+ export interface EarnedBadge {
46
+ readonly badgeId: string;
47
+ readonly robotId: string;
48
+ readonly earnedAt: number;
49
+ readonly signature: string; // HMAC-signed proof of earning
50
+ readonly metadata?: Record<string, number>; // stats at time of earning
51
+ }
52
+
53
+ // ============================================================
54
+ // BADGE CATALOG
55
+ // ============================================================
56
+
57
+ export const BADGE_CATALOG: readonly BadgeDefinition[] = [
58
+ // OPERATIONAL
59
+ {
60
+ id: 'first-delivery',
61
+ name: 'First Delivery',
62
+ description: 'Complete your first verified spatial delivery',
63
+ category: BadgeCategory.OPERATIONAL,
64
+ rarity: BadgeRarity.COMMON,
65
+ criteria: { type: 'count', metric: 'successful_verifications', target: 1 },
66
+ pointValue: 50,
67
+ },
68
+ {
69
+ id: 'century-club',
70
+ name: 'Century Club',
71
+ description: 'Complete 100 verified operations',
72
+ category: BadgeCategory.OPERATIONAL,
73
+ rarity: BadgeRarity.UNCOMMON,
74
+ criteria: { type: 'count', metric: 'successful_verifications', target: 100 },
75
+ pointValue: 200,
76
+ },
77
+ {
78
+ id: 'thousand-strong',
79
+ name: 'Thousand Strong',
80
+ description: 'Complete 1,000 verified operations',
81
+ category: BadgeCategory.OPERATIONAL,
82
+ rarity: BadgeRarity.RARE,
83
+ criteria: { type: 'count', metric: 'successful_verifications', target: 1000 },
84
+ pointValue: 500,
85
+ },
86
+ {
87
+ id: 'ten-thousand-veteran',
88
+ name: 'Ten Thousand Veteran',
89
+ description: 'Complete 10,000 verified operations',
90
+ category: BadgeCategory.OPERATIONAL,
91
+ rarity: BadgeRarity.LEGENDARY,
92
+ criteria: { type: 'count', metric: 'successful_verifications', target: 10000 },
93
+ pointValue: 2000,
94
+ },
95
+
96
+ // SAFETY
97
+ {
98
+ id: 'clean-record',
99
+ name: 'Clean Record',
100
+ description: '50 operations with zero spoofing incidents',
101
+ category: BadgeCategory.SAFETY,
102
+ rarity: BadgeRarity.UNCOMMON,
103
+ criteria: {
104
+ type: 'compound',
105
+ metric: 'successful_verifications',
106
+ target: 50,
107
+ secondaryMetric: 'spoofing_incidents',
108
+ secondaryTarget: 0,
109
+ },
110
+ pointValue: 300,
111
+ },
112
+ {
113
+ id: 'impenetrable',
114
+ name: 'Impenetrable',
115
+ description: '500 operations, zero security incidents',
116
+ category: BadgeCategory.SAFETY,
117
+ rarity: BadgeRarity.EPIC,
118
+ criteria: {
119
+ type: 'compound',
120
+ metric: 'successful_verifications',
121
+ target: 500,
122
+ secondaryMetric: 'spoofing_incidents',
123
+ secondaryTarget: 0,
124
+ },
125
+ pointValue: 1000,
126
+ },
127
+ {
128
+ id: 'fraud-detector',
129
+ name: 'Fraud Detector',
130
+ description: 'Detect and report 10 adversarial attacks',
131
+ category: BadgeCategory.SAFETY,
132
+ rarity: BadgeRarity.RARE,
133
+ criteria: { type: 'count', metric: 'threats_detected', target: 10 },
134
+ pointValue: 400,
135
+ },
136
+
137
+ // NAVIGATION
138
+ {
139
+ id: 'explorer',
140
+ name: 'Explorer',
141
+ description: 'Map 5 distinct zones',
142
+ category: BadgeCategory.NAVIGATION,
143
+ rarity: BadgeRarity.COMMON,
144
+ criteria: { type: 'count', metric: 'zones_mapped', target: 5 },
145
+ pointValue: 100,
146
+ },
147
+ {
148
+ id: 'cartographer',
149
+ name: 'Cartographer',
150
+ description: 'Map 25 distinct zones',
151
+ category: BadgeCategory.NAVIGATION,
152
+ rarity: BadgeRarity.UNCOMMON,
153
+ criteria: { type: 'count', metric: 'zones_mapped', target: 25 },
154
+ pointValue: 300,
155
+ },
156
+ {
157
+ id: 'globe-trotter',
158
+ name: 'Globe Trotter',
159
+ description: 'Map 100 distinct zones',
160
+ category: BadgeCategory.NAVIGATION,
161
+ rarity: BadgeRarity.RARE,
162
+ criteria: { type: 'count', metric: 'zones_mapped', target: 100 },
163
+ pointValue: 750,
164
+ },
165
+ {
166
+ id: 'zone-master',
167
+ name: 'Zone Master',
168
+ description: 'Achieve 90%+ mastery in any single zone',
169
+ category: BadgeCategory.NAVIGATION,
170
+ rarity: BadgeRarity.RARE,
171
+ criteria: { type: 'threshold', metric: 'max_zone_mastery', target: 0.9 },
172
+ pointValue: 500,
173
+ },
174
+ {
175
+ id: 'pathfinder',
176
+ name: 'Pathfinder',
177
+ description: 'Successfully navigate 50 unique routes',
178
+ category: BadgeCategory.NAVIGATION,
179
+ rarity: BadgeRarity.UNCOMMON,
180
+ criteria: { type: 'count', metric: 'unique_routes', target: 50 },
181
+ pointValue: 200,
182
+ },
183
+
184
+ // ENDURANCE
185
+ {
186
+ id: 'on-a-roll',
187
+ name: 'On a Roll',
188
+ description: '7 consecutive days of verified operations',
189
+ category: BadgeCategory.ENDURANCE,
190
+ rarity: BadgeRarity.COMMON,
191
+ criteria: { type: 'streak', metric: 'consecutive_days', target: 7 },
192
+ pointValue: 100,
193
+ },
194
+ {
195
+ id: 'hot-streak',
196
+ name: 'Hot Streak',
197
+ description: '30 consecutive days of verified operations',
198
+ category: BadgeCategory.ENDURANCE,
199
+ rarity: BadgeRarity.UNCOMMON,
200
+ criteria: { type: 'streak', metric: 'consecutive_days', target: 30 },
201
+ pointValue: 300,
202
+ },
203
+ {
204
+ id: 'unstoppable',
205
+ name: 'Unstoppable',
206
+ description: '90 consecutive days of verified operations',
207
+ category: BadgeCategory.ENDURANCE,
208
+ rarity: BadgeRarity.RARE,
209
+ criteria: { type: 'streak', metric: 'consecutive_days', target: 90 },
210
+ pointValue: 750,
211
+ },
212
+ {
213
+ id: 'eternal-flame',
214
+ name: 'Eternal Flame',
215
+ description: '365 consecutive days of verified operations',
216
+ category: BadgeCategory.ENDURANCE,
217
+ rarity: BadgeRarity.LEGENDARY,
218
+ criteria: { type: 'streak', metric: 'consecutive_days', target: 365 },
219
+ pointValue: 3000,
220
+ },
221
+ {
222
+ id: 'night-owl',
223
+ name: 'Night Owl',
224
+ description: 'Complete 50 verified operations between midnight and 6am',
225
+ category: BadgeCategory.ENDURANCE,
226
+ rarity: BadgeRarity.UNCOMMON,
227
+ criteria: { type: 'count', metric: 'night_operations', target: 50 },
228
+ pointValue: 200,
229
+ },
230
+ {
231
+ id: 'all-weather',
232
+ name: 'All Weather',
233
+ description: 'Maintain 95%+ success rate across 200 operations in varying conditions',
234
+ category: BadgeCategory.ENDURANCE,
235
+ rarity: BadgeRarity.EPIC,
236
+ criteria: {
237
+ type: 'compound',
238
+ metric: 'total_verifications',
239
+ target: 200,
240
+ secondaryMetric: 'success_rate',
241
+ secondaryTarget: 95,
242
+ },
243
+ pointValue: 800,
244
+ },
245
+
246
+ // SPECIAL
247
+ {
248
+ id: 'perfect-score',
249
+ name: 'Perfect Score',
250
+ description: 'Achieve SSIM > 0.99 on a spatial verification',
251
+ category: BadgeCategory.SPECIAL,
252
+ rarity: BadgeRarity.RARE,
253
+ criteria: { type: 'threshold', metric: 'max_ssim', target: 0.99 },
254
+ pointValue: 500,
255
+ },
256
+ {
257
+ id: 'cross-zone',
258
+ name: 'Cross-Zone Navigator',
259
+ description: 'Complete a delivery spanning 3+ zones in one trip',
260
+ category: BadgeCategory.SPECIAL,
261
+ rarity: BadgeRarity.UNCOMMON,
262
+ criteria: { type: 'threshold', metric: 'max_zones_per_trip', target: 3 },
263
+ pointValue: 300,
264
+ },
265
+ {
266
+ id: 'speed-demon',
267
+ name: 'Speed Demon',
268
+ description: 'Complete 10 operations in under 60 seconds each',
269
+ category: BadgeCategory.SPECIAL,
270
+ rarity: BadgeRarity.RARE,
271
+ criteria: { type: 'count', metric: 'fast_operations', target: 10 },
272
+ pointValue: 400,
273
+ },
274
+ ];
275
+
276
+ function signBadgeAward(badgeId: string, robotId: string, earnedAt: number, secret: string): string {
277
+ const payload = `badge:${badgeId}:${robotId}:${earnedAt}`;
278
+ return hmacSign(Buffer.from(payload), secret);
279
+ }
280
+
281
+ export function verifyBadgeAward(badge: EarnedBadge, secret: string): boolean {
282
+ const payload = `badge:${badge.badgeId}:${badge.robotId}:${badge.earnedAt}`;
283
+ return hmacVerify(Buffer.from(payload), badge.signature, secret);
284
+ }
285
+
286
+ export interface RobotMetrics {
287
+ successful_verifications: number;
288
+ spoofing_incidents: number;
289
+ threats_detected: number;
290
+ zones_mapped: number;
291
+ max_zone_mastery: number;
292
+ unique_routes: number;
293
+ consecutive_days: number;
294
+ night_operations: number;
295
+ total_verifications: number;
296
+ success_rate: number; // percentage 0-100
297
+ max_ssim: number;
298
+ max_zones_per_trip: number;
299
+ fast_operations: number;
300
+ }
301
+
302
+ export class BadgeSystem {
303
+ private readonly earned = new Map<string, EarnedBadge[]>(); // robotId → badges
304
+ private readonly secret: string;
305
+ private readonly catalog: readonly BadgeDefinition[];
306
+
307
+ constructor(secret: string, catalog?: readonly BadgeDefinition[]) {
308
+ if (!secret || secret.length < 32) {
309
+ throw new Error('BadgeSystem requires a secret of at least 32 chars');
310
+ }
311
+ this.secret = secret;
312
+ this.catalog = catalog ?? BADGE_CATALOG;
313
+ }
314
+
315
+ /**
316
+ * Check and award any newly qualified badges
317
+ */
318
+ evaluate(robotId: string, metrics: RobotMetrics): EarnedBadge[] {
319
+ if (!robotId) throw new Error('robotId is required');
320
+
321
+ const existing = this.earned.get(robotId) ?? [];
322
+ const existingIds = new Set(existing.map(b => b.badgeId));
323
+ const newBadges: EarnedBadge[] = [];
324
+
325
+ for (const badge of this.catalog) {
326
+ if (existingIds.has(badge.id)) continue;
327
+ if (this.checkCriteria(badge.criteria, metrics)) {
328
+ const now = Date.now();
329
+ const earned: EarnedBadge = {
330
+ badgeId: badge.id,
331
+ robotId,
332
+ earnedAt: now,
333
+ signature: signBadgeAward(badge.id, robotId, now, this.secret),
334
+ metadata: { ...metrics },
335
+ };
336
+ newBadges.push(earned);
337
+ }
338
+ }
339
+
340
+ if (newBadges.length > 0) {
341
+ this.earned.set(robotId, [...existing, ...newBadges]);
342
+ }
343
+ return newBadges;
344
+ }
345
+
346
+ private checkCriteria(criteria: BadgeCriteria, metrics: RobotMetrics): boolean {
347
+ const value = metrics[criteria.metric as keyof RobotMetrics] ?? 0;
348
+
349
+ switch (criteria.type) {
350
+ case 'count':
351
+ case 'streak':
352
+ case 'threshold':
353
+ return value >= criteria.target;
354
+ case 'rate':
355
+ return value >= criteria.target;
356
+ case 'compound': {
357
+ const primary = value >= criteria.target;
358
+ if (!primary) return false;
359
+ if (criteria.secondaryMetric && criteria.secondaryTarget !== undefined) {
360
+ const secondary = metrics[criteria.secondaryMetric as keyof RobotMetrics] ?? 0;
361
+ // For "zero incidents" badges, target is 0 and we want value <= target
362
+ // For rate badges, we want value >= target
363
+ if (criteria.secondaryTarget === 0) {
364
+ return secondary <= criteria.secondaryTarget;
365
+ }
366
+ return secondary >= criteria.secondaryTarget;
367
+ }
368
+ return true;
369
+ }
370
+ default:
371
+ return false;
372
+ }
373
+ }
374
+
375
+ /**
376
+ * Get all badges earned by a robot
377
+ */
378
+ getBadges(robotId: string): readonly EarnedBadge[] {
379
+ return this.earned.get(robotId) ?? [];
380
+ }
381
+
382
+ /**
383
+ * Get badge count for a robot
384
+ */
385
+ getBadgeCount(robotId: string): number {
386
+ return (this.earned.get(robotId) ?? []).length;
387
+ }
388
+
389
+ /**
390
+ * Get total point value of all earned badges
391
+ */
392
+ getTotalBadgePoints(robotId: string): number {
393
+ const badges = this.earned.get(robotId) ?? [];
394
+ let total = 0;
395
+ for (const earned of badges) {
396
+ const def = this.catalog.find(b => b.id === earned.badgeId);
397
+ if (def) total += def.pointValue;
398
+ }
399
+ return total;
400
+ }
401
+
402
+ /**
403
+ * Verify all badges for a robot are authentic
404
+ */
405
+ verifyAllBadges(robotId: string): { valid: boolean; forged: string[] } {
406
+ const badges = this.earned.get(robotId) ?? [];
407
+ const forged: string[] = [];
408
+ for (const badge of badges) {
409
+ if (!verifyBadgeAward(badge, this.secret)) {
410
+ forged.push(badge.badgeId);
411
+ }
412
+ }
413
+ return { valid: forged.length === 0, forged };
414
+ }
415
+
416
+ /**
417
+ * Get catalog size
418
+ */
419
+ get catalogSize(): number {
420
+ return this.catalog.length;
421
+ }
422
+
423
+ /**
424
+ * Get badge definition by ID
425
+ */
426
+ getDefinition(badgeId: string): BadgeDefinition | undefined {
427
+ return this.catalog.find(b => b.id === badgeId);
428
+ }
429
+ }