@particle-academy/fancy-conformance 0.13.0 → 0.14.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.
package/VERSION CHANGED
@@ -1 +1 @@
1
- 0.13.0
1
+ 0.14.0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@particle-academy/fancy-conformance",
3
- "version": "0.13.0",
3
+ "version": "0.14.0",
4
4
  "description": "Shared cross-language conformance fixtures for the Fancy suite. One contract, N implementations, and a single table that every implementation asserts in its own CI \u2014 so 'parity' is a test result rather than a claim. Ships the fixture data itself, so a Rust, Go or Python runner can consume it without a JavaScript toolchain.",
5
5
  "repository": {
6
6
  "type": "git",
@@ -0,0 +1,1248 @@
1
+ {
2
+ "$schema": "../../../schema/case-table.schema.json",
3
+ "suite": "expr/evaluate",
4
+ "cases": [
5
+ {
6
+ "id": "0101-simple-path",
7
+ "title": "A dot-path resolves.",
8
+ "since": "0.14.0",
9
+ "tags": [
10
+ "expr"
11
+ ],
12
+ "input": {
13
+ "expression": "in.content",
14
+ "context": {
15
+ "in": {
16
+ "content": "hello",
17
+ "count": 3,
18
+ "deal_id": "D-1",
19
+ "transcript": null,
20
+ "items": [
21
+ 1,
22
+ 2
23
+ ],
24
+ "empty": [],
25
+ "obj": {},
26
+ "flag": true,
27
+ "zero": 0,
28
+ "blank": ""
29
+ },
30
+ "user": {
31
+ "name": "Ada"
32
+ }
33
+ }
34
+ },
35
+ "expected": {
36
+ "ok": true,
37
+ "value": "hello"
38
+ }
39
+ },
40
+ {
41
+ "id": "0102-nested-path",
42
+ "title": "Nesting resolves.",
43
+ "since": "0.14.0",
44
+ "tags": [
45
+ "expr"
46
+ ],
47
+ "input": {
48
+ "expression": "user.name",
49
+ "context": {
50
+ "in": {
51
+ "content": "hello",
52
+ "count": 3,
53
+ "deal_id": "D-1",
54
+ "transcript": null,
55
+ "items": [
56
+ 1,
57
+ 2
58
+ ],
59
+ "empty": [],
60
+ "obj": {},
61
+ "flag": true,
62
+ "zero": 0,
63
+ "blank": ""
64
+ },
65
+ "user": {
66
+ "name": "Ada"
67
+ }
68
+ }
69
+ },
70
+ "expected": {
71
+ "ok": true,
72
+ "value": "Ada"
73
+ }
74
+ },
75
+ {
76
+ "id": "0103-absent-path-is-null",
77
+ "title": "An absent path is null, NOT an error.",
78
+ "since": "0.14.0",
79
+ "tags": [
80
+ "expr",
81
+ "the-point"
82
+ ],
83
+ "input": {
84
+ "expression": "in.nope",
85
+ "context": {
86
+ "in": {
87
+ "content": "hello",
88
+ "count": 3,
89
+ "deal_id": "D-1",
90
+ "transcript": null,
91
+ "items": [
92
+ 1,
93
+ 2
94
+ ],
95
+ "empty": [],
96
+ "obj": {},
97
+ "flag": true,
98
+ "zero": 0,
99
+ "blank": ""
100
+ },
101
+ "user": {
102
+ "name": "Ada"
103
+ }
104
+ }
105
+ },
106
+ "expected": {
107
+ "ok": true,
108
+ "value": null
109
+ },
110
+ "notes": "The legitimate null, and the one this grammar keeps. A path that is absent today may be present tomorrow; that is not a malformed expression and must never throw."
111
+ },
112
+ {
113
+ "id": "0104-bracket-index",
114
+ "title": "A bracket index resolves.",
115
+ "since": "0.14.0",
116
+ "tags": [
117
+ "expr"
118
+ ],
119
+ "input": {
120
+ "expression": "in.items[0]",
121
+ "context": {
122
+ "in": {
123
+ "content": "hello",
124
+ "count": 3,
125
+ "deal_id": "D-1",
126
+ "transcript": null,
127
+ "items": [
128
+ 1,
129
+ 2
130
+ ],
131
+ "empty": [],
132
+ "obj": {},
133
+ "flag": true,
134
+ "zero": 0,
135
+ "blank": ""
136
+ },
137
+ "user": {
138
+ "name": "Ada"
139
+ }
140
+ }
141
+ },
142
+ "expected": {
143
+ "ok": true,
144
+ "value": 1
145
+ }
146
+ },
147
+ {
148
+ "id": "0105-absent-through-null",
149
+ "title": "Walking through a null yields null rather than throwing.",
150
+ "since": "0.14.0",
151
+ "tags": [
152
+ "expr"
153
+ ],
154
+ "input": {
155
+ "expression": "in.transcript.anything",
156
+ "context": {
157
+ "in": {
158
+ "content": "hello",
159
+ "count": 3,
160
+ "deal_id": "D-1",
161
+ "transcript": null,
162
+ "items": [
163
+ 1,
164
+ 2
165
+ ],
166
+ "empty": [],
167
+ "obj": {},
168
+ "flag": true,
169
+ "zero": 0,
170
+ "blank": ""
171
+ },
172
+ "user": {
173
+ "name": "Ada"
174
+ }
175
+ }
176
+ },
177
+ "expected": {
178
+ "ok": true,
179
+ "value": null
180
+ }
181
+ },
182
+ {
183
+ "id": "0201-number",
184
+ "title": "A number literal.",
185
+ "since": "0.14.0",
186
+ "tags": [
187
+ "expr"
188
+ ],
189
+ "input": {
190
+ "expression": "42",
191
+ "context": {}
192
+ },
193
+ "expected": {
194
+ "ok": true,
195
+ "value": 42
196
+ }
197
+ },
198
+ {
199
+ "id": "0202-string",
200
+ "title": "A string literal.",
201
+ "since": "0.14.0",
202
+ "tags": [
203
+ "expr"
204
+ ],
205
+ "input": {
206
+ "expression": "'hi'",
207
+ "context": {}
208
+ },
209
+ "expected": {
210
+ "ok": true,
211
+ "value": "hi"
212
+ }
213
+ },
214
+ {
215
+ "id": "0203-true",
216
+ "title": "true.",
217
+ "since": "0.14.0",
218
+ "tags": [
219
+ "expr"
220
+ ],
221
+ "input": {
222
+ "expression": "true",
223
+ "context": {}
224
+ },
225
+ "expected": {
226
+ "ok": true,
227
+ "value": true
228
+ }
229
+ },
230
+ {
231
+ "id": "0204-false",
232
+ "title": "false.",
233
+ "since": "0.14.0",
234
+ "tags": [
235
+ "expr"
236
+ ],
237
+ "input": {
238
+ "expression": "false",
239
+ "context": {}
240
+ },
241
+ "expected": {
242
+ "ok": true,
243
+ "value": false
244
+ }
245
+ },
246
+ {
247
+ "id": "0205-null",
248
+ "title": "null.",
249
+ "since": "0.14.0",
250
+ "tags": [
251
+ "expr"
252
+ ],
253
+ "input": {
254
+ "expression": "null",
255
+ "context": {}
256
+ },
257
+ "expected": {
258
+ "ok": true,
259
+ "value": null
260
+ }
261
+ },
262
+ {
263
+ "id": "0301-empty-array-is-truthy",
264
+ "title": "An EMPTY ARRAY is truthy.",
265
+ "since": "0.14.0",
266
+ "tags": [
267
+ "expr",
268
+ "truthiness",
269
+ "deliberate"
270
+ ],
271
+ "input": {
272
+ "expression": "in.empty ? 'yes' : 'no'",
273
+ "context": {
274
+ "in": {
275
+ "content": "hello",
276
+ "count": 3,
277
+ "deal_id": "D-1",
278
+ "transcript": null,
279
+ "items": [
280
+ 1,
281
+ 2
282
+ ],
283
+ "empty": [],
284
+ "obj": {},
285
+ "flag": true,
286
+ "zero": 0,
287
+ "blank": ""
288
+ },
289
+ "user": {
290
+ "name": "Ada"
291
+ }
292
+ }
293
+ },
294
+ "expected": {
295
+ "ok": true,
296
+ "value": "yes"
297
+ },
298
+ "notes": "Deliberate, and against PHP's and Python's native instincts, both of which call an empty container false. The data arrived as JSON: an array that EXISTS is a value, and asking whether it is empty is what `.length` is for. Conflating the two turns 'did we get results?' into 'did the call succeed?'."
299
+ },
300
+ {
301
+ "id": "0302-empty-object-is-truthy",
302
+ "title": "An EMPTY OBJECT is truthy, for the same reason.",
303
+ "since": "0.14.0",
304
+ "tags": [
305
+ "expr",
306
+ "truthiness",
307
+ "deliberate"
308
+ ],
309
+ "input": {
310
+ "expression": "in.obj ? 'yes' : 'no'",
311
+ "context": {
312
+ "in": {
313
+ "content": "hello",
314
+ "count": 3,
315
+ "deal_id": "D-1",
316
+ "transcript": null,
317
+ "items": [
318
+ 1,
319
+ 2
320
+ ],
321
+ "empty": [],
322
+ "obj": {},
323
+ "flag": true,
324
+ "zero": 0,
325
+ "blank": ""
326
+ },
327
+ "user": {
328
+ "name": "Ada"
329
+ }
330
+ }
331
+ },
332
+ "expected": {
333
+ "ok": true,
334
+ "value": "yes"
335
+ }
336
+ },
337
+ {
338
+ "id": "0303-zero-is-falsy",
339
+ "title": "0 is falsy.",
340
+ "since": "0.14.0",
341
+ "tags": [
342
+ "expr",
343
+ "truthiness"
344
+ ],
345
+ "input": {
346
+ "expression": "in.zero ? 'yes' : 'no'",
347
+ "context": {
348
+ "in": {
349
+ "content": "hello",
350
+ "count": 3,
351
+ "deal_id": "D-1",
352
+ "transcript": null,
353
+ "items": [
354
+ 1,
355
+ 2
356
+ ],
357
+ "empty": [],
358
+ "obj": {},
359
+ "flag": true,
360
+ "zero": 0,
361
+ "blank": ""
362
+ },
363
+ "user": {
364
+ "name": "Ada"
365
+ }
366
+ }
367
+ },
368
+ "expected": {
369
+ "ok": true,
370
+ "value": "no"
371
+ }
372
+ },
373
+ {
374
+ "id": "0304-empty-string-is-falsy",
375
+ "title": "An empty string is falsy.",
376
+ "since": "0.14.0",
377
+ "tags": [
378
+ "expr",
379
+ "truthiness"
380
+ ],
381
+ "input": {
382
+ "expression": "in.blank ? 'yes' : 'no'",
383
+ "context": {
384
+ "in": {
385
+ "content": "hello",
386
+ "count": 3,
387
+ "deal_id": "D-1",
388
+ "transcript": null,
389
+ "items": [
390
+ 1,
391
+ 2
392
+ ],
393
+ "empty": [],
394
+ "obj": {},
395
+ "flag": true,
396
+ "zero": 0,
397
+ "blank": ""
398
+ },
399
+ "user": {
400
+ "name": "Ada"
401
+ }
402
+ }
403
+ },
404
+ "expected": {
405
+ "ok": true,
406
+ "value": "no"
407
+ }
408
+ },
409
+ {
410
+ "id": "0305-null-is-falsy",
411
+ "title": "null is falsy.",
412
+ "since": "0.14.0",
413
+ "tags": [
414
+ "expr",
415
+ "truthiness"
416
+ ],
417
+ "input": {
418
+ "expression": "in.transcript ? 'yes' : 'no'",
419
+ "context": {
420
+ "in": {
421
+ "content": "hello",
422
+ "count": 3,
423
+ "deal_id": "D-1",
424
+ "transcript": null,
425
+ "items": [
426
+ 1,
427
+ 2
428
+ ],
429
+ "empty": [],
430
+ "obj": {},
431
+ "flag": true,
432
+ "zero": 0,
433
+ "blank": ""
434
+ },
435
+ "user": {
436
+ "name": "Ada"
437
+ }
438
+ }
439
+ },
440
+ "expected": {
441
+ "ok": true,
442
+ "value": "no"
443
+ }
444
+ },
445
+ {
446
+ "id": "0401-or-returns-the-operand",
447
+ "title": "`||` returns the OPERAND, not a boolean.",
448
+ "since": "0.14.0",
449
+ "tags": [
450
+ "expr",
451
+ "short-circuit"
452
+ ],
453
+ "input": {
454
+ "expression": "in.transcript || in.content",
455
+ "context": {
456
+ "in": {
457
+ "content": "hello",
458
+ "count": 3,
459
+ "deal_id": "D-1",
460
+ "transcript": null,
461
+ "items": [
462
+ 1,
463
+ 2
464
+ ],
465
+ "empty": [],
466
+ "obj": {},
467
+ "flag": true,
468
+ "zero": 0,
469
+ "blank": ""
470
+ },
471
+ "user": {
472
+ "name": "Ada"
473
+ }
474
+ }
475
+ },
476
+ "expected": {
477
+ "ok": true,
478
+ "value": "hello"
479
+ },
480
+ "notes": "What makes `||` useful rather than merely true. The reported production case wrote exactly this shape to fall back from a missing transcript to the content, and got null."
481
+ },
482
+ {
483
+ "id": "0402-or-short-circuits",
484
+ "title": "`||` yields the left operand when truthy.",
485
+ "since": "0.14.0",
486
+ "tags": [
487
+ "expr"
488
+ ],
489
+ "input": {
490
+ "expression": "in.content || in.nope",
491
+ "context": {
492
+ "in": {
493
+ "content": "hello",
494
+ "count": 3,
495
+ "deal_id": "D-1",
496
+ "transcript": null,
497
+ "items": [
498
+ 1,
499
+ 2
500
+ ],
501
+ "empty": [],
502
+ "obj": {},
503
+ "flag": true,
504
+ "zero": 0,
505
+ "blank": ""
506
+ },
507
+ "user": {
508
+ "name": "Ada"
509
+ }
510
+ }
511
+ },
512
+ "expected": {
513
+ "ok": true,
514
+ "value": "hello"
515
+ }
516
+ },
517
+ {
518
+ "id": "0403-and-returns-the-operand",
519
+ "title": "`&&` returns the right operand when the left is truthy.",
520
+ "since": "0.14.0",
521
+ "tags": [
522
+ "expr"
523
+ ],
524
+ "input": {
525
+ "expression": "in.content && in.deal_id",
526
+ "context": {
527
+ "in": {
528
+ "content": "hello",
529
+ "count": 3,
530
+ "deal_id": "D-1",
531
+ "transcript": null,
532
+ "items": [
533
+ 1,
534
+ 2
535
+ ],
536
+ "empty": [],
537
+ "obj": {},
538
+ "flag": true,
539
+ "zero": 0,
540
+ "blank": ""
541
+ },
542
+ "user": {
543
+ "name": "Ada"
544
+ }
545
+ }
546
+ },
547
+ "expected": {
548
+ "ok": true,
549
+ "value": "D-1"
550
+ }
551
+ },
552
+ {
553
+ "id": "0404-and-short-circuits",
554
+ "title": "`&&` yields the left operand when falsy.",
555
+ "since": "0.14.0",
556
+ "tags": [
557
+ "expr"
558
+ ],
559
+ "input": {
560
+ "expression": "in.zero && in.content",
561
+ "context": {
562
+ "in": {
563
+ "content": "hello",
564
+ "count": 3,
565
+ "deal_id": "D-1",
566
+ "transcript": null,
567
+ "items": [
568
+ 1,
569
+ 2
570
+ ],
571
+ "empty": [],
572
+ "obj": {},
573
+ "flag": true,
574
+ "zero": 0,
575
+ "blank": ""
576
+ },
577
+ "user": {
578
+ "name": "Ada"
579
+ }
580
+ }
581
+ },
582
+ "expected": {
583
+ "ok": true,
584
+ "value": 0
585
+ }
586
+ },
587
+ {
588
+ "id": "0405-not",
589
+ "title": "`!` yields a boolean.",
590
+ "since": "0.14.0",
591
+ "tags": [
592
+ "expr"
593
+ ],
594
+ "input": {
595
+ "expression": "!in.transcript",
596
+ "context": {
597
+ "in": {
598
+ "content": "hello",
599
+ "count": 3,
600
+ "deal_id": "D-1",
601
+ "transcript": null,
602
+ "items": [
603
+ 1,
604
+ 2
605
+ ],
606
+ "empty": [],
607
+ "obj": {},
608
+ "flag": true,
609
+ "zero": 0,
610
+ "blank": ""
611
+ },
612
+ "user": {
613
+ "name": "Ada"
614
+ }
615
+ }
616
+ },
617
+ "expected": {
618
+ "ok": true,
619
+ "value": true
620
+ }
621
+ },
622
+ {
623
+ "id": "0406-the-reported-condition",
624
+ "title": "The exact condition that silently routed a production graph the wrong way.",
625
+ "since": "0.14.0",
626
+ "tags": [
627
+ "expr",
628
+ "regression",
629
+ "the-point"
630
+ ],
631
+ "input": {
632
+ "expression": "in.deal_id && in.deal_id !== ''",
633
+ "context": {
634
+ "in": {
635
+ "content": "hello",
636
+ "count": 3,
637
+ "deal_id": "D-1",
638
+ "transcript": null,
639
+ "items": [
640
+ 1,
641
+ 2
642
+ ],
643
+ "empty": [],
644
+ "obj": {},
645
+ "flag": true,
646
+ "zero": 0,
647
+ "blank": ""
648
+ },
649
+ "user": {
650
+ "name": "Ada"
651
+ }
652
+ }
653
+ },
654
+ "expected": {
655
+ "ok": true,
656
+ "value": true
657
+ },
658
+ "notes": "Verbatim from the report. It returned null, `branch` read that as false, and the graph took the wrong road on EVERY run while reporting success."
659
+ },
660
+ {
661
+ "id": "0501-strict-equal",
662
+ "title": "`===` compares by value without coercion.",
663
+ "since": "0.14.0",
664
+ "tags": [
665
+ "expr"
666
+ ],
667
+ "input": {
668
+ "expression": "in.count === 3",
669
+ "context": {
670
+ "in": {
671
+ "content": "hello",
672
+ "count": 3,
673
+ "deal_id": "D-1",
674
+ "transcript": null,
675
+ "items": [
676
+ 1,
677
+ 2
678
+ ],
679
+ "empty": [],
680
+ "obj": {},
681
+ "flag": true,
682
+ "zero": 0,
683
+ "blank": ""
684
+ },
685
+ "user": {
686
+ "name": "Ada"
687
+ }
688
+ }
689
+ },
690
+ "expected": {
691
+ "ok": true,
692
+ "value": true
693
+ }
694
+ },
695
+ {
696
+ "id": "0502-double-equals-is-the-same",
697
+ "title": "`==` is the SAME operator as `===`.",
698
+ "since": "0.14.0",
699
+ "tags": [
700
+ "expr",
701
+ "deliberate"
702
+ ],
703
+ "input": {
704
+ "expression": "in.count == 3",
705
+ "context": {
706
+ "in": {
707
+ "content": "hello",
708
+ "count": 3,
709
+ "deal_id": "D-1",
710
+ "transcript": null,
711
+ "items": [
712
+ 1,
713
+ 2
714
+ ],
715
+ "empty": [],
716
+ "obj": {},
717
+ "flag": true,
718
+ "zero": 0,
719
+ "blank": ""
720
+ },
721
+ "user": {
722
+ "name": "Ada"
723
+ }
724
+ }
725
+ },
726
+ "expected": {
727
+ "ok": true,
728
+ "value": true
729
+ },
730
+ "notes": "Both spellings are accepted because authors write both, and NEITHER coerces. A language with two equality operators that differ subtly generates bug reports, and there are three runtimes to keep in step."
731
+ },
732
+ {
733
+ "id": "0503-no-coercion",
734
+ "title": "A string is never equal to a number.",
735
+ "since": "0.14.0",
736
+ "tags": [
737
+ "expr",
738
+ "deliberate"
739
+ ],
740
+ "input": {
741
+ "expression": "'3' === in.count",
742
+ "context": {
743
+ "in": {
744
+ "content": "hello",
745
+ "count": 3,
746
+ "deal_id": "D-1",
747
+ "transcript": null,
748
+ "items": [
749
+ 1,
750
+ 2
751
+ ],
752
+ "empty": [],
753
+ "obj": {},
754
+ "flag": true,
755
+ "zero": 0,
756
+ "blank": ""
757
+ },
758
+ "user": {
759
+ "name": "Ada"
760
+ }
761
+ }
762
+ },
763
+ "expected": {
764
+ "ok": true,
765
+ "value": false
766
+ }
767
+ },
768
+ {
769
+ "id": "0504-not-equal",
770
+ "title": "`!==` negates it.",
771
+ "since": "0.14.0",
772
+ "tags": [
773
+ "expr"
774
+ ],
775
+ "input": {
776
+ "expression": "in.count !== 4",
777
+ "context": {
778
+ "in": {
779
+ "content": "hello",
780
+ "count": 3,
781
+ "deal_id": "D-1",
782
+ "transcript": null,
783
+ "items": [
784
+ 1,
785
+ 2
786
+ ],
787
+ "empty": [],
788
+ "obj": {},
789
+ "flag": true,
790
+ "zero": 0,
791
+ "blank": ""
792
+ },
793
+ "user": {
794
+ "name": "Ada"
795
+ }
796
+ }
797
+ },
798
+ "expected": {
799
+ "ok": true,
800
+ "value": true
801
+ }
802
+ },
803
+ {
804
+ "id": "0601-greater-than",
805
+ "title": "Numeric comparison.",
806
+ "since": "0.14.0",
807
+ "tags": [
808
+ "expr"
809
+ ],
810
+ "input": {
811
+ "expression": "in.count > 2",
812
+ "context": {
813
+ "in": {
814
+ "content": "hello",
815
+ "count": 3,
816
+ "deal_id": "D-1",
817
+ "transcript": null,
818
+ "items": [
819
+ 1,
820
+ 2
821
+ ],
822
+ "empty": [],
823
+ "obj": {},
824
+ "flag": true,
825
+ "zero": 0,
826
+ "blank": ""
827
+ },
828
+ "user": {
829
+ "name": "Ada"
830
+ }
831
+ }
832
+ },
833
+ "expected": {
834
+ "ok": true,
835
+ "value": true
836
+ }
837
+ },
838
+ {
839
+ "id": "0602-less-or-equal",
840
+ "title": "Inclusive comparison.",
841
+ "since": "0.14.0",
842
+ "tags": [
843
+ "expr"
844
+ ],
845
+ "input": {
846
+ "expression": "in.count <= 3",
847
+ "context": {
848
+ "in": {
849
+ "content": "hello",
850
+ "count": 3,
851
+ "deal_id": "D-1",
852
+ "transcript": null,
853
+ "items": [
854
+ 1,
855
+ 2
856
+ ],
857
+ "empty": [],
858
+ "obj": {},
859
+ "flag": true,
860
+ "zero": 0,
861
+ "blank": ""
862
+ },
863
+ "user": {
864
+ "name": "Ada"
865
+ }
866
+ }
867
+ },
868
+ "expected": {
869
+ "ok": true,
870
+ "value": true
871
+ }
872
+ },
873
+ {
874
+ "id": "0603-addition",
875
+ "title": "`+` on two numbers adds.",
876
+ "since": "0.14.0",
877
+ "tags": [
878
+ "expr"
879
+ ],
880
+ "input": {
881
+ "expression": "in.count + 1",
882
+ "context": {
883
+ "in": {
884
+ "content": "hello",
885
+ "count": 3,
886
+ "deal_id": "D-1",
887
+ "transcript": null,
888
+ "items": [
889
+ 1,
890
+ 2
891
+ ],
892
+ "empty": [],
893
+ "obj": {},
894
+ "flag": true,
895
+ "zero": 0,
896
+ "blank": ""
897
+ },
898
+ "user": {
899
+ "name": "Ada"
900
+ }
901
+ }
902
+ },
903
+ "expected": {
904
+ "ok": true,
905
+ "value": 4
906
+ }
907
+ },
908
+ {
909
+ "id": "0604-string-concat",
910
+ "title": "`+` with a string concatenates.",
911
+ "since": "0.14.0",
912
+ "tags": [
913
+ "expr"
914
+ ],
915
+ "input": {
916
+ "expression": "'n=' + in.count",
917
+ "context": {
918
+ "in": {
919
+ "content": "hello",
920
+ "count": 3,
921
+ "deal_id": "D-1",
922
+ "transcript": null,
923
+ "items": [
924
+ 1,
925
+ 2
926
+ ],
927
+ "empty": [],
928
+ "obj": {},
929
+ "flag": true,
930
+ "zero": 0,
931
+ "blank": ""
932
+ },
933
+ "user": {
934
+ "name": "Ada"
935
+ }
936
+ }
937
+ },
938
+ "expected": {
939
+ "ok": true,
940
+ "value": "n=3"
941
+ }
942
+ },
943
+ {
944
+ "id": "0605-unary-minus",
945
+ "title": "Unary minus.",
946
+ "since": "0.14.0",
947
+ "tags": [
948
+ "expr"
949
+ ],
950
+ "input": {
951
+ "expression": "-in.count",
952
+ "context": {
953
+ "in": {
954
+ "content": "hello",
955
+ "count": 3,
956
+ "deal_id": "D-1",
957
+ "transcript": null,
958
+ "items": [
959
+ 1,
960
+ 2
961
+ ],
962
+ "empty": [],
963
+ "obj": {},
964
+ "flag": true,
965
+ "zero": 0,
966
+ "blank": ""
967
+ },
968
+ "user": {
969
+ "name": "Ada"
970
+ }
971
+ }
972
+ },
973
+ "expected": {
974
+ "ok": true,
975
+ "value": -3
976
+ }
977
+ },
978
+ {
979
+ "id": "0701-ternary",
980
+ "title": "A ternary picks a branch.",
981
+ "since": "0.14.0",
982
+ "tags": [
983
+ "expr"
984
+ ],
985
+ "input": {
986
+ "expression": "in.count > 2 ? 'big' : 'small'",
987
+ "context": {
988
+ "in": {
989
+ "content": "hello",
990
+ "count": 3,
991
+ "deal_id": "D-1",
992
+ "transcript": null,
993
+ "items": [
994
+ 1,
995
+ 2
996
+ ],
997
+ "empty": [],
998
+ "obj": {},
999
+ "flag": true,
1000
+ "zero": 0,
1001
+ "blank": ""
1002
+ },
1003
+ "user": {
1004
+ "name": "Ada"
1005
+ }
1006
+ }
1007
+ },
1008
+ "expected": {
1009
+ "ok": true,
1010
+ "value": "big"
1011
+ }
1012
+ },
1013
+ {
1014
+ "id": "0702-object-literal",
1015
+ "title": "An object literal builds a value.",
1016
+ "since": "0.14.0",
1017
+ "tags": [
1018
+ "expr",
1019
+ "regression"
1020
+ ],
1021
+ "input": {
1022
+ "expression": "{ transcript: in.content }",
1023
+ "context": {
1024
+ "in": {
1025
+ "content": "hello",
1026
+ "count": 3,
1027
+ "deal_id": "D-1",
1028
+ "transcript": null,
1029
+ "items": [
1030
+ 1,
1031
+ 2
1032
+ ],
1033
+ "empty": [],
1034
+ "obj": {},
1035
+ "flag": true,
1036
+ "zero": 0,
1037
+ "blank": ""
1038
+ },
1039
+ "user": {
1040
+ "name": "Ada"
1041
+ }
1042
+ }
1043
+ },
1044
+ "expected": {
1045
+ "ok": true,
1046
+ "value": {
1047
+ "transcript": "hello"
1048
+ }
1049
+ },
1050
+ "notes": "The shape an agent authoring a graph reaches for first, and the one that used to yield null."
1051
+ },
1052
+ {
1053
+ "id": "0703-array-literal",
1054
+ "title": "An array literal builds a value.",
1055
+ "since": "0.14.0",
1056
+ "tags": [
1057
+ "expr"
1058
+ ],
1059
+ "input": {
1060
+ "expression": "[in.count, 'x']",
1061
+ "context": {
1062
+ "in": {
1063
+ "content": "hello",
1064
+ "count": 3,
1065
+ "deal_id": "D-1",
1066
+ "transcript": null,
1067
+ "items": [
1068
+ 1,
1069
+ 2
1070
+ ],
1071
+ "empty": [],
1072
+ "obj": {},
1073
+ "flag": true,
1074
+ "zero": 0,
1075
+ "blank": ""
1076
+ },
1077
+ "user": {
1078
+ "name": "Ada"
1079
+ }
1080
+ }
1081
+ },
1082
+ "expected": {
1083
+ "ok": true,
1084
+ "value": [
1085
+ 3,
1086
+ "x"
1087
+ ]
1088
+ }
1089
+ },
1090
+ {
1091
+ "id": "0704-parens",
1092
+ "title": "Parentheses group.",
1093
+ "since": "0.14.0",
1094
+ "tags": [
1095
+ "expr"
1096
+ ],
1097
+ "input": {
1098
+ "expression": "(1 + 2) * 1 === 3",
1099
+ "context": {
1100
+ "in": {
1101
+ "content": "hello",
1102
+ "count": 3,
1103
+ "deal_id": "D-1",
1104
+ "transcript": null,
1105
+ "items": [
1106
+ 1,
1107
+ 2
1108
+ ],
1109
+ "empty": [],
1110
+ "obj": {},
1111
+ "flag": true,
1112
+ "zero": 0,
1113
+ "blank": ""
1114
+ },
1115
+ "user": {
1116
+ "name": "Ada"
1117
+ }
1118
+ }
1119
+ },
1120
+ "expected": {
1121
+ "ok": true,
1122
+ "value": true
1123
+ }
1124
+ },
1125
+ {
1126
+ "id": "0801-unclosed-string",
1127
+ "title": "An unterminated string does not parse.",
1128
+ "since": "0.14.0",
1129
+ "tags": [
1130
+ "expr",
1131
+ "malformed",
1132
+ "the-point"
1133
+ ],
1134
+ "input": {
1135
+ "expression": "'oops",
1136
+ "context": {}
1137
+ },
1138
+ "expected": {
1139
+ "ok": false
1140
+ },
1141
+ "notes": "Malformed expressions must FAIL, never return null. A null here would be indistinguishable from an absent path, which is the entire defect this package exists to remove."
1142
+ },
1143
+ {
1144
+ "id": "0802-dangling-operator",
1145
+ "title": "A dangling operator does not parse.",
1146
+ "since": "0.14.0",
1147
+ "tags": [
1148
+ "expr",
1149
+ "malformed"
1150
+ ],
1151
+ "input": {
1152
+ "expression": "in.count &&",
1153
+ "context": {
1154
+ "in": {
1155
+ "content": "hello",
1156
+ "count": 3,
1157
+ "deal_id": "D-1",
1158
+ "transcript": null,
1159
+ "items": [
1160
+ 1,
1161
+ 2
1162
+ ],
1163
+ "empty": [],
1164
+ "obj": {},
1165
+ "flag": true,
1166
+ "zero": 0,
1167
+ "blank": ""
1168
+ },
1169
+ "user": {
1170
+ "name": "Ada"
1171
+ }
1172
+ }
1173
+ },
1174
+ "expected": {
1175
+ "ok": false
1176
+ }
1177
+ },
1178
+ {
1179
+ "id": "0803-unclosed-paren",
1180
+ "title": "An unclosed paren does not parse.",
1181
+ "since": "0.14.0",
1182
+ "tags": [
1183
+ "expr",
1184
+ "malformed"
1185
+ ],
1186
+ "input": {
1187
+ "expression": "(1 + 2",
1188
+ "context": {}
1189
+ },
1190
+ "expected": {
1191
+ "ok": false
1192
+ }
1193
+ },
1194
+ {
1195
+ "id": "0804-empty-expression",
1196
+ "title": "An empty expression does not parse.",
1197
+ "since": "0.14.0",
1198
+ "tags": [
1199
+ "expr",
1200
+ "malformed"
1201
+ ],
1202
+ "input": {
1203
+ "expression": "",
1204
+ "context": {}
1205
+ },
1206
+ "expected": {
1207
+ "ok": false
1208
+ }
1209
+ },
1210
+ {
1211
+ "id": "0805-no-function-calls",
1212
+ "title": "A call does not parse — this is not a programming language.",
1213
+ "since": "0.14.0",
1214
+ "tags": [
1215
+ "expr",
1216
+ "malformed",
1217
+ "sandbox"
1218
+ ],
1219
+ "input": {
1220
+ "expression": "count(in.items)",
1221
+ "context": {
1222
+ "in": {
1223
+ "content": "hello",
1224
+ "count": 3,
1225
+ "deal_id": "D-1",
1226
+ "transcript": null,
1227
+ "items": [
1228
+ 1,
1229
+ 2
1230
+ ],
1231
+ "empty": [],
1232
+ "obj": {},
1233
+ "flag": true,
1234
+ "zero": 0,
1235
+ "blank": ""
1236
+ },
1237
+ "user": {
1238
+ "name": "Ada"
1239
+ }
1240
+ }
1241
+ },
1242
+ "expected": {
1243
+ "ok": false
1244
+ },
1245
+ "notes": "Sandboxing is a security property, not a style preference: these expressions arrive from end users and from agents, over the wire. No calls, no host reach, in any implementation."
1246
+ }
1247
+ ]
1248
+ }
@@ -0,0 +1,28 @@
1
+ {
2
+ "$schema": "../../../schema/suite-manifest.schema.json",
3
+ "suite": "expr/evaluate",
4
+ "title": "The fancy-expr grammar — what an expression evaluates to",
5
+ "since": "0.14.0",
6
+ "caseFormat": "table",
7
+ "cases": "cases.json",
8
+ "contract": {
9
+ "function": "evaluate(expression: string, context: object) -> { ok: true, value: json } | { ok: false }",
10
+ "summary": "Parse and evaluate one expression against a context. `{ ok: true, value }` when it evaluates; `{ ok: false }` when it cannot PARSE. An expression that parses but whose path is absent is `{ ok: true, value: null }` — absence and malformity are different outcomes and the distinction is the reason this package exists. Evaluation is total otherwise: it never reaches the host, never calls anything, and never throws for missing data.",
11
+ "reference": "node",
12
+ "referenceNote": "No implementation exists yet in any language. This table is written FIRST and every implementation is built against it, which is the whole argument for owning the grammar rather than adopting three libraries that disagree.",
13
+ "implementations": [
14
+ { "language": "node", "package": "@particle-academy/fancy-expr", "symbol": "evaluate" },
15
+ { "language": "php", "package": "particle-academy/fancy-expr", "symbol": "FancyExpr\\Expr::evaluate" },
16
+ { "language": "python", "package": "fancy-expr", "symbol": "fancy_expr.evaluate" }
17
+ ]
18
+ },
19
+ "notes": [
20
+ "WRITTEN BEFORE ANY IMPLEMENTATION. Every previous flow suite was a post-mortem in fixture form — rows written after all the runtimes had already shipped the same bug. This one is a specification: nothing satisfies it yet, and each port will be built against published rows rather than against another port's source. The one time that was done before (flow/workflow-props) the second implementation failed exactly one row on its first run, and the row turned out to describe a genuine cross-language limit rather than a mistake.",
21
+ "THE DEFECT THIS REPLACES. fancy-flow's `{{ }}` resolved dot-paths ONLY and returned null for everything else — the same null a real-but-absent path returns. Indistinguishable, so a condition the engine could not evaluate silently read as FALSE: `branch` took the wrong road on every run and the run reported success. A consumer lost a production workflow to it and found it only by reading the resolver's source. Row 0406 is their condition verbatim.",
22
+ "TRUTHINESS IS THE ROW SET THAT MATTERS MOST (0301-0305), because it is where three languages disagree natively and where a silent divergence is most expensive. `[]` and `{}` are TRUTHY here — against PHP's and Python's instincts, both of which call an empty container false. The data arrived as JSON: an array that exists is a value, and asking whether it is EMPTY is what `.length` is for. Conflating the two turns 'did we get results?' into 'did the call succeed?'.",
23
+ "`==` AND `===` ARE THE SAME OPERATOR (0501-0503), and neither coerces. Both spellings are accepted because authors write both. Two equality operators that differ subtly is a language that generates bug reports, and there are three runtimes to keep in step.",
24
+ "`&&` AND `||` RETURN THE OPERAND, NOT A BOOLEAN (0401-0404). That is what makes `in.transcript || in.content` a useful fallback rather than merely `true` — and the reported production case wrote exactly that shape.",
25
+ "MALFORMED EXPRESSIONS FAIL (0801-0805) and never return null. A null there would be indistinguishable from an absent path, which is the defect being removed. 0805 pins that a function call does not parse: sandboxing is a security property here, not a style preference, because these expressions arrive from end users and from agents over the wire.",
26
+ "This suite needs DISCRIMINATION PROBES before it can claim more than drift-guarding — deliberately-wrong evaluators that must each fail an exact set of ids. Notably: one that uses native truthiness (must fail 0301/0302), one that coerces on `==` (must fail 0503), and one that returns booleans from `&&`/`||` (must fail 0401/0403/0404). Not yet written; recorded so a green tick is not read as the stronger claim."
27
+ ]
28
+ }