restless-sdk 0.1.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.
@@ -0,0 +1,1119 @@
1
+ {
2
+ "$generated": "GENERATED FILE - do not edit by hand. Inputs live in spec/cases.ts; run `npm run spec:vectors` to regenerate.",
3
+ "$contract": "spec/CONTRACT.md",
4
+ "specVersion": "1.0.0",
5
+ "cases": [
6
+ {
7
+ "id": "fp/404-parameterized",
8
+ "requirement": "FP-013",
9
+ "op": "fingerprint",
10
+ "input": {
11
+ "status": 404,
12
+ "method": "GET",
13
+ "route": "/car/{id}"
14
+ },
15
+ "expected": {
16
+ "strategy": "resource",
17
+ "key": "404:resource"
18
+ }
19
+ },
20
+ {
21
+ "id": "fp/404-concrete-id-is-parameterized",
22
+ "requirement": "FP-014",
23
+ "op": "fingerprint",
24
+ "input": {
25
+ "status": 404,
26
+ "method": "GET",
27
+ "route": "/car/123"
28
+ },
29
+ "expected": {
30
+ "strategy": "resource",
31
+ "key": "404:resource"
32
+ },
33
+ "note": "Normalization runs BEFORE the parameter test, so a concrete id counts."
34
+ },
35
+ {
36
+ "id": "fp/404-collapses-across-routes",
37
+ "requirement": "FP-013",
38
+ "op": "fingerprint",
39
+ "input": {
40
+ "status": 404,
41
+ "method": "DELETE",
42
+ "route": "/user/{id}/posts/{postId}"
43
+ },
44
+ "expected": {
45
+ "strategy": "resource",
46
+ "key": "404:resource"
47
+ }
48
+ },
49
+ {
50
+ "id": "fp/404-paramless-route",
51
+ "requirement": "FP-013",
52
+ "op": "fingerprint",
53
+ "input": {
54
+ "status": 404,
55
+ "method": "GET",
56
+ "route": "/users"
57
+ },
58
+ "expected": {
59
+ "strategy": "endpoint",
60
+ "key": "404:endpoint"
61
+ }
62
+ },
63
+ {
64
+ "id": "fp/404-no-route",
65
+ "requirement": "FP-014",
66
+ "op": "fingerprint",
67
+ "input": {
68
+ "status": 404,
69
+ "method": "GET"
70
+ },
71
+ "expected": {
72
+ "strategy": "endpoint",
73
+ "key": "404:endpoint"
74
+ }
75
+ },
76
+ {
77
+ "id": "fp/404-header-code-does-not-win",
78
+ "requirement": "FP-012",
79
+ "op": "fingerprint",
80
+ "input": {
81
+ "status": 404,
82
+ "method": "GET",
83
+ "route": "/car/{id}",
84
+ "responseHeaders": {
85
+ "x-restless-error-code": "not_found"
86
+ }
87
+ },
88
+ "expected": {
89
+ "strategy": "resource",
90
+ "key": "404:resource"
91
+ },
92
+ "note": "404 is intercepted before the header strategy. A generic not_found must not re-collapse the two buckets."
93
+ },
94
+ {
95
+ "id": "fp/404-body-code-does-not-win",
96
+ "requirement": "FP-012",
97
+ "op": "fingerprint",
98
+ "input": {
99
+ "status": 404,
100
+ "method": "GET",
101
+ "route": "/users",
102
+ "responseBody": {
103
+ "code": "not_found"
104
+ }
105
+ },
106
+ "expected": {
107
+ "strategy": "endpoint",
108
+ "key": "404:endpoint"
109
+ }
110
+ },
111
+ {
112
+ "id": "fp/header-code",
113
+ "requirement": "FP-010",
114
+ "op": "fingerprint",
115
+ "input": {
116
+ "status": 400,
117
+ "method": "GET",
118
+ "route": "/car/{id}",
119
+ "responseHeaders": {
120
+ "x-restless-error-code": "invalid_param"
121
+ }
122
+ },
123
+ "expected": {
124
+ "strategy": "header",
125
+ "key": "400:invalid_param"
126
+ }
127
+ },
128
+ {
129
+ "id": "fp/header-case-insensitive",
130
+ "requirement": "FP-017",
131
+ "op": "fingerprint",
132
+ "input": {
133
+ "status": 400,
134
+ "responseHeaders": {
135
+ "X-Restless-Error-Code": "invalid_param"
136
+ }
137
+ },
138
+ "expected": {
139
+ "strategy": "header",
140
+ "key": "400:invalid_param"
141
+ }
142
+ },
143
+ {
144
+ "id": "fp/header-rejects-sentence",
145
+ "requirement": "FP-015",
146
+ "op": "fingerprint",
147
+ "input": {
148
+ "status": 400,
149
+ "method": "POST",
150
+ "route": "/pay",
151
+ "responseHeaders": {
152
+ "x-restless-error-code": "Your card was declined."
153
+ }
154
+ },
155
+ "expected": {
156
+ "strategy": "route-only",
157
+ "key": "400:POST:/pay"
158
+ },
159
+ "note": "Not identifier-shaped, so it is not a code and the ladder continues."
160
+ },
161
+ {
162
+ "id": "fp/header-rejects-leading-digit",
163
+ "requirement": "FP-015",
164
+ "op": "fingerprint",
165
+ "input": {
166
+ "status": 400,
167
+ "method": "POST",
168
+ "route": "/pay",
169
+ "responseHeaders": {
170
+ "x-restless-error-code": "4xx_bad"
171
+ }
172
+ },
173
+ "expected": {
174
+ "strategy": "route-only",
175
+ "key": "400:POST:/pay"
176
+ }
177
+ },
178
+ {
179
+ "id": "fp/header-rejects-too-long",
180
+ "requirement": "FP-015",
181
+ "op": "fingerprint",
182
+ "input": {
183
+ "status": 400,
184
+ "method": "POST",
185
+ "route": "/pay",
186
+ "responseHeaders": {
187
+ "x-restless-error-code": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
188
+ }
189
+ },
190
+ "expected": {
191
+ "strategy": "route-only",
192
+ "key": "400:POST:/pay"
193
+ }
194
+ },
195
+ {
196
+ "id": "fp/header-accepts-64",
197
+ "requirement": "FP-015",
198
+ "op": "fingerprint",
199
+ "input": {
200
+ "status": 400,
201
+ "responseHeaders": {
202
+ "x-restless-error-code": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
203
+ }
204
+ },
205
+ "expected": {
206
+ "strategy": "header",
207
+ "key": "400:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
208
+ }
209
+ },
210
+ {
211
+ "id": "fp/body-code",
212
+ "requirement": "FP-016",
213
+ "op": "fingerprint",
214
+ "input": {
215
+ "status": 402,
216
+ "method": "POST",
217
+ "route": "/pay",
218
+ "responseBody": {
219
+ "code": "card_declined"
220
+ }
221
+ },
222
+ "expected": {
223
+ "strategy": "body-code",
224
+ "key": "402:card_declined"
225
+ }
226
+ },
227
+ {
228
+ "id": "fp/body-error_code",
229
+ "requirement": "FP-016",
230
+ "op": "fingerprint",
231
+ "input": {
232
+ "status": 402,
233
+ "responseBody": {
234
+ "error_code": "card_declined"
235
+ }
236
+ },
237
+ "expected": {
238
+ "strategy": "body-code",
239
+ "key": "402:card_declined"
240
+ }
241
+ },
242
+ {
243
+ "id": "fp/body-errorCode",
244
+ "requirement": "FP-016",
245
+ "op": "fingerprint",
246
+ "input": {
247
+ "status": 402,
248
+ "responseBody": {
249
+ "errorCode": "card_declined"
250
+ }
251
+ },
252
+ "expected": {
253
+ "strategy": "body-code",
254
+ "key": "402:card_declined"
255
+ }
256
+ },
257
+ {
258
+ "id": "fp/body-type",
259
+ "requirement": "FP-016",
260
+ "op": "fingerprint",
261
+ "input": {
262
+ "status": 402,
263
+ "responseBody": {
264
+ "type": "card_error"
265
+ }
266
+ },
267
+ "expected": {
268
+ "strategy": "body-code",
269
+ "key": "402:card_error"
270
+ }
271
+ },
272
+ {
273
+ "id": "fp/body-nested-error-code",
274
+ "requirement": "FP-016",
275
+ "op": "fingerprint",
276
+ "input": {
277
+ "status": 402,
278
+ "responseBody": {
279
+ "error": {
280
+ "code": "card_declined"
281
+ }
282
+ }
283
+ },
284
+ "expected": {
285
+ "strategy": "body-code",
286
+ "key": "402:card_declined"
287
+ }
288
+ },
289
+ {
290
+ "id": "fp/body-nested-error-type",
291
+ "requirement": "FP-016",
292
+ "op": "fingerprint",
293
+ "input": {
294
+ "status": 402,
295
+ "responseBody": {
296
+ "error": {
297
+ "type": "card_error"
298
+ }
299
+ }
300
+ },
301
+ "expected": {
302
+ "strategy": "body-code",
303
+ "key": "402:card_error"
304
+ }
305
+ },
306
+ {
307
+ "id": "fp/body-code-precedence",
308
+ "requirement": "FP-016",
309
+ "op": "fingerprint",
310
+ "input": {
311
+ "status": 402,
312
+ "responseBody": {
313
+ "type": "b",
314
+ "code": "a"
315
+ }
316
+ },
317
+ "expected": {
318
+ "strategy": "body-code",
319
+ "key": "402:a"
320
+ },
321
+ "note": "Field order is fixed: code before error_code before errorCode before type."
322
+ },
323
+ {
324
+ "id": "fp/body-code-exact-case",
325
+ "requirement": "FP-016",
326
+ "op": "fingerprint",
327
+ "input": {
328
+ "status": 402,
329
+ "method": "POST",
330
+ "route": "/pay",
331
+ "responseBody": {
332
+ "Code": "card_declined"
333
+ }
334
+ },
335
+ "expected": {
336
+ "strategy": "route-only",
337
+ "key": "402:POST:/pay"
338
+ },
339
+ "note": "Code field names are matched case-SENSITIVELY. Unlike redaction keys, there is no normalization here."
340
+ },
341
+ {
342
+ "id": "fp/header-beats-body",
343
+ "requirement": "FP-010",
344
+ "op": "fingerprint",
345
+ "input": {
346
+ "status": 402,
347
+ "responseHeaders": {
348
+ "x-restless-error-code": "from_header"
349
+ },
350
+ "responseBody": {
351
+ "code": "from_body"
352
+ }
353
+ },
354
+ "expected": {
355
+ "strategy": "header",
356
+ "key": "402:from_header"
357
+ }
358
+ },
359
+ {
360
+ "id": "fp/stack-basic",
361
+ "requirement": "FP-040",
362
+ "op": "fingerprint",
363
+ "input": {
364
+ "status": 500,
365
+ "method": "GET",
366
+ "route": "/users",
367
+ "stackTrace": "Error: boom\n at findById (/Users/dev/proj/src/db/users.js:12:34)\n at handler (/Users/dev/proj/src/routes/users.js:5:1)"
368
+ },
369
+ "expected": {
370
+ "strategy": "stack",
371
+ "key": "500:src/db/users.js:findById"
372
+ }
373
+ },
374
+ {
375
+ "id": "fp/stack-path-machine-independent",
376
+ "requirement": "FP-042",
377
+ "op": "fingerprint",
378
+ "input": {
379
+ "status": 500,
380
+ "method": "GET",
381
+ "route": "/users",
382
+ "stackTrace": "Error: boom\n at findById (/srv/app/src/db/users.js:99:1)"
383
+ },
384
+ "expected": {
385
+ "strategy": "stack",
386
+ "key": "500:src/db/users.js:findById"
387
+ },
388
+ "note": "Same key as fp/stack-basic, which is the whole point of FP-042: a different absolute prefix must not change the fingerprint. Was broken until FP-042 switched from first-match to last-match."
389
+ },
390
+ {
391
+ "id": "fp/stack-skips-vendor",
392
+ "requirement": "FP-043",
393
+ "op": "fingerprint",
394
+ "input": {
395
+ "status": 500,
396
+ "stackTrace": "Error: boom\n at wrap (/proj/node_modules/express/lib/router.js:1:1)\n at emit (node:internal/events:100:1)\n at real (/proj/src/db/users.js:12:34)"
397
+ },
398
+ "expected": {
399
+ "strategy": "stack",
400
+ "key": "500:src/db/users.js:real"
401
+ }
402
+ },
403
+ {
404
+ "id": "fp/stack-anonymous-frame",
405
+ "requirement": "FP-045",
406
+ "op": "fingerprint",
407
+ "input": {
408
+ "status": 500,
409
+ "stackTrace": "Error: boom\n at /proj/src/db/users.js:12:34"
410
+ },
411
+ "expected": {
412
+ "strategy": "stack",
413
+ "key": "500:src/db/users.js:anonymous"
414
+ }
415
+ },
416
+ {
417
+ "id": "fp/stack-no-known-dir",
418
+ "requirement": "FP-042",
419
+ "op": "fingerprint",
420
+ "input": {
421
+ "status": 500,
422
+ "stackTrace": "Error: boom\n at go (/opt/weird/place/thing.js:3:1)"
423
+ },
424
+ "expected": {
425
+ "strategy": "stack",
426
+ "key": "500:place/thing.js:go"
427
+ },
428
+ "note": "No src/lib/app/api/routes/controllers/handlers segment, so fall back to the last two path components."
429
+ },
430
+ {
431
+ "id": "fp/stack-array-form",
432
+ "requirement": "FP-040",
433
+ "op": "fingerprint",
434
+ "input": {
435
+ "status": 500,
436
+ "stackTrace": [
437
+ "Error: boom",
438
+ " at findById (/proj/src/db/users.js:12:34)"
439
+ ]
440
+ },
441
+ "expected": {
442
+ "strategy": "stack",
443
+ "key": "500:src/db/users.js:findById"
444
+ }
445
+ },
446
+ {
447
+ "id": "fp/stack-not-used-for-4xx",
448
+ "requirement": "FP-010",
449
+ "op": "fingerprint",
450
+ "input": {
451
+ "status": 400,
452
+ "method": "GET",
453
+ "route": "/users",
454
+ "stackTrace": "Error: boom\n at findById (/proj/src/db/users.js:12:34)"
455
+ },
456
+ "expected": {
457
+ "strategy": "route-only",
458
+ "key": "400:GET:/users"
459
+ },
460
+ "note": "The stack strategy is 5xx only."
461
+ },
462
+ {
463
+ "id": "fp/message-basic",
464
+ "requirement": "FP-010",
465
+ "op": "fingerprint",
466
+ "input": {
467
+ "status": 400,
468
+ "method": "POST",
469
+ "route": "/users",
470
+ "responseBody": {
471
+ "message": "Cannot read property of undefined"
472
+ }
473
+ },
474
+ "expected": {
475
+ "strategy": "message",
476
+ "key": "400:POST:/users:cannot-read-property-of-undefined"
477
+ }
478
+ },
479
+ {
480
+ "id": "fp/message-strips-ids",
481
+ "requirement": "FP-021",
482
+ "op": "fingerprint",
483
+ "input": {
484
+ "status": 400,
485
+ "method": "POST",
486
+ "route": "/users",
487
+ "responseBody": {
488
+ "message": "User user_abc123 not found in workspace 42"
489
+ }
490
+ },
491
+ "expected": {
492
+ "strategy": "message",
493
+ "key": "400:POST:/users:user-not-found-in-workspace"
494
+ }
495
+ },
496
+ {
497
+ "id": "fp/message-nested",
498
+ "requirement": "FP-018",
499
+ "op": "fingerprint",
500
+ "input": {
501
+ "status": 400,
502
+ "method": "POST",
503
+ "route": "/users",
504
+ "responseBody": {
505
+ "error": {
506
+ "message": "Something broke badly here"
507
+ }
508
+ }
509
+ },
510
+ "expected": {
511
+ "strategy": "message",
512
+ "key": "400:POST:/users:something-broke-badly-here"
513
+ }
514
+ },
515
+ {
516
+ "id": "fp/message-error-as-string",
517
+ "requirement": "FP-018",
518
+ "op": "fingerprint",
519
+ "input": {
520
+ "status": 400,
521
+ "method": "POST",
522
+ "route": "/users",
523
+ "responseBody": {
524
+ "error": "Something broke badly here"
525
+ }
526
+ },
527
+ "expected": {
528
+ "strategy": "message",
529
+ "key": "400:POST:/users:something-broke-badly-here"
530
+ }
531
+ },
532
+ {
533
+ "id": "fp/message-body-as-string",
534
+ "requirement": "FP-018",
535
+ "op": "fingerprint",
536
+ "input": {
537
+ "status": 400,
538
+ "method": "POST",
539
+ "route": "/users",
540
+ "responseBody": "Something broke badly here"
541
+ },
542
+ "expected": {
543
+ "strategy": "message",
544
+ "key": "400:POST:/users:something-broke-badly-here"
545
+ }
546
+ },
547
+ {
548
+ "id": "fp/message-empty-falls-through",
549
+ "requirement": "FP-022",
550
+ "op": "fingerprint",
551
+ "input": {
552
+ "status": 400,
553
+ "method": "POST",
554
+ "route": "/users",
555
+ "responseBody": {
556
+ "message": "42 1 7"
557
+ }
558
+ },
559
+ "expected": {
560
+ "strategy": "route-only",
561
+ "key": "400:POST:/users"
562
+ },
563
+ "note": "Everything normalizes away, so the message strategy does not fire and we land on route-only."
564
+ },
565
+ {
566
+ "id": "fp/route-only",
567
+ "requirement": "FP-010",
568
+ "op": "fingerprint",
569
+ "input": {
570
+ "status": 503,
571
+ "method": "GET",
572
+ "route": "/health"
573
+ },
574
+ "expected": {
575
+ "strategy": "route-only",
576
+ "key": "503:GET:/health"
577
+ }
578
+ },
579
+ {
580
+ "id": "fp/route-only-no-method",
581
+ "requirement": "FP-011",
582
+ "op": "fingerprint",
583
+ "input": {
584
+ "status": 503,
585
+ "route": "/health"
586
+ },
587
+ "expected": {
588
+ "strategy": "route-only",
589
+ "key": "503:GET:/health"
590
+ },
591
+ "note": "Absent method defaults to GET."
592
+ },
593
+ {
594
+ "id": "fp/route-only-no-route",
595
+ "requirement": "FP-032",
596
+ "op": "fingerprint",
597
+ "input": {
598
+ "status": 503,
599
+ "method": "GET"
600
+ },
601
+ "expected": {
602
+ "strategy": "route-only",
603
+ "key": "503:GET:/"
604
+ }
605
+ },
606
+ {
607
+ "id": "route/numeric",
608
+ "requirement": "FP-030",
609
+ "op": "normalizeRoute",
610
+ "input": {
611
+ "route": "/users/123"
612
+ },
613
+ "expected": "/users/:id"
614
+ },
615
+ {
616
+ "id": "route/uuid",
617
+ "requirement": "FP-030",
618
+ "op": "normalizeRoute",
619
+ "input": {
620
+ "route": "/users/550e8400-e29b-41d4-a716-446655440000"
621
+ },
622
+ "expected": "/users/:id"
623
+ },
624
+ {
625
+ "id": "route/uuid-uppercase",
626
+ "requirement": "FP-030",
627
+ "op": "normalizeRoute",
628
+ "input": {
629
+ "route": "/users/550E8400-E29B-41D4-A716-446655440000"
630
+ },
631
+ "expected": "/users/:id"
632
+ },
633
+ {
634
+ "id": "route/long-hex",
635
+ "requirement": "FP-030",
636
+ "op": "normalizeRoute",
637
+ "input": {
638
+ "route": "/blobs/deadbeefdeadbeef"
639
+ },
640
+ "expected": "/blobs/:id"
641
+ },
642
+ {
643
+ "id": "route/short-hex-untouched",
644
+ "requirement": "FP-030",
645
+ "op": "normalizeRoute",
646
+ "input": {
647
+ "route": "/blobs/deadbeefdeadbee"
648
+ },
649
+ "expected": "/blobs/deadbeefdeadbee",
650
+ "note": "15 hex chars is one below the 16 threshold, so it stays."
651
+ },
652
+ {
653
+ "id": "route/multiple-segments",
654
+ "requirement": "FP-031",
655
+ "op": "normalizeRoute",
656
+ "input": {
657
+ "route": "/users/1/posts/2/comments/3"
658
+ },
659
+ "expected": "/users/:id/posts/:id/comments/:id"
660
+ },
661
+ {
662
+ "id": "route/already-templated",
663
+ "requirement": "FP-030",
664
+ "op": "normalizeRoute",
665
+ "input": {
666
+ "route": "/users/{id}"
667
+ },
668
+ "expected": "/users/{id}"
669
+ },
670
+ {
671
+ "id": "route/already-colon",
672
+ "requirement": "FP-030",
673
+ "op": "normalizeRoute",
674
+ "input": {
675
+ "route": "/users/:id"
676
+ },
677
+ "expected": "/users/:id"
678
+ },
679
+ {
680
+ "id": "route/trailing-slash",
681
+ "requirement": "FP-031",
682
+ "op": "normalizeRoute",
683
+ "input": {
684
+ "route": "/users/123/"
685
+ },
686
+ "expected": "/users/:id/"
687
+ },
688
+ {
689
+ "id": "route/index-zero-exempt",
690
+ "requirement": "FP-031",
691
+ "op": "normalizeRoute",
692
+ "input": {
693
+ "route": "123"
694
+ },
695
+ "expected": "123",
696
+ "note": "The text before the first slash is never normalized. Preserved deliberately so stored fingerprints stay stable."
697
+ },
698
+ {
699
+ "id": "route/index-zero-exempt-with-tail",
700
+ "requirement": "FP-031",
701
+ "op": "normalizeRoute",
702
+ "input": {
703
+ "route": "123/456"
704
+ },
705
+ "expected": "123/:id"
706
+ },
707
+ {
708
+ "id": "route/empty",
709
+ "requirement": "FP-032",
710
+ "op": "normalizeRoute",
711
+ "input": {
712
+ "route": ""
713
+ },
714
+ "expected": "/"
715
+ },
716
+ {
717
+ "id": "route/null",
718
+ "requirement": "FP-032",
719
+ "op": "normalizeRoute",
720
+ "input": {
721
+ "route": null
722
+ },
723
+ "expected": "/"
724
+ },
725
+ {
726
+ "id": "route/double-slash",
727
+ "requirement": "FP-031",
728
+ "op": "normalizeRoute",
729
+ "input": {
730
+ "route": "//users//123"
731
+ },
732
+ "expected": "//users//:id"
733
+ },
734
+ {
735
+ "id": "route/zero",
736
+ "requirement": "FP-030",
737
+ "op": "normalizeRoute",
738
+ "input": {
739
+ "route": "/users/0"
740
+ },
741
+ "expected": "/users/:id"
742
+ },
743
+ {
744
+ "id": "route/leading-zeros",
745
+ "requirement": "FP-030",
746
+ "op": "normalizeRoute",
747
+ "input": {
748
+ "route": "/users/007"
749
+ },
750
+ "expected": "/users/:id"
751
+ },
752
+ {
753
+ "id": "route/segment-with-trailing-newline",
754
+ "requirement": "PRIM-005",
755
+ "op": "normalizeRoute",
756
+ "input": {
757
+ "route": "/users/5\n"
758
+ },
759
+ "expected": "/users/5\n",
760
+ "note": "Found by differential fuzz. Python's `$` also matches before a trailing newline, so ^[0-9]+$ accepted \"5\\n\" and normalized it to :id while JS left it alone. Anchored patterns must match the WHOLE string (re.fullmatch)."
761
+ },
762
+ {
763
+ "id": "route/not-quite-uuid",
764
+ "requirement": "FP-030",
765
+ "op": "normalizeRoute",
766
+ "input": {
767
+ "route": "/users/550e8400-e29b-41d4-a716-44665544000"
768
+ },
769
+ "expected": "/users/550e8400-e29b-41d4-a716-44665544000",
770
+ "note": "One char short of a UUID, and only 35 chars so not long-hex either. Stays."
771
+ },
772
+ {
773
+ "id": "path/src",
774
+ "requirement": "FP-042",
775
+ "op": "projectRelative",
776
+ "input": {
777
+ "file": "/Users/dev/proj/src/db/users.js"
778
+ },
779
+ "expected": "src/db/users.js"
780
+ },
781
+ {
782
+ "id": "path/src-other-machine",
783
+ "requirement": "FP-042",
784
+ "op": "projectRelative",
785
+ "input": {
786
+ "file": "/srv/app/src/db/users.js"
787
+ },
788
+ "expected": "src/db/users.js",
789
+ "note": "Must equal path/src. A first-match rule returns app/src/db/users.js here, because the deploy root IS the first project dir."
790
+ },
791
+ {
792
+ "id": "path/docker-root",
793
+ "requirement": "FP-042",
794
+ "op": "projectRelative",
795
+ "input": {
796
+ "file": "/app/src/db/users.js"
797
+ },
798
+ "expected": "src/db/users.js",
799
+ "note": "Docker WORKDIR /app, the most common containerized layout there is, and the one a first-match rule breaks hardest."
800
+ },
801
+ {
802
+ "id": "path/lib",
803
+ "requirement": "FP-042",
804
+ "op": "projectRelative",
805
+ "input": {
806
+ "file": "/opt/x/lib/thing.py"
807
+ },
808
+ "expected": "lib/thing.py"
809
+ },
810
+ {
811
+ "id": "path/routes",
812
+ "requirement": "FP-042",
813
+ "op": "projectRelative",
814
+ "input": {
815
+ "file": "/a/b/routes/users.rb"
816
+ },
817
+ "expected": "routes/users.rb"
818
+ },
819
+ {
820
+ "id": "path/controllers",
821
+ "requirement": "FP-042",
822
+ "op": "projectRelative",
823
+ "input": {
824
+ "file": "/a/controllers/x.php"
825
+ },
826
+ "expected": "controllers/x.php"
827
+ },
828
+ {
829
+ "id": "path/handlers",
830
+ "requirement": "FP-042",
831
+ "op": "projectRelative",
832
+ "input": {
833
+ "file": "/a/handlers/x.go"
834
+ },
835
+ "expected": "handlers/x.go"
836
+ },
837
+ {
838
+ "id": "path/api",
839
+ "requirement": "FP-042",
840
+ "op": "projectRelative",
841
+ "input": {
842
+ "file": "/a/api/x.js"
843
+ },
844
+ "expected": "api/x.js"
845
+ },
846
+ {
847
+ "id": "path/app",
848
+ "requirement": "FP-042",
849
+ "op": "projectRelative",
850
+ "input": {
851
+ "file": "/a/app/x.js"
852
+ },
853
+ "expected": "app/x.js"
854
+ },
855
+ {
856
+ "id": "path/no-known-dir",
857
+ "requirement": "FP-042",
858
+ "op": "projectRelative",
859
+ "input": {
860
+ "file": "/opt/weird/place/thing.js"
861
+ },
862
+ "expected": "place/thing.js",
863
+ "note": "Falls back to the last two path components."
864
+ },
865
+ {
866
+ "id": "path/nested-collapses-to-last",
867
+ "requirement": "FP-042",
868
+ "op": "projectRelative",
869
+ "input": {
870
+ "file": "/a/src/b/src/c.js"
871
+ },
872
+ "expected": "src/c.js",
873
+ "note": "The LAST project-dir match wins. Collapsing further than a first-match rule is the accepted trade for machine independence."
874
+ },
875
+ {
876
+ "id": "path/bare-filename",
877
+ "requirement": "FP-042",
878
+ "op": "projectRelative",
879
+ "input": {
880
+ "file": "thing.js"
881
+ },
882
+ "expected": "thing.js"
883
+ },
884
+ {
885
+ "id": "path/single-segment",
886
+ "requirement": "FP-042",
887
+ "op": "projectRelative",
888
+ "input": {
889
+ "file": "/thing.js"
890
+ },
891
+ "expected": "/thing.js"
892
+ },
893
+ {
894
+ "id": "msg/basic",
895
+ "requirement": "FP-020",
896
+ "op": "normalizeMessage",
897
+ "input": {
898
+ "message": "Cannot read property of undefined"
899
+ },
900
+ "expected": "cannot-read-property-of-undefined"
901
+ },
902
+ {
903
+ "id": "msg/lowercases",
904
+ "requirement": "PRIM-020",
905
+ "op": "normalizeMessage",
906
+ "input": {
907
+ "message": "CANNOT READ PROPERTY"
908
+ },
909
+ "expected": "cannot-read-property"
910
+ },
911
+ {
912
+ "id": "msg/strips-url",
913
+ "requirement": "FP-020",
914
+ "op": "normalizeMessage",
915
+ "input": {
916
+ "message": "failed calling https://api.example.com/v1/x?a=1 twice"
917
+ },
918
+ "expected": "failed-calling-twice"
919
+ },
920
+ {
921
+ "id": "msg/strips-email",
922
+ "requirement": "FP-020",
923
+ "op": "normalizeMessage",
924
+ "input": {
925
+ "message": "no account for jane.doe@example.com found"
926
+ },
927
+ "expected": "no-account-for-found"
928
+ },
929
+ {
930
+ "id": "msg/strips-quoted",
931
+ "requirement": "FP-020",
932
+ "op": "normalizeMessage",
933
+ "input": {
934
+ "message": "column 'user_name' does not exist"
935
+ },
936
+ "expected": "column-does-not-exist"
937
+ },
938
+ {
939
+ "id": "msg/strips-double-quoted",
940
+ "requirement": "FP-020",
941
+ "op": "normalizeMessage",
942
+ "input": {
943
+ "message": "column \"user_name\" does not exist"
944
+ },
945
+ "expected": "column-does-not-exist"
946
+ },
947
+ {
948
+ "id": "msg/strips-digit-words",
949
+ "requirement": "FP-021",
950
+ "op": "normalizeMessage",
951
+ "input": {
952
+ "message": "token sk_live_4242 rejected for user abc123"
953
+ },
954
+ "expected": "token-rejected-for-user"
955
+ },
956
+ {
957
+ "id": "msg/digit-word-not-just-digit",
958
+ "requirement": "FP-021",
959
+ "op": "normalizeMessage",
960
+ "input": {
961
+ "message": "prefix abc123 suffix"
962
+ },
963
+ "expected": "prefix-suffix",
964
+ "note": "The whole word goes, not just the digits. Otherwise 'abc' would survive and still influence the key."
965
+ },
966
+ {
967
+ "id": "msg/hyphenated-digit-word",
968
+ "requirement": "FP-021",
969
+ "op": "normalizeMessage",
970
+ "input": {
971
+ "message": "request id abc-123-def failed"
972
+ },
973
+ "expected": "request-id-failed"
974
+ },
975
+ {
976
+ "id": "msg/underscore-is-word-char",
977
+ "requirement": "PRIM-001",
978
+ "op": "normalizeMessage",
979
+ "input": {
980
+ "message": "user_123_name missing"
981
+ },
982
+ "expected": "missing"
983
+ },
984
+ {
985
+ "id": "msg/drops-short-tokens",
986
+ "requirement": "FP-020",
987
+ "op": "normalizeMessage",
988
+ "input": {
989
+ "message": "a bb c ddd e ffff"
990
+ },
991
+ "expected": "bb-ddd-ffff",
992
+ "note": "Tokens of length 1 are dropped."
993
+ },
994
+ {
995
+ "id": "msg/caps-at-six",
996
+ "requirement": "FP-020",
997
+ "op": "normalizeMessage",
998
+ "input": {
999
+ "message": "one two three four five six seven eight nine"
1000
+ },
1001
+ "expected": "one-two-three-four-five-six"
1002
+ },
1003
+ {
1004
+ "id": "msg/empty",
1005
+ "requirement": "FP-022",
1006
+ "op": "normalizeMessage",
1007
+ "input": {
1008
+ "message": ""
1009
+ },
1010
+ "expected": ""
1011
+ },
1012
+ {
1013
+ "id": "msg/all-stripped",
1014
+ "requirement": "FP-022",
1015
+ "op": "normalizeMessage",
1016
+ "input": {
1017
+ "message": "42 7 1"
1018
+ },
1019
+ "expected": ""
1020
+ },
1021
+ {
1022
+ "id": "msg/nbsp-whitespace",
1023
+ "requirement": "PRIM-002",
1024
+ "op": "normalizeMessage",
1025
+ "input": {
1026
+ "message": "failed to connect"
1027
+ },
1028
+ "expected": "failed-to-connect",
1029
+ "note": "NBSP is in WS. A port using ASCII-only \\s would treat it as punctuation instead, and while both routes happen to yield a space here, the class must still be right."
1030
+ },
1031
+ {
1032
+ "id": "msg/unicode-word-chars-stripped",
1033
+ "requirement": "PRIM-001",
1034
+ "op": "normalizeMessage",
1035
+ "input": {
1036
+ "message": "erreur café serveur"
1037
+ },
1038
+ "expected": "erreur-caf-serveur",
1039
+ "note": "WORD is ASCII-only. Python's default Unicode-aware \\w would keep the é and produce a different key."
1040
+ },
1041
+ {
1042
+ "id": "msg/cjk-stripped",
1043
+ "requirement": "PRIM-001",
1044
+ "op": "normalizeMessage",
1045
+ "input": {
1046
+ "message": "接続 failed 完全に"
1047
+ },
1048
+ "expected": "failed"
1049
+ },
1050
+ {
1051
+ "id": "msg/turkish-dotted-i",
1052
+ "requirement": "PRIM-020",
1053
+ "op": "normalizeMessage",
1054
+ "input": {
1055
+ "message": "İSTANBUL connection failed"
1056
+ },
1057
+ "expected": "stanbul-connection-failed",
1058
+ "note": "Locale-independent lowercase. A Turkish-locale mapping would differ."
1059
+ },
1060
+ {
1061
+ "id": "msg/final-sigma",
1062
+ "requirement": "PRIM-021",
1063
+ "op": "normalizeMessage",
1064
+ "input": {
1065
+ "message": "ΑΣ failed"
1066
+ },
1067
+ "expected": "failed",
1068
+ "note": "DOES NOT DISCRIMINATE - kept as executable documentation. Greek is not in WORD, so step 6 strips both sigma forms to a space before either reaches a token. A Go driver without Final_Sigma passes this. See the PRIM-021 note."
1069
+ },
1070
+ {
1071
+ "id": "msg/medial-sigma",
1072
+ "requirement": "PRIM-021",
1073
+ "op": "normalizeMessage",
1074
+ "input": {
1075
+ "message": "ΑΣΑ failed"
1076
+ },
1077
+ "expected": "failed",
1078
+ "note": "Companion to msg/final-sigma, and equally non-discriminating. Both exist so that if a future change ever puts lowercased non-ASCII on the wire, these cases start failing loudly instead of silently diverging."
1079
+ },
1080
+ {
1081
+ "id": "msg/eszett",
1082
+ "requirement": "PRIM-020",
1083
+ "op": "normalizeMessage",
1084
+ "input": {
1085
+ "message": "STRASSE ausfall"
1086
+ },
1087
+ "expected": "strasse-ausfall",
1088
+ "note": "Simple lowercase, not full case folding: casefold would map ß to ss and change tokenization."
1089
+ },
1090
+ {
1091
+ "id": "msg/emoji",
1092
+ "requirement": "FP-020",
1093
+ "op": "normalizeMessage",
1094
+ "input": {
1095
+ "message": "deploy 🚀 failed badly"
1096
+ },
1097
+ "expected": "deploy-failed-badly"
1098
+ },
1099
+ {
1100
+ "id": "msg/punctuation-collapse",
1101
+ "requirement": "FP-020",
1102
+ "op": "normalizeMessage",
1103
+ "input": {
1104
+ "message": "one...two,,,three!!!four"
1105
+ },
1106
+ "expected": "one-two-three-four"
1107
+ },
1108
+ {
1109
+ "id": "msg/hyphen-preserved",
1110
+ "requirement": "FP-020",
1111
+ "op": "normalizeMessage",
1112
+ "input": {
1113
+ "message": "not-found while resolving"
1114
+ },
1115
+ "expected": "not-found-while-resolving",
1116
+ "note": "Hyphen is neither WORD nor punctuation-to-strip; it survives inside a token."
1117
+ }
1118
+ ]
1119
+ }