@particle-academy/fancy-conformance 0.10.0 → 0.11.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.10.0
1
+ 0.11.0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@particle-academy/fancy-conformance",
3
- "version": "0.10.0",
3
+ "version": "0.11.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,524 @@
1
+ {
2
+ "$schema": "../../../schema/cases.schema.json",
3
+ "suite": "shared/value-equality",
4
+ "cases": [
5
+ {
6
+ "id": "0001-identical-strings",
7
+ "title": "Identical strings are equal.",
8
+ "since": "0.11.0",
9
+ "tags": [
10
+ "scalars"
11
+ ],
12
+ "input": {
13
+ "a": "a",
14
+ "b": "a"
15
+ },
16
+ "expected": true
17
+ },
18
+ {
19
+ "id": "0002-different-strings",
20
+ "title": "Different strings are not.",
21
+ "since": "0.11.0",
22
+ "tags": [
23
+ "scalars"
24
+ ],
25
+ "input": {
26
+ "a": "a",
27
+ "b": "b"
28
+ },
29
+ "expected": false
30
+ },
31
+ {
32
+ "id": "0003-identical-integers",
33
+ "title": "Identical integers are equal.",
34
+ "since": "0.11.0",
35
+ "tags": [
36
+ "scalars"
37
+ ],
38
+ "input": {
39
+ "a": 3,
40
+ "b": 3
41
+ },
42
+ "expected": true
43
+ },
44
+ {
45
+ "id": "0004-different-integers",
46
+ "title": "Different integers are not.",
47
+ "since": "0.11.0",
48
+ "tags": [
49
+ "scalars"
50
+ ],
51
+ "input": {
52
+ "a": 2,
53
+ "b": 3
54
+ },
55
+ "expected": false
56
+ },
57
+ {
58
+ "id": "0005-null-equals-null",
59
+ "title": "null equals null.",
60
+ "since": "0.11.0",
61
+ "tags": [
62
+ "scalars",
63
+ "absence"
64
+ ],
65
+ "input": {
66
+ "a": null,
67
+ "b": null
68
+ },
69
+ "expected": true
70
+ },
71
+ {
72
+ "id": "0101-int-golden-satisfied-by-float",
73
+ "title": "An integer is equal to a float of the same value.",
74
+ "since": "0.11.0",
75
+ "tags": [
76
+ "numbers",
77
+ "regression",
78
+ "trap"
79
+ ],
80
+ "input": {
81
+ "a": 100000,
82
+ "b": 100000.0
83
+ },
84
+ "expected": true,
85
+ "notes": "The rule a Rust unit test denied while `shared/decimal/0008-coerce-exponent` required it: that case has the integer golden 100000 and PHP's `\"1e5\" + 0` yields float(100000). The reference language has ONE number type, so no golden can demand a float."
86
+ },
87
+ {
88
+ "id": "0102-float-golden-satisfied-by-int",
89
+ "title": "...and the same in reverse.",
90
+ "since": "0.11.0",
91
+ "tags": [
92
+ "numbers",
93
+ "trap"
94
+ ],
95
+ "input": {
96
+ "a": 3.0,
97
+ "b": 3
98
+ },
99
+ "expected": true
100
+ },
101
+ {
102
+ "id": "0103-different-values-across-types",
103
+ "title": "Different values are unequal regardless of type.",
104
+ "since": "0.11.0",
105
+ "tags": [
106
+ "numbers"
107
+ ],
108
+ "input": {
109
+ "a": 2,
110
+ "b": 3.0
111
+ },
112
+ "expected": false
113
+ },
114
+ {
115
+ "id": "0110-true-is-not-one",
116
+ "title": "true is not 1.",
117
+ "since": "0.11.0",
118
+ "tags": [
119
+ "booleans",
120
+ "trap"
121
+ ],
122
+ "input": {
123
+ "a": true,
124
+ "b": 1
125
+ },
126
+ "expected": false,
127
+ "notes": "Python's `==` says `True == 1`, and PHP's loose comparison agrees. Without this a row expecting `false` is satisfied by an implementation returning `0`."
128
+ },
129
+ {
130
+ "id": "0111-false-is-not-zero",
131
+ "title": "false is not 0.",
132
+ "since": "0.11.0",
133
+ "tags": [
134
+ "booleans",
135
+ "trap"
136
+ ],
137
+ "input": {
138
+ "a": false,
139
+ "b": 0
140
+ },
141
+ "expected": false
142
+ },
143
+ {
144
+ "id": "0112-true-is-not-the-string",
145
+ "title": "true is not \"true\".",
146
+ "since": "0.11.0",
147
+ "tags": [
148
+ "booleans"
149
+ ],
150
+ "input": {
151
+ "a": true,
152
+ "b": "true"
153
+ },
154
+ "expected": false
155
+ },
156
+ {
157
+ "id": "0120-same-double",
158
+ "title": "The same double is equal to itself.",
159
+ "since": "0.11.0",
160
+ "tags": [
161
+ "floats"
162
+ ],
163
+ "input": {
164
+ "a": 0.1,
165
+ "b": 0.1
166
+ },
167
+ "expected": true
168
+ },
169
+ {
170
+ "id": "0121-adjacent-doubles-are-not-equal",
171
+ "title": "Genuinely different doubles are NOT equal.",
172
+ "since": "0.11.0",
173
+ "tags": [
174
+ "floats",
175
+ "regression"
176
+ ],
177
+ "input": {
178
+ "a": 1.0,
179
+ "b": 1.0000000000001
180
+ },
181
+ "expected": false,
182
+ "notes": "A scaled 1e-12 epsilon in three of the four loaders passed this until 0.10.0. On a money row a relative 1e-12 is real money at scale."
183
+ },
184
+ {
185
+ "id": "0122-tolerance-accepts-a-near-miss",
186
+ "title": "A declared tolerance accepts a near miss.",
187
+ "since": "0.11.0",
188
+ "tags": [
189
+ "floats",
190
+ "tolerance"
191
+ ],
192
+ "input": {
193
+ "a": 1.0,
194
+ "b": 1.0000000000001,
195
+ "tolerance": 1e-09
196
+ },
197
+ "expected": true
198
+ },
199
+ {
200
+ "id": "0123-tolerance-is-not-unlimited",
201
+ "title": "A tolerance widens the target; it does not remove it.",
202
+ "since": "0.11.0",
203
+ "tags": [
204
+ "floats",
205
+ "tolerance"
206
+ ],
207
+ "input": {
208
+ "a": 1.0,
209
+ "b": 1.5,
210
+ "tolerance": 1e-09
211
+ },
212
+ "expected": false
213
+ },
214
+ {
215
+ "id": "0124-tolerance-tighter-than-the-gap",
216
+ "title": "A tolerance smaller than the gap still rejects.",
217
+ "since": "0.11.0",
218
+ "tags": [
219
+ "floats",
220
+ "tolerance"
221
+ ],
222
+ "input": {
223
+ "a": 1.0,
224
+ "b": 1.0000000000001,
225
+ "tolerance": 1e-15
226
+ },
227
+ "expected": false
228
+ },
229
+ {
230
+ "id": "0130-numeric-string-is-not-a-number",
231
+ "title": "\"1\" is not 1.",
232
+ "since": "0.11.0",
233
+ "tags": [
234
+ "scalars",
235
+ "trap"
236
+ ],
237
+ "input": {
238
+ "a": "1",
239
+ "b": 1
240
+ },
241
+ "expected": false,
242
+ "notes": "PHP's `==` coerces a numeric string to a number. A golden of 1 must not be satisfied by an implementation returning the string."
243
+ },
244
+ {
245
+ "id": "0140-arrays-order-matters",
246
+ "title": "Array order is significant.",
247
+ "since": "0.11.0",
248
+ "tags": [
249
+ "arrays"
250
+ ],
251
+ "input": {
252
+ "a": [
253
+ 1,
254
+ 2
255
+ ],
256
+ "b": [
257
+ 2,
258
+ 1
259
+ ]
260
+ },
261
+ "expected": false
262
+ },
263
+ {
264
+ "id": "0141-arrays-same-order",
265
+ "title": "Same values in the same order are equal.",
266
+ "since": "0.11.0",
267
+ "tags": [
268
+ "arrays"
269
+ ],
270
+ "input": {
271
+ "a": [
272
+ 1,
273
+ 2
274
+ ],
275
+ "b": [
276
+ 1,
277
+ 2
278
+ ]
279
+ },
280
+ "expected": true
281
+ },
282
+ {
283
+ "id": "0142-arrays-different-length",
284
+ "title": "Different lengths are unequal.",
285
+ "since": "0.11.0",
286
+ "tags": [
287
+ "arrays"
288
+ ],
289
+ "input": {
290
+ "a": [
291
+ 1
292
+ ],
293
+ "b": [
294
+ 1,
295
+ 2
296
+ ]
297
+ },
298
+ "expected": false
299
+ },
300
+ {
301
+ "id": "0143-empty-arrays",
302
+ "title": "Two empty arrays are equal.",
303
+ "since": "0.11.0",
304
+ "tags": [
305
+ "arrays"
306
+ ],
307
+ "input": {
308
+ "a": [],
309
+ "b": []
310
+ },
311
+ "expected": true
312
+ },
313
+ {
314
+ "id": "0150-object-key-order-does-not-matter",
315
+ "title": "Object key order is NOT significant.",
316
+ "since": "0.11.0",
317
+ "tags": [
318
+ "objects"
319
+ ],
320
+ "input": {
321
+ "a": {
322
+ "a": 1,
323
+ "b": 2
324
+ },
325
+ "b": {
326
+ "b": 2,
327
+ "a": 1
328
+ }
329
+ },
330
+ "expected": true
331
+ },
332
+ {
333
+ "id": "0151-object-different-values",
334
+ "title": "Same keys, different values, unequal.",
335
+ "since": "0.11.0",
336
+ "tags": [
337
+ "objects"
338
+ ],
339
+ "input": {
340
+ "a": {
341
+ "a": 1
342
+ },
343
+ "b": {
344
+ "a": 2
345
+ }
346
+ },
347
+ "expected": false
348
+ },
349
+ {
350
+ "id": "0152-nested",
351
+ "title": "Nesting is compared all the way down.",
352
+ "since": "0.11.0",
353
+ "tags": [
354
+ "objects"
355
+ ],
356
+ "input": {
357
+ "a": {
358
+ "a": {
359
+ "b": [
360
+ 1,
361
+ {
362
+ "c": "d"
363
+ }
364
+ ]
365
+ }
366
+ },
367
+ "b": {
368
+ "a": {
369
+ "b": [
370
+ 1,
371
+ {
372
+ "c": "d"
373
+ }
374
+ ]
375
+ }
376
+ }
377
+ },
378
+ "expected": true
379
+ },
380
+ {
381
+ "id": "0153-nested-differs-deep",
382
+ "title": "A difference at depth is still a difference.",
383
+ "since": "0.11.0",
384
+ "tags": [
385
+ "objects"
386
+ ],
387
+ "input": {
388
+ "a": {
389
+ "a": {
390
+ "b": [
391
+ 1,
392
+ {
393
+ "c": "d"
394
+ }
395
+ ]
396
+ }
397
+ },
398
+ "b": {
399
+ "a": {
400
+ "b": [
401
+ 1,
402
+ {
403
+ "c": "e"
404
+ }
405
+ ]
406
+ }
407
+ }
408
+ },
409
+ "expected": false
410
+ },
411
+ {
412
+ "id": "0201-explicit-null-key-vs-missing-key",
413
+ "title": "An object with an explicit null value is NOT equal to one missing that key.",
414
+ "since": "0.11.0",
415
+ "tags": [
416
+ "absence",
417
+ "trap"
418
+ ],
419
+ "input": {
420
+ "a": {
421
+ "x": null
422
+ },
423
+ "b": {}
424
+ },
425
+ "expected": false,
426
+ "notes": "The axis a PHP-referenced corpus cannot express: PHP has ONE absent value while JavaScript has null and undefined and Python has None. Raised by an agent building a parity repo whose reference IS PHP, who spotted that a TypeScript loader could assert a null/undefined distinction no PHP-authored golden can encode, pass its own tests, and be wrong. Pinned here so the four loaders' current agreement is a test result rather than a coincidence."
427
+ },
428
+ {
429
+ "id": "0202-both-missing",
430
+ "title": "Two objects both missing the key are equal.",
431
+ "since": "0.11.0",
432
+ "tags": [
433
+ "absence"
434
+ ],
435
+ "input": {
436
+ "a": {},
437
+ "b": {}
438
+ },
439
+ "expected": true
440
+ },
441
+ {
442
+ "id": "0203-null-is-not-zero",
443
+ "title": "null is not 0.",
444
+ "since": "0.11.0",
445
+ "tags": [
446
+ "absence",
447
+ "trap"
448
+ ],
449
+ "input": {
450
+ "a": null,
451
+ "b": 0
452
+ },
453
+ "expected": false
454
+ },
455
+ {
456
+ "id": "0204-null-is-not-empty-string",
457
+ "title": "null is not \"\".",
458
+ "since": "0.11.0",
459
+ "tags": [
460
+ "absence",
461
+ "trap"
462
+ ],
463
+ "input": {
464
+ "a": null,
465
+ "b": ""
466
+ },
467
+ "expected": false
468
+ },
469
+ {
470
+ "id": "0205-null-inside-an-array-holds-its-place",
471
+ "title": "A null inside an array is a value, not a gap.",
472
+ "since": "0.11.0",
473
+ "tags": [
474
+ "absence",
475
+ "arrays"
476
+ ],
477
+ "input": {
478
+ "a": [
479
+ 1,
480
+ null,
481
+ 3
482
+ ],
483
+ "b": [
484
+ 1,
485
+ 3
486
+ ]
487
+ },
488
+ "expected": false
489
+ },
490
+ {
491
+ "id": "0210-array-is-not-an-object",
492
+ "title": "An array is not an object.",
493
+ "since": "0.11.0",
494
+ "tags": [
495
+ "containers",
496
+ "trap"
497
+ ],
498
+ "input": {
499
+ "a": [],
500
+ "b": {}
501
+ },
502
+ "expected": false,
503
+ "notes": "PHP has ONE array type for both, so this needs asserting rather than assuming. FOUND BY THIS SUITE ON ITS FIRST RUN, which is the argument for the suite: three loaders agreed and the fourth could not even express the question, and nothing had ever asked them the same question side by side.",
504
+ "skip": {
505
+ "php": "Not representable in PHP. `json_decode('[]', true)` and `json_decode('{}', true)` produce the IDENTICAL value -- an empty PHP array -- so the loader has nothing left to compare. This is a real expressiveness limit rather than a loader defect: a PHP-authored golden cannot distinguish an empty array from an empty object at all. Node, Python and Rust all keep the distinction and assert it."
506
+ }
507
+ },
508
+ {
509
+ "id": "0211-array-is-not-a-scalar",
510
+ "title": "An array is not a scalar.",
511
+ "since": "0.11.0",
512
+ "tags": [
513
+ "containers"
514
+ ],
515
+ "input": {
516
+ "a": [
517
+ 1
518
+ ],
519
+ "b": 1
520
+ },
521
+ "expected": false
522
+ }
523
+ ]
524
+ }
@@ -0,0 +1,28 @@
1
+ {
2
+ "$schema": "../../../schema/suite-manifest.schema.json",
3
+ "suite": "shared/value-equality",
4
+ "title": "What the LOADERS mean by equal",
5
+ "since": "0.11.0",
6
+ "caseFormat": "table",
7
+ "cases": "cases.json",
8
+ "contract": {
9
+ "function": "equals(a: json, b: json, tolerance: number|null) -> boolean",
10
+ "summary": "Each loader's own comparison, run against a shared table. `a` and `b` are arbitrary JSON values; `tolerance` is the case's declared relative float tolerance, or null for exact. The result is whether that loader considers the two values equal.",
11
+ "reference": "node",
12
+ "referenceNote": "The rules were settled in 0.10.0 when the four loaders' float comparison was unified. This table makes them a test result rather than four independent readings of the same paragraph.",
13
+ "implementations": [
14
+ { "language": "node", "package": "@particle-academy/fancy-conformance", "symbol": "deepEquals" },
15
+ { "language": "php", "package": "particle-academy/fancy-conformance", "symbol": "ParticleAcademy\\Conformance\\Conformance::equals" },
16
+ { "language": "python", "package": "fancy-conformance", "symbol": "fancy_conformance.equals" },
17
+ { "language": "rust", "package": "fancy-conformance", "symbol": "fancy_conformance::equals_within" }
18
+ ]
19
+ },
20
+ "notes": [
21
+ "THIS PACKAGE HELD EVERY IMPLEMENTATION TO A TABLE EXCEPT ITSELF. The loaders are implementations too, and until this suite existed each one's comparison was asserted only by its own hand-written unit tests, against pairs its own author chose. That is precisely the setup this repository exists to argue against.",
22
+ "It was not hypothetical. The Rust loader asserted that an integer golden is never satisfied by a float, and had a passing unit test saying so — while `shared/decimal/0008-coerce-exponent` carries the integer golden `100000` that PHP's `\"1e5\" + 0` satisfies with a float. A loader had encoded a rule the shipped corpus disproves, and nothing ran the two against each other. It surfaced in 0.10.0 only because an unrelated change happened to break it.",
23
+ "The generalisation, and the reason this suite is worth its weight: A LOADER CAN ASSERT SOMETHING THE REFERENCE LANGUAGE CANNOT EXPRESS, AND NO NUMBER OF GREEN TICKS WILL SURFACE IT. The reference here is JavaScript, which has ONE number type, so no golden can encode \"this must be a float\" — a loader enforcing that distinction asserts something the fixtures are structurally unable to state.",
24
+ "Raised independently by an agent building a parity repo for another package family, who identified their own instance before writing it: their reference is PHP, which has ONE absent value, while TypeScript has `null` and `undefined` and Python has `None`. A TS loader could very reasonably assert a null-versus-undefined distinction that a PHP-authored golden cannot encode, pass its own tests, and be wrong in exactly the Rust way. Cases 0201-0204 pin the absent-versus-null axis for that reason.",
25
+ "Numbers compare BY VALUE, not by JSON type (0101-0103). Booleans are never numbers (0110-0111) — Python's `==` says `True == 1` and PHP's loose comparison agrees, so this needs asserting rather than assuming.",
26
+ "Floats are EXACT unless the case declares a tolerance (0120-0124). A scaled 1e-12 epsilon lived in three of the four loaders until 0.10.0, on a justification that measured false: `0.002`, `0.1`, `1e300`, `DBL_MAX`, the `5e-324` denormal and `0.30000000000000004` all parse to bit-identical doubles in PHP, Python and Node."
27
+ ]
28
+ }