@particle-academy/fancy-conformance 0.2.0 → 0.5.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/README.md CHANGED
@@ -61,7 +61,11 @@ verbatim.
61
61
  |---|---|---|
62
62
  | `shared/satisfies-range` | 17 | Minimal semver range matching, including **two rows that deliberately disagree with standard semver** |
63
63
  | `shared/decimal` | 18 | Float formatting, numeric-string coercion, and money rounding |
64
+ | `shared/money-minor-units` | 26 | Minor-unit conversion across zero-decimal and three-decimal currencies |
64
65
  | `shared/strings` | 8 | Inline-markdown segmentation across CJK, emoji, combining marks and accented Latin |
66
+ | `shared/expr` | 20 | `{{ }}` dot-path resolution and branch truthiness for fancy-flow node config |
67
+ | `shared/image-header` | 16 | Image dimensions read from the header bytes, without an image library |
68
+ | `shared/flow-run-identity` | 25 | fancy-flow's run/step identity: the idempotency key a retrying connector sends, and when a retry may still reuse it |
65
69
 
66
70
  Every case carries an `id`, a `title`, the suite version it arrived in, and —
67
71
  where it exists to catch something specific — a `notes` field saying what.
package/VERSION CHANGED
@@ -1 +1 @@
1
- 0.2.0
1
+ 0.5.0
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@particle-academy/fancy-conformance",
3
- "version": "0.2.0",
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.",
3
+ "version": "0.5.0",
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 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",
7
7
  "url": "git+https://github.com/Particle-Academy/fancy-conformance.git"
@@ -0,0 +1,459 @@
1
+ {
2
+ "$schema": "../../../schema/case-table.schema.json",
3
+ "suite": "shared/feature-entitlement",
4
+ "cases": [
5
+ {
6
+ "id": "0001-entitled-granted-with-quota-left",
7
+ "title": "an enabled resource grant with quota left is entitled",
8
+ "since": "0.4.0",
9
+ "tags": [
10
+ "entitled",
11
+ "control"
12
+ ],
13
+ "fn": "entitled",
14
+ "input": {
15
+ "enabled": true,
16
+ "type": "resource",
17
+ "includedQuantity": 100,
18
+ "used": 30
19
+ },
20
+ "expected": true,
21
+ "notes": "The control row. Without it the two hazard rows below prove nothing, because an implementation that always returns false would pass them."
22
+ },
23
+ {
24
+ "id": "0002-entitled-granted-with-quota-exhausted",
25
+ "title": "an enabled resource grant with ZERO quota left is still entitled",
26
+ "since": "0.4.0",
27
+ "tags": [
28
+ "entitled",
29
+ "hazard",
30
+ "ruling-1"
31
+ ],
32
+ "fn": "entitled",
33
+ "input": {
34
+ "enabled": true,
35
+ "type": "resource",
36
+ "includedQuantity": 100,
37
+ "used": 100
38
+ },
39
+ "expected": true,
40
+ "notes": "THE ruling. Both twins answered false here, because a grant-sourced resource feature was only 'on' while quota remained -- while the same feature defined in the registry was on regardless. One question, two answers, decided by which layer the plan happened to be modelled in. An implementation that reintroduces the quota check fails this row and 0004 and nothing else."
41
+ },
42
+ {
43
+ "id": "0003-entitled-not-granted",
44
+ "title": "a disabled grant is not entitled, whatever the quota says",
45
+ "since": "0.4.0",
46
+ "tags": [
47
+ "entitled"
48
+ ],
49
+ "fn": "entitled",
50
+ "input": {
51
+ "enabled": false,
52
+ "type": "resource",
53
+ "includedQuantity": 100,
54
+ "used": 0
55
+ },
56
+ "expected": false,
57
+ "notes": "The other half of the control: entitlement still tracks `enabled`. A runtime that hard-coded true to pass 0002 fails here."
58
+ },
59
+ {
60
+ "id": "0004-entitled-past-the-ceiling",
61
+ "title": "usage PAST the included quantity does not revoke entitlement",
62
+ "since": "0.4.0",
63
+ "tags": [
64
+ "entitled",
65
+ "hazard",
66
+ "ruling-1",
67
+ "overage"
68
+ ],
69
+ "fn": "entitled",
70
+ "input": {
71
+ "enabled": true,
72
+ "type": "resource",
73
+ "includedQuantity": 100,
74
+ "used": 140
75
+ },
76
+ "expected": true,
77
+ "notes": "A subject in billable overage is emphatically still entitled -- they are paying for the feature twice over. A settings page that hides it here is hiding it at the exact moment the customer is spending most."
78
+ },
79
+ {
80
+ "id": "0005-entitled-boolean-feature",
81
+ "title": "a boolean grant ignores quantity fields entirely",
82
+ "since": "0.4.0",
83
+ "tags": [
84
+ "entitled"
85
+ ],
86
+ "fn": "entitled",
87
+ "input": {
88
+ "enabled": true,
89
+ "type": "boolean",
90
+ "includedQuantity": null,
91
+ "used": 0
92
+ },
93
+ "expected": true
94
+ },
95
+ {
96
+ "id": "0006-ceiling-no-overage-configured",
97
+ "title": "a null overage limit means no overage: the ceiling is the included quantity",
98
+ "since": "0.4.0",
99
+ "tags": [
100
+ "consumptionCeiling",
101
+ "hazard",
102
+ "ruling-2"
103
+ ],
104
+ "fn": "consumptionCeiling",
105
+ "input": {
106
+ "includedQuantity": 100,
107
+ "overageLimit": null
108
+ },
109
+ "expected": 100,
110
+ "notes": "The row that keeps every existing install behaving as it does today. The column has been stored-and-ignored for the life of the package, so every untouched row is null -- reading null as 'unbounded' would turn each of them into an unlimited spending authority on upgrade."
111
+ },
112
+ {
113
+ "id": "0007-ceiling-explicit-zero",
114
+ "title": "an overage limit of 0 is the same as none, stated out loud",
115
+ "since": "0.4.0",
116
+ "tags": [
117
+ "consumptionCeiling"
118
+ ],
119
+ "fn": "consumptionCeiling",
120
+ "input": {
121
+ "includedQuantity": 100,
122
+ "overageLimit": 0
123
+ },
124
+ "expected": 100
125
+ },
126
+ {
127
+ "id": "0008-ceiling-with-overage",
128
+ "title": "an overage limit of 50 on 100 included gives a ceiling of 150",
129
+ "since": "0.4.0",
130
+ "tags": [
131
+ "consumptionCeiling",
132
+ "overage"
133
+ ],
134
+ "fn": "consumptionCeiling",
135
+ "input": {
136
+ "includedQuantity": 100,
137
+ "overageLimit": 50
138
+ },
139
+ "expected": 150,
140
+ "notes": "overage_limit is a ceiling on BILLABLE OVERAGE, not the total. 150, never 50."
141
+ },
142
+ {
143
+ "id": "0009-ceiling-unlimited-included",
144
+ "title": "an unlimited included quantity has no ceiling, and overage is meaningless",
145
+ "since": "0.4.0",
146
+ "tags": [
147
+ "consumptionCeiling",
148
+ "unlimited"
149
+ ],
150
+ "fn": "consumptionCeiling",
151
+ "input": {
152
+ "includedQuantity": null,
153
+ "overageLimit": 50
154
+ },
155
+ "expected": null,
156
+ "notes": "There is no included line to exceed, so there is nothing to bill as overage. The overage limit is ignored rather than added to something."
157
+ },
158
+ {
159
+ "id": "0010-ceiling-zero-included-with-overage",
160
+ "title": "zero included plus an overage allowance is a pay-as-you-go band",
161
+ "since": "0.4.0",
162
+ "tags": [
163
+ "consumptionCeiling",
164
+ "overage"
165
+ ],
166
+ "fn": "consumptionCeiling",
167
+ "input": {
168
+ "includedQuantity": 0,
169
+ "overageLimit": 500
170
+ },
171
+ "expected": 500,
172
+ "notes": "Nothing free, 500 billable. `includedQuantity: 0` and `includedQuantity: null` are opposite configurations and a runtime that conflates them charges nobody or everybody."
173
+ },
174
+ {
175
+ "id": "0011-allows-exactly-to-the-ceiling",
176
+ "title": "consuming exactly up to the ceiling is allowed",
177
+ "since": "0.4.0",
178
+ "tags": [
179
+ "allowsConsumption",
180
+ "hazard",
181
+ "boundary"
182
+ ],
183
+ "fn": "allowsConsumption",
184
+ "input": {
185
+ "used": 90,
186
+ "amount": 10,
187
+ "ceiling": 100
188
+ },
189
+ "expected": true,
190
+ "notes": "The off-by-one. `used + amount <= ceiling`, not `<`. A plan that says 100 must permit the hundredth unit."
191
+ },
192
+ {
193
+ "id": "0012-allows-one-past-the-ceiling",
194
+ "title": "consuming one past the ceiling is refused",
195
+ "since": "0.4.0",
196
+ "tags": [
197
+ "allowsConsumption",
198
+ "boundary"
199
+ ],
200
+ "fn": "allowsConsumption",
201
+ "input": {
202
+ "used": 90,
203
+ "amount": 11,
204
+ "ceiling": 100
205
+ },
206
+ "expected": false
207
+ },
208
+ {
209
+ "id": "0013-allows-all-or-nothing",
210
+ "title": "a request that only partly fits is refused entirely",
211
+ "since": "0.4.0",
212
+ "tags": [
213
+ "allowsConsumption",
214
+ "hazard"
215
+ ],
216
+ "fn": "allowsConsumption",
217
+ "input": {
218
+ "used": 0,
219
+ "amount": 150,
220
+ "ceiling": 100
221
+ },
222
+ "expected": false,
223
+ "notes": "No partial fill. A caller asking for 150 and silently getting 100 has no way to know, because the answer is a boolean -- and callers do not check quantities they did not ask to be told about."
224
+ },
225
+ {
226
+ "id": "0014-allows-unlimited",
227
+ "title": "a null ceiling permits any amount",
228
+ "since": "0.4.0",
229
+ "tags": [
230
+ "allowsConsumption",
231
+ "unlimited"
232
+ ],
233
+ "fn": "allowsConsumption",
234
+ "input": {
235
+ "used": 10000000,
236
+ "amount": 1,
237
+ "ceiling": null
238
+ },
239
+ "expected": true
240
+ },
241
+ {
242
+ "id": "0015-allows-zero-amount",
243
+ "title": "consuming nothing is always allowed, even at the ceiling",
244
+ "since": "0.4.0",
245
+ "tags": [
246
+ "allowsConsumption",
247
+ "boundary"
248
+ ],
249
+ "fn": "allowsConsumption",
250
+ "input": {
251
+ "used": 100,
252
+ "amount": 0,
253
+ "ceiling": 100
254
+ },
255
+ "expected": true
256
+ },
257
+ {
258
+ "id": "0016-overage-delta-wholly-inside-the-allowance",
259
+ "title": "consumption that stays under the included quantity bills nothing",
260
+ "since": "0.4.0",
261
+ "tags": [
262
+ "overageDelta",
263
+ "control"
264
+ ],
265
+ "fn": "overageDelta",
266
+ "input": {
267
+ "used": 10,
268
+ "amount": 20,
269
+ "includedQuantity": 100
270
+ },
271
+ "expected": 0
272
+ },
273
+ {
274
+ "id": "0017-overage-delta-straddling-the-line",
275
+ "title": "consumption that crosses the included line bills only the part above it",
276
+ "since": "0.4.0",
277
+ "tags": [
278
+ "overageDelta",
279
+ "hazard",
280
+ "overage"
281
+ ],
282
+ "fn": "overageDelta",
283
+ "input": {
284
+ "used": 90,
285
+ "amount": 30,
286
+ "includedQuantity": 100
287
+ },
288
+ "expected": 20,
289
+ "notes": "20, not 30 and not 0. An implementation that bills the whole amount once the line is crossed overcharges by the included remainder; one that bills nothing until the line is passed loses it."
290
+ },
291
+ {
292
+ "id": "0018-overage-delta-already-in-overage",
293
+ "title": "consumption that starts ABOVE the included line bills the whole amount",
294
+ "since": "0.4.0",
295
+ "tags": [
296
+ "overageDelta",
297
+ "hazard",
298
+ "overage"
299
+ ],
300
+ "fn": "overageDelta",
301
+ "input": {
302
+ "used": 140,
303
+ "amount": 10,
304
+ "includedQuantity": 100
305
+ },
306
+ "expected": 10,
307
+ "notes": "The row a naive `max(0, after - included)` gets wrong: it answers 50, re-billing the 40 units already recorded. Subtracting the overage that existed BEFORE the call is what makes the function composable over a period."
308
+ },
309
+ {
310
+ "id": "0019-overage-delta-refund",
311
+ "title": "a refund unwinds overage as a negative delta",
312
+ "since": "0.4.0",
313
+ "tags": [
314
+ "overageDelta",
315
+ "refund",
316
+ "sign"
317
+ ],
318
+ "fn": "overageDelta",
319
+ "input": {
320
+ "used": 140,
321
+ "amount": -10,
322
+ "includedQuantity": 100
323
+ },
324
+ "expected": -10,
325
+ "notes": "One signed function serves increment and decrement, so the two cannot drift apart. The stored total is clamped at zero by the caller, not here."
326
+ },
327
+ {
328
+ "id": "0020-overage-delta-refund-below-the-line",
329
+ "title": "a refund that drops usage below the included line unwinds only the billable part",
330
+ "since": "0.4.0",
331
+ "tags": [
332
+ "overageDelta",
333
+ "refund",
334
+ "hazard"
335
+ ],
336
+ "fn": "overageDelta",
337
+ "input": {
338
+ "used": 110,
339
+ "amount": -30,
340
+ "includedQuantity": 100
341
+ },
342
+ "expected": -10,
343
+ "notes": "-10, not -30. Refunding 30 units when only 10 of them were ever billable must not credit 20 units of overage that never existed."
344
+ },
345
+ {
346
+ "id": "0021-overage-delta-unlimited",
347
+ "title": "an unlimited allowance never accrues overage",
348
+ "since": "0.4.0",
349
+ "tags": [
350
+ "overageDelta",
351
+ "unlimited"
352
+ ],
353
+ "fn": "overageDelta",
354
+ "input": {
355
+ "used": 10000,
356
+ "amount": 500,
357
+ "includedQuantity": null
358
+ },
359
+ "expected": 0,
360
+ "notes": "Unlimited is not unmetered -- usage is still recorded -- but there is no included line, so nothing above it."
361
+ },
362
+ {
363
+ "id": "0022-can-consume-inside-the-included-quantity",
364
+ "title": "an entitled subject inside their allowance may consume",
365
+ "since": "0.4.0",
366
+ "tags": [
367
+ "canConsume",
368
+ "control"
369
+ ],
370
+ "fn": "canConsume",
371
+ "input": {
372
+ "enabled": true,
373
+ "includedQuantity": 100,
374
+ "overageLimit": null,
375
+ "used": 50,
376
+ "amount": 10
377
+ },
378
+ "expected": true
379
+ },
380
+ {
381
+ "id": "0023-can-consume-exhausted-without-overage",
382
+ "title": "an exhausted allowance with no overage refuses -- and this is what canAccess used to answer",
383
+ "since": "0.4.0",
384
+ "tags": [
385
+ "canConsume",
386
+ "ruling-1",
387
+ "hazard"
388
+ ],
389
+ "fn": "canConsume",
390
+ "input": {
391
+ "enabled": true,
392
+ "includedQuantity": 100,
393
+ "overageLimit": null,
394
+ "used": 100,
395
+ "amount": 1
396
+ },
397
+ "expected": false,
398
+ "notes": "The migration target. A caller who used canAccess as a consumption gate wants THIS answer, and canConsume is where it moved."
399
+ },
400
+ {
401
+ "id": "0024-can-consume-into-the-overage-band",
402
+ "title": "an exhausted allowance WITH an overage limit permits billable consumption",
403
+ "since": "0.4.0",
404
+ "tags": [
405
+ "canConsume",
406
+ "overage",
407
+ "hazard",
408
+ "ruling-2"
409
+ ],
410
+ "fn": "canConsume",
411
+ "input": {
412
+ "enabled": true,
413
+ "includedQuantity": 100,
414
+ "overageLimit": 50,
415
+ "used": 100,
416
+ "amount": 1
417
+ },
418
+ "expected": true,
419
+ "notes": "The behaviour change overage_limit buys. Before this ruling the column was stored by three runtimes and read by none, so this answered false everywhere."
420
+ },
421
+ {
422
+ "id": "0025-can-consume-past-the-overage-ceiling",
423
+ "title": "the overage band has an end, and it is enforced",
424
+ "since": "0.4.0",
425
+ "tags": [
426
+ "canConsume",
427
+ "overage",
428
+ "boundary"
429
+ ],
430
+ "fn": "canConsume",
431
+ "input": {
432
+ "enabled": true,
433
+ "includedQuantity": 100,
434
+ "overageLimit": 50,
435
+ "used": 150,
436
+ "amount": 1
437
+ },
438
+ "expected": false,
439
+ "notes": "overage_limit is a ceiling, not an alert. A field named *_limit that does not limit is the same defect in a new costume."
440
+ },
441
+ {
442
+ "id": "0026-can-consume-not-entitled",
443
+ "title": "no entitlement means no consumption, however much quota is configured",
444
+ "since": "0.4.0",
445
+ "tags": [
446
+ "canConsume"
447
+ ],
448
+ "fn": "canConsume",
449
+ "input": {
450
+ "enabled": false,
451
+ "includedQuantity": 1000,
452
+ "overageLimit": 1000,
453
+ "used": 0,
454
+ "amount": 1
455
+ },
456
+ "expected": false
457
+ }
458
+ ]
459
+ }
@@ -0,0 +1,33 @@
1
+ {
2
+ "$schema": "../../../schema/suite-manifest.schema.json",
3
+ "suite": "shared/feature-entitlement",
4
+ "title": "Entitlement, quota ceilings and billable overage",
5
+ "since": "0.4.0",
6
+ "caseFormat": "table",
7
+ "cases": "cases.json",
8
+ "contract": {
9
+ "summary": "The five decisions a gating engine makes about a metered feature: is the subject entitled to it at all, how far may their usage go, does this particular request fit, how much of it is billable overage, and may they take it. Entitlement and quota are separate questions here on purpose -- they were the same question, answered differently depending on which layer the plan happened to be modelled in.",
10
+ "functions": {
11
+ "entitled": "entitled(enabled: bool, type: string, includedQuantity: int|null, used: int) -> bool - is the feature granted. Quota-blind BY CONTRACT: the quantity arguments are handed over so the answer can be required to ignore them.",
12
+ "consumptionCeiling": "consumptionCeiling(includedQuantity: int|null, overageLimit: int|null) -> int|null - the highest total usage permitted. null means unlimited.",
13
+ "allowsConsumption": "allowsConsumption(used: int, amount: int, ceiling: int|null) -> bool - does this request fit under the ceiling. All-or-nothing.",
14
+ "overageDelta": "overageDelta(used: int, amount: int, includedQuantity: int|null) -> int - the billable units this consumption adds. Signed: a negative amount (a refund) gives a negative delta.",
15
+ "canConsume": "canConsume(enabled: bool, includedQuantity: int|null, overageLimit: int|null, used: int, amount: int) -> bool - entitled AND it fits. The quota-aware read that canAccess used to answer for a source grant."
16
+ },
17
+ "reference": "php",
18
+ "referenceNote": "Two of these five are NEW contracts rather than ports of existing behaviour -- `canAccess` was quota-blind in one code path and quota-aware in another, and `overage_limit` was stored by all three runtimes and read by none -- so there was no prior implementation whose output could be the golden. The goldens are computed by scripts/build-feature-entitlement-cases.py from the ruling in .ai/plans/fancy-commerce-gating-rulings.md, and `php` is named as reference because laravel-fms is the package that ruling changes most. Every row that is not marked `control` exists to catch one specific wrong answer, named in its notes.",
19
+ "implementations": [
20
+ { "language": "php", "package": "particle-academy/laravel-fms", "symbol": "ParticleAcademy\\Fms\\Quota" },
21
+ { "language": "node", "package": "@particle-academy/fancy-features", "symbol": "quota" },
22
+ { "language": "python", "package": "fancy-features", "symbol": "fancy_features.quota" }
23
+ ]
24
+ },
25
+ "notes": [
26
+ "Every quantity here is a WHOLE UNIT and every value is an integer. A resource feature is counted, never measured, so there is no fractional unit, no rate and no proportional split anywhere in these rows.",
27
+ "Money is deliberately absent. It enters only when a host multiplies recorded overage units by a unit amount in minor units, which is `lineTotal` in shared/money-minor-units -- referenced rather than duplicated, because a golden that exists twice is a golden that can disagree with itself.",
28
+ "`entitled` receives includedQuantity and used and must IGNORE them. That is not a redundant signature: it is the assertion. A runtime that reintroduces the quota check into canAccess fails 0002 and 0004 and nothing else, which is the exact signal this suite exists to produce.",
29
+ "overage_limit is a CEILING on billable overage, not a soft alert. null means no overage at all, which is what every existing database row says, so this ruling is opt-in for anyone upgrading rather than a new unlimited spending authority.",
30
+ "NOT COVERED, deliberately: the atomicity of check-and-increment. A TOCTOU race between reading a quota and writing usage is a property of a store under concurrency and has no representation in a case table; each runtime asserts it against its own locking store.",
31
+ "NOT COVERED: what a runtime does when asked to consume a negative amount. All three refuse it (a negative consume walks past every ceiling), but a refusal has no representation in this format -- each implementation asserts the raise in its own suite."
32
+ ]
33
+ }