@particle-academy/fancy-conformance 0.6.0 → 0.9.1

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,558 @@
1
+ {
2
+ "$schema": "../../../schema/cases.schema.json",
3
+ "suite": "flow/workflow-props",
4
+ "cases": [
5
+ {
6
+ "id": "0001-supplied-value-passes-through",
7
+ "title": "A declared, supplied value arrives in the resolved map unchanged.",
8
+ "since": "0.9.0",
9
+ "tags": [
10
+ "happy-path"
11
+ ],
12
+ "input": {
13
+ "declared": [
14
+ {
15
+ "name": "topic",
16
+ "type": "string"
17
+ }
18
+ ],
19
+ "passed": {
20
+ "topic": "otters"
21
+ }
22
+ },
23
+ "expected": {
24
+ "ok": true,
25
+ "props": {
26
+ "topic": "otters"
27
+ }
28
+ }
29
+ },
30
+ {
31
+ "id": "0002-default-fills-an-omitted-value",
32
+ "title": "An omitted input with a default resolves to the default.",
33
+ "since": "0.9.0",
34
+ "tags": [
35
+ "defaults"
36
+ ],
37
+ "input": {
38
+ "declared": [
39
+ {
40
+ "name": "limit",
41
+ "type": "number",
42
+ "default": 10
43
+ }
44
+ ],
45
+ "passed": {}
46
+ },
47
+ "expected": {
48
+ "ok": true,
49
+ "props": {
50
+ "limit": 10
51
+ }
52
+ }
53
+ },
54
+ {
55
+ "id": "0003-supplied-beats-default",
56
+ "title": "An explicitly supplied value wins over the declared default.",
57
+ "since": "0.9.0",
58
+ "tags": [
59
+ "defaults"
60
+ ],
61
+ "input": {
62
+ "declared": [
63
+ {
64
+ "name": "limit",
65
+ "type": "number",
66
+ "default": 10
67
+ }
68
+ ],
69
+ "passed": {
70
+ "limit": 25
71
+ }
72
+ },
73
+ "expected": {
74
+ "ok": true,
75
+ "props": {
76
+ "limit": 25
77
+ }
78
+ }
79
+ },
80
+ {
81
+ "id": "0004-explicit-zero-is-not-replaced-by-a-default",
82
+ "title": "A supplied 0 survives a non-zero default.",
83
+ "since": "0.9.0",
84
+ "tags": [
85
+ "defaults",
86
+ "falsy",
87
+ "trap"
88
+ ],
89
+ "input": {
90
+ "declared": [
91
+ {
92
+ "name": "limit",
93
+ "type": "number",
94
+ "default": 10
95
+ }
96
+ ],
97
+ "passed": {
98
+ "limit": 0
99
+ }
100
+ },
101
+ "expected": {
102
+ "ok": true,
103
+ "props": {
104
+ "limit": 0
105
+ }
106
+ }
107
+ },
108
+ {
109
+ "id": "0005-explicit-false-is-not-replaced-by-a-default",
110
+ "title": "A supplied false survives a true default.",
111
+ "since": "0.9.0",
112
+ "tags": [
113
+ "defaults",
114
+ "falsy",
115
+ "trap"
116
+ ],
117
+ "input": {
118
+ "declared": [
119
+ {
120
+ "name": "dryRun",
121
+ "type": "boolean",
122
+ "default": true
123
+ }
124
+ ],
125
+ "passed": {
126
+ "dryRun": false
127
+ }
128
+ },
129
+ "expected": {
130
+ "ok": true,
131
+ "props": {
132
+ "dryRun": false
133
+ }
134
+ }
135
+ },
136
+ {
137
+ "id": "0006-explicit-empty-string-is-not-replaced-by-a-default",
138
+ "title": "A supplied empty string survives a non-empty default.",
139
+ "since": "0.9.0",
140
+ "tags": [
141
+ "defaults",
142
+ "falsy",
143
+ "trap"
144
+ ],
145
+ "input": {
146
+ "declared": [
147
+ {
148
+ "name": "note",
149
+ "type": "string",
150
+ "default": "unset"
151
+ }
152
+ ],
153
+ "passed": {
154
+ "note": ""
155
+ }
156
+ },
157
+ "expected": {
158
+ "ok": true,
159
+ "props": {
160
+ "note": ""
161
+ }
162
+ }
163
+ },
164
+ {
165
+ "id": "0007-absent-optional-is-absent-not-null",
166
+ "title": "An optional input with no default and no value is missing from the map entirely.",
167
+ "since": "0.9.0",
168
+ "tags": [
169
+ "absence"
170
+ ],
171
+ "input": {
172
+ "declared": [
173
+ {
174
+ "name": "topic",
175
+ "type": "string"
176
+ },
177
+ {
178
+ "name": "note",
179
+ "type": "string"
180
+ }
181
+ ],
182
+ "passed": {
183
+ "topic": "otters"
184
+ }
185
+ },
186
+ "expected": {
187
+ "ok": true,
188
+ "props": {
189
+ "topic": "otters"
190
+ }
191
+ }
192
+ },
193
+ {
194
+ "id": "0008-untyped-declaration-accepts-anything",
195
+ "title": "An input declaring no type accepts a nested object.",
196
+ "since": "0.9.0",
197
+ "tags": [
198
+ "types"
199
+ ],
200
+ "input": {
201
+ "declared": [
202
+ {
203
+ "name": "payload"
204
+ }
205
+ ],
206
+ "passed": {
207
+ "payload": {
208
+ "nested": [
209
+ 1,
210
+ 2
211
+ ]
212
+ }
213
+ }
214
+ },
215
+ "expected": {
216
+ "ok": true,
217
+ "props": {
218
+ "payload": {
219
+ "nested": [
220
+ 1,
221
+ 2
222
+ ]
223
+ }
224
+ }
225
+ }
226
+ },
227
+ {
228
+ "id": "0009-required-is-satisfied-by-its-default",
229
+ "title": "A required input carrying a default does not need the caller to supply it.",
230
+ "since": "0.9.0",
231
+ "tags": [
232
+ "required",
233
+ "defaults"
234
+ ],
235
+ "input": {
236
+ "declared": [
237
+ {
238
+ "name": "limit",
239
+ "type": "number",
240
+ "required": true,
241
+ "default": 5
242
+ }
243
+ ],
244
+ "passed": {}
245
+ },
246
+ "expected": {
247
+ "ok": true,
248
+ "props": {
249
+ "limit": 5
250
+ }
251
+ }
252
+ },
253
+ {
254
+ "id": "0010-array-satisfies-array-not-object",
255
+ "title": "A list satisfies a declared array.",
256
+ "since": "0.9.0",
257
+ "tags": [
258
+ "types",
259
+ "trap"
260
+ ],
261
+ "input": {
262
+ "declared": [
263
+ {
264
+ "name": "tags",
265
+ "type": "array"
266
+ }
267
+ ],
268
+ "passed": {
269
+ "tags": [
270
+ "a",
271
+ "b"
272
+ ]
273
+ }
274
+ },
275
+ "expected": {
276
+ "ok": true,
277
+ "props": {
278
+ "tags": [
279
+ "a",
280
+ "b"
281
+ ]
282
+ }
283
+ }
284
+ },
285
+ {
286
+ "id": "0011-object-satisfies-object",
287
+ "title": "A map satisfies a declared object.",
288
+ "since": "0.9.0",
289
+ "tags": [
290
+ "types"
291
+ ],
292
+ "input": {
293
+ "declared": [
294
+ {
295
+ "name": "meta",
296
+ "type": "object"
297
+ }
298
+ ],
299
+ "passed": {
300
+ "meta": {
301
+ "k": "v"
302
+ }
303
+ }
304
+ },
305
+ "expected": {
306
+ "ok": true,
307
+ "props": {
308
+ "meta": {
309
+ "k": "v"
310
+ }
311
+ }
312
+ }
313
+ },
314
+ {
315
+ "id": "0012-no-declaration-and-no-props",
316
+ "title": "A workflow that declares nothing, called with nothing, resolves to an empty map.",
317
+ "since": "0.9.0",
318
+ "tags": [
319
+ "happy-path",
320
+ "absence"
321
+ ],
322
+ "input": {
323
+ "declared": null,
324
+ "passed": null
325
+ },
326
+ "expected": {
327
+ "ok": true,
328
+ "props": {}
329
+ }
330
+ },
331
+ {
332
+ "id": "0101-an-unknown-key-fails",
333
+ "title": "A misspelled input name FAILS rather than sitting unread.",
334
+ "since": "0.9.0",
335
+ "tags": [
336
+ "validation",
337
+ "regression",
338
+ "silent-failure"
339
+ ],
340
+ "input": {
341
+ "declared": [
342
+ {
343
+ "name": "topic",
344
+ "type": "string"
345
+ }
346
+ ],
347
+ "passed": {
348
+ "topik": "otters"
349
+ }
350
+ },
351
+ "expected": {
352
+ "ok": false,
353
+ "code": "unknown_input"
354
+ }
355
+ },
356
+ {
357
+ "id": "0102-props-passed-to-a-workflow-declaring-none-fails",
358
+ "title": "Passing anything to a workflow that declares no inputs FAILS.",
359
+ "since": "0.9.0",
360
+ "tags": [
361
+ "validation",
362
+ "silent-failure"
363
+ ],
364
+ "input": {
365
+ "declared": null,
366
+ "passed": {
367
+ "topic": "otters"
368
+ }
369
+ },
370
+ "expected": {
371
+ "ok": false,
372
+ "code": "unknown_input"
373
+ }
374
+ },
375
+ {
376
+ "id": "0103-unknown-is-reported-before-missing-required",
377
+ "title": "A caller who misspells a required input is told about the word they typed.",
378
+ "since": "0.9.0",
379
+ "tags": [
380
+ "validation",
381
+ "ordering"
382
+ ],
383
+ "input": {
384
+ "declared": [
385
+ {
386
+ "name": "topic",
387
+ "type": "string",
388
+ "required": true
389
+ }
390
+ ],
391
+ "passed": {
392
+ "topik": "otters"
393
+ }
394
+ },
395
+ "expected": {
396
+ "ok": false,
397
+ "code": "unknown_input"
398
+ }
399
+ },
400
+ {
401
+ "id": "0104-missing-required-fails",
402
+ "title": "A required input with no default and no value FAILS.",
403
+ "since": "0.9.0",
404
+ "tags": [
405
+ "validation",
406
+ "required"
407
+ ],
408
+ "input": {
409
+ "declared": [
410
+ {
411
+ "name": "topic",
412
+ "type": "string",
413
+ "required": true
414
+ }
415
+ ],
416
+ "passed": {}
417
+ },
418
+ "expected": {
419
+ "ok": false,
420
+ "code": "missing_required"
421
+ }
422
+ },
423
+ {
424
+ "id": "0105-wrong-type-fails",
425
+ "title": "A string supplied where a number is declared FAILS.",
426
+ "since": "0.9.0",
427
+ "tags": [
428
+ "validation",
429
+ "types"
430
+ ],
431
+ "input": {
432
+ "declared": [
433
+ {
434
+ "name": "limit",
435
+ "type": "number"
436
+ }
437
+ ],
438
+ "passed": {
439
+ "limit": "ten"
440
+ }
441
+ },
442
+ "expected": {
443
+ "ok": false,
444
+ "code": "type_mismatch"
445
+ }
446
+ },
447
+ {
448
+ "id": "0106-object-does-not-satisfy-array",
449
+ "title": "A map supplied where an array is declared FAILS.",
450
+ "since": "0.9.0",
451
+ "tags": [
452
+ "validation",
453
+ "types",
454
+ "trap"
455
+ ],
456
+ "input": {
457
+ "declared": [
458
+ {
459
+ "name": "tags",
460
+ "type": "array"
461
+ }
462
+ ],
463
+ "passed": {
464
+ "tags": {
465
+ "0": "a"
466
+ }
467
+ }
468
+ },
469
+ "expected": {
470
+ "ok": false,
471
+ "code": "type_mismatch"
472
+ },
473
+ "skip": {
474
+ "php": "Not representable in PHP. `json_decode('{\"0\":\"a\"}', true)` coerces the numeric STRING key to int 0, producing a list -- so the map this case describes cannot exist on that runtime and `array_is_list` correctly reports a list. The rule itself is pinned for every runtime by 0109, which uses a non-numeric key."
475
+ },
476
+ "notes": "Kept rather than rewritten because the divergence is worth recording: a JS host and a PHP host genuinely disagree about this value, and the reason is PHP's key coercion rather than either implementation being wrong."
477
+ },
478
+ {
479
+ "id": "0107-array-does-not-satisfy-object",
480
+ "title": "A list supplied where an object is declared FAILS.",
481
+ "since": "0.9.0",
482
+ "tags": [
483
+ "validation",
484
+ "types",
485
+ "trap"
486
+ ],
487
+ "input": {
488
+ "declared": [
489
+ {
490
+ "name": "meta",
491
+ "type": "object"
492
+ }
493
+ ],
494
+ "passed": {
495
+ "meta": [
496
+ "a"
497
+ ]
498
+ }
499
+ },
500
+ "expected": {
501
+ "ok": false,
502
+ "code": "type_mismatch"
503
+ }
504
+ },
505
+ {
506
+ "id": "0108-null-does-not-satisfy-a-declared-type",
507
+ "title": "An explicit null supplied where a string is declared FAILS.",
508
+ "since": "0.9.0",
509
+ "tags": [
510
+ "validation",
511
+ "types"
512
+ ],
513
+ "input": {
514
+ "declared": [
515
+ {
516
+ "name": "topic",
517
+ "type": "string"
518
+ }
519
+ ],
520
+ "passed": {
521
+ "topic": null
522
+ }
523
+ },
524
+ "expected": {
525
+ "ok": false,
526
+ "code": "type_mismatch"
527
+ }
528
+ },
529
+ {
530
+ "id": "0109-a-string-keyed-map-does-not-satisfy-array",
531
+ "title": "A map with a non-numeric key supplied where an array is declared FAILS.",
532
+ "since": "0.9.1",
533
+ "tags": [
534
+ "validation",
535
+ "types",
536
+ "trap"
537
+ ],
538
+ "notes": "The runnable half of 0106. A non-numeric key survives json_decode on PHP as a string-keyed array, so `array_is_list` reports false and every runtime agrees. This is the case that actually pins object-is-not-array across all three.",
539
+ "input": {
540
+ "declared": [
541
+ {
542
+ "name": "tags",
543
+ "type": "array"
544
+ }
545
+ ],
546
+ "passed": {
547
+ "tags": {
548
+ "a": 1
549
+ }
550
+ }
551
+ },
552
+ "expected": {
553
+ "ok": false,
554
+ "code": "type_mismatch"
555
+ }
556
+ }
557
+ ]
558
+ }
@@ -0,0 +1,28 @@
1
+ {
2
+ "$schema": "../../../schema/suite-manifest.schema.json",
3
+ "suite": "flow/workflow-props",
4
+ "title": "Resolving a caller's props against what a workflow declares",
5
+ "since": "0.9.0",
6
+ "caseFormat": "table",
7
+ "cases": "cases.json",
8
+ "contract": {
9
+ "function": "resolveWorkflowProps(declared: array|null, passed: object|null) -> {ok: true, props: object} | {ok: false, code: string}",
10
+ "summary": "Check the flat, by-name object a caller passed against the `inputs` a workflow declares, and fill in declared defaults. `declared` is the list of input declarations ({name, type?, required?, default?}) or null when the workflow declares none. `passed` is what the caller supplied, or null. On success the result carries the RESOLVED map -- supplied values plus defaults for anything omitted, and nothing else. On failure it carries a stable `code`; the human-readable message is deliberately NOT part of the contract.",
11
+ "reference": "node",
12
+ "referenceNote": "@particle-academy/fancy-flow's src/runtime/workflow-props.ts is the behavioural source of truth, and the rules were designed there with this table written alongside rather than after.",
13
+ "implementations": [
14
+ { "language": "node", "package": "@particle-academy/fancy-flow", "symbol": "resolveWorkflowProps" },
15
+ { "language": "php", "package": "particle-academy/fancy-flow-php", "symbol": "FancyFlow\\Runtime\\WorkflowProps::resolve" },
16
+ { "language": "python", "package": "fancy-flow", "symbol": "fancy_flow.runtime.workflow_props.resolve_workflow_props" }
17
+ ]
18
+ },
19
+ "notes": [
20
+ "The CODE is asserted, never the message. Each runtime words its errors idiomatically and an English sentence is not a behaviour -- pinning the prose would hold three implementations to a translation, and would go red on a wording improvement that changed nothing.",
21
+ "The reason this feature exists is case 0101: passing a MISSPELLED key. Before workflow props, `initialInputs` was keyed by node id and an unrecognised key was not an error -- the value simply sat unread, the node saw nothing, and the run reported success with output that was quietly wrong. Every implementation must fail that case, because the silent version is what shipped.",
22
+ "Cases 0004-0006 are the falsy-value trap and they are the ones a re-implementation gets wrong. `0`, `false` and `\"\"` are values a caller MEANT to pass. A default applied with `||`, or with `??` on the wrong side, silently replaces them -- and a declared limit of 0 quietly becoming 10 is not an error anybody observes.",
23
+ "Absent is ABSENT. An input that is not supplied, has no default and is not required must not appear in the resolved map at all -- not as null, not as an empty string. PHP has one absent value and JS has two; letting that difference leak would make `{{ $props.x }}` disagree across runtimes for no useful reason.",
24
+ "`type` is optional and an omitted type accepts ANYTHING (case 0008). 'I am not asserting a shape' must not degrade into 'nothing is allowed', which is the failure mode of a validator written defensively.",
25
+ "Arrays are checked before objects (case 0010). `typeof []` is `\"object\"` in JavaScript, and a declaration saying `array` that accepts `{}` is a check that passes while meaning nothing.",
26
+ "Unknown keys are checked BEFORE required ones (case 0103). A caller who typed `topik` instead of `topic` should be told about `topik` -- the word they wrote -- rather than that `topic` is missing, which describes a key they believe they supplied."
27
+ ]
28
+ }