@particle-academy/fancy-conformance 0.16.0 → 0.17.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.
|
|
1
|
+
0.17.0
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@particle-academy/fancy-conformance",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.17.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",
|
|
@@ -1470,6 +1470,81 @@
|
|
|
1470
1470
|
"value": null
|
|
1471
1471
|
},
|
|
1472
1472
|
"notes": "The companion to 0904, and the form every runtime can express. PHP cannot distinguish an EMPTY object from an empty array -- both decode to the same value and both read as a list -- so 0904 is skipped there. A non-empty object is unambiguous in all three, which keeps the rule pinned rather than merely skipped. Same shape as flow/workflow-props 0106/0109."
|
|
1473
|
+
},
|
|
1474
|
+
{
|
|
1475
|
+
"id": "1001-the-normalisation-an-agent-writes",
|
|
1476
|
+
"title": "The production expression that started this package: object literal, || fallback chains and a ternary, every operand legitimately absent.",
|
|
1477
|
+
"since": "0.17.0",
|
|
1478
|
+
"tags": [
|
|
1479
|
+
"expr",
|
|
1480
|
+
"field-report"
|
|
1481
|
+
],
|
|
1482
|
+
"notes": "VERBATIM from a consumer's production Op, authored by a chat agent that reasonably assumed `{{ }}` was an expression language. Under the old dot-path-only resolver the WHOLE thing evaluated to null and the run reported success. They describe it as 'what an agent writes when it is trying to normalise two trigger shapes into one, which is the single most common thing they attempt' -- so this is the shape to keep working, not a toy. Every other row here isolates one rule; this one is the rules COMPOSED, which is how they actually arrive.",
|
|
1483
|
+
"input": {
|
|
1484
|
+
"expression": "{ transcript: in.transcript || in.file_content || '', deal_id: in.deal_id || '', file_name: in.name || 'Pasted transcript', source: in.transcript ? 'manual' : 'file_event' }",
|
|
1485
|
+
"context": {
|
|
1486
|
+
"in": {
|
|
1487
|
+
"transcript": null,
|
|
1488
|
+
"file_content": "…the call went well…",
|
|
1489
|
+
"deal_id": "D-1",
|
|
1490
|
+
"name": null
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
},
|
|
1494
|
+
"expected": {
|
|
1495
|
+
"ok": true,
|
|
1496
|
+
"value": {
|
|
1497
|
+
"transcript": "…the call went well…",
|
|
1498
|
+
"deal_id": "D-1",
|
|
1499
|
+
"file_name": "Pasted transcript",
|
|
1500
|
+
"source": "file_event"
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
},
|
|
1504
|
+
{
|
|
1505
|
+
"id": "1002-the-branch-condition-that-took-the-wrong-road",
|
|
1506
|
+
"title": "The other half of the same Op: a branch condition using && and !==.",
|
|
1507
|
+
"since": "0.17.0",
|
|
1508
|
+
"tags": [
|
|
1509
|
+
"expr",
|
|
1510
|
+
"field-report"
|
|
1511
|
+
],
|
|
1512
|
+
"notes": "The expensive face of the defect. `truthy(null)` is false, so a condition the engine could not evaluate did not error -- it silently picked the false road on EVERY run while the run reported success. 0406 pins the same expression against a present deal_id; this pins it against an ABSENT one, where the honest answer is a real false rather than a false that means 'could not tell'.",
|
|
1513
|
+
"input": {
|
|
1514
|
+
"expression": "in.deal_id && in.deal_id !== ''",
|
|
1515
|
+
"context": {
|
|
1516
|
+
"in": {
|
|
1517
|
+
"deal_id": null
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
},
|
|
1521
|
+
"expected": {
|
|
1522
|
+
"ok": true,
|
|
1523
|
+
"value": null
|
|
1524
|
+
}
|
|
1525
|
+
},
|
|
1526
|
+
{
|
|
1527
|
+
"id": "1003-empty-string-fallback-survives-the-chain",
|
|
1528
|
+
"title": "A || chain ending in '' yields the empty string, not null.",
|
|
1529
|
+
"since": "0.17.0",
|
|
1530
|
+
"tags": [
|
|
1531
|
+
"expr",
|
|
1532
|
+
"field-report"
|
|
1533
|
+
],
|
|
1534
|
+
"notes": "Extracted from 1001 because the composed row would still pass if this rule broke -- its `transcript` key never reaches the final fallback. An author writes `|| ''` precisely so a missing field becomes a printable gap rather than the word null.",
|
|
1535
|
+
"input": {
|
|
1536
|
+
"expression": "in.a || in.b || ''",
|
|
1537
|
+
"context": {
|
|
1538
|
+
"in": {
|
|
1539
|
+
"a": null,
|
|
1540
|
+
"b": null
|
|
1541
|
+
}
|
|
1542
|
+
}
|
|
1543
|
+
},
|
|
1544
|
+
"expected": {
|
|
1545
|
+
"ok": true,
|
|
1546
|
+
"value": ""
|
|
1547
|
+
}
|
|
1473
1548
|
}
|
|
1474
1549
|
]
|
|
1475
1550
|
}
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../../../schema/case-table.schema.json",
|
|
3
|
+
"suite": "expr/references",
|
|
4
|
+
"cases": [
|
|
5
|
+
{
|
|
6
|
+
"id": "0101-single-root",
|
|
7
|
+
"title": "One path reads one root.",
|
|
8
|
+
"since": "0.17.0",
|
|
9
|
+
"tags": [
|
|
10
|
+
"expr",
|
|
11
|
+
"references"
|
|
12
|
+
],
|
|
13
|
+
"input": {
|
|
14
|
+
"expression": "in.content"
|
|
15
|
+
},
|
|
16
|
+
"expected": {
|
|
17
|
+
"ok": true,
|
|
18
|
+
"value": [
|
|
19
|
+
"in"
|
|
20
|
+
]
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
"id": "0102-unique-and-sorted",
|
|
25
|
+
"title": "Repeated roots collapse; the order is sorted, not source order.",
|
|
26
|
+
"since": "0.17.0",
|
|
27
|
+
"tags": [
|
|
28
|
+
"expr",
|
|
29
|
+
"references"
|
|
30
|
+
],
|
|
31
|
+
"notes": "Sorted and unique so three languages produce the SAME list. A set iterated in insertion order agrees with a sorted list often enough to look correct and not always -- and the disagreement would surface as a host rejecting an expression in one runtime and accepting it in another.",
|
|
32
|
+
"input": {
|
|
33
|
+
"expression": "user.name + in.a + in.b + user.id"
|
|
34
|
+
},
|
|
35
|
+
"expected": {
|
|
36
|
+
"ok": true,
|
|
37
|
+
"value": [
|
|
38
|
+
"in",
|
|
39
|
+
"user"
|
|
40
|
+
]
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"id": "0103-dollar-root-is-reported-like-any-other",
|
|
45
|
+
"title": "A $-prefixed root is returned, not filtered and not special-cased.",
|
|
46
|
+
"since": "0.17.0",
|
|
47
|
+
"tags": [
|
|
48
|
+
"expr",
|
|
49
|
+
"references"
|
|
50
|
+
],
|
|
51
|
+
"notes": "THE REPORTED CASE. This package does not know whether `$now` exists -- `$json`, `$input` and `$props` do exist in one host and mean nothing in another. It reports what is read; the HOST owns the list. Filtering `$` names here would silently delete the only signal the check needs.",
|
|
52
|
+
"input": {
|
|
53
|
+
"expression": "$now"
|
|
54
|
+
},
|
|
55
|
+
"expected": {
|
|
56
|
+
"ok": true,
|
|
57
|
+
"value": [
|
|
58
|
+
"$now"
|
|
59
|
+
]
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
"id": "0104-mixed-dollar-and-plain",
|
|
64
|
+
"title": "Both kinds in one expression.",
|
|
65
|
+
"since": "0.17.0",
|
|
66
|
+
"tags": [
|
|
67
|
+
"expr",
|
|
68
|
+
"references"
|
|
69
|
+
],
|
|
70
|
+
"input": {
|
|
71
|
+
"expression": "$props.content || in.content"
|
|
72
|
+
},
|
|
73
|
+
"expected": {
|
|
74
|
+
"ok": true,
|
|
75
|
+
"value": [
|
|
76
|
+
"$props",
|
|
77
|
+
"in"
|
|
78
|
+
]
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
"id": "0105-computed-index-is-a-reference",
|
|
83
|
+
"title": "A computed index reads a root too.",
|
|
84
|
+
"since": "0.17.0",
|
|
85
|
+
"tags": [
|
|
86
|
+
"expr",
|
|
87
|
+
"references"
|
|
88
|
+
],
|
|
89
|
+
"notes": "An implementation that walked only `.name` steps passes every other row in this suite and fails exactly here. A typo'd index gets the same silent empty as `$now`, so it needs the same static answer.",
|
|
90
|
+
"input": {
|
|
91
|
+
"expression": "items[i]"
|
|
92
|
+
},
|
|
93
|
+
"expected": {
|
|
94
|
+
"ok": true,
|
|
95
|
+
"value": [
|
|
96
|
+
"i",
|
|
97
|
+
"items"
|
|
98
|
+
]
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
"id": "0106-object-keys-are-not-references",
|
|
103
|
+
"title": "An object literal's KEYS are written, not read.",
|
|
104
|
+
"since": "0.17.0",
|
|
105
|
+
"tags": [
|
|
106
|
+
"expr",
|
|
107
|
+
"references"
|
|
108
|
+
],
|
|
109
|
+
"notes": "The damaging direction: an implementation collecting every identifier would report `transcript` and `source`, and the host would reject a perfectly valid expression. A false rejection at save time is worse than a missed one, because the author has no way to comply.",
|
|
110
|
+
"input": {
|
|
111
|
+
"expression": "{ transcript: in.content, source: 'manual' }"
|
|
112
|
+
},
|
|
113
|
+
"expected": {
|
|
114
|
+
"ok": true,
|
|
115
|
+
"value": [
|
|
116
|
+
"in"
|
|
117
|
+
]
|
|
118
|
+
}
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
"id": "0107-literals-read-nothing",
|
|
122
|
+
"title": "An expression with no paths reads nothing.",
|
|
123
|
+
"since": "0.17.0",
|
|
124
|
+
"tags": [
|
|
125
|
+
"expr",
|
|
126
|
+
"references"
|
|
127
|
+
],
|
|
128
|
+
"input": {
|
|
129
|
+
"expression": "1 + 2 === 3 ? 'yes' : 'no'"
|
|
130
|
+
},
|
|
131
|
+
"expected": {
|
|
132
|
+
"ok": true,
|
|
133
|
+
"value": []
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
"id": "0108-both-sides-of-a-branch",
|
|
138
|
+
"title": "Every branch is read, not only the taken one.",
|
|
139
|
+
"since": "0.17.0",
|
|
140
|
+
"tags": [
|
|
141
|
+
"expr",
|
|
142
|
+
"references"
|
|
143
|
+
],
|
|
144
|
+
"notes": "A static question has no run to take a branch in. An implementation that short-circuited here -- reusing the evaluator's logic -- would report only what one run happened to touch, and the host would approve an expression that fails on the other road.",
|
|
145
|
+
"input": {
|
|
146
|
+
"expression": "a ? b : c"
|
|
147
|
+
},
|
|
148
|
+
"expected": {
|
|
149
|
+
"ok": true,
|
|
150
|
+
"value": [
|
|
151
|
+
"a",
|
|
152
|
+
"b",
|
|
153
|
+
"c"
|
|
154
|
+
]
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
"id": "0109-the-field-report-normalisation",
|
|
159
|
+
"title": "The consumer's production expression reads exactly one root.",
|
|
160
|
+
"since": "0.17.0",
|
|
161
|
+
"tags": [
|
|
162
|
+
"expr",
|
|
163
|
+
"references"
|
|
164
|
+
],
|
|
165
|
+
"notes": "The same expression as expr/evaluate 1001, asked the static question. Four keys, three fallback chains and a ternary, and the honest answer is one root -- which is what makes the check cheap enough for a host to run on every save.",
|
|
166
|
+
"input": {
|
|
167
|
+
"expression": "{ transcript: in.transcript || in.file_content || '', deal_id: in.deal_id || '', file_name: in.name || 'Pasted transcript', source: in.transcript ? 'manual' : 'file_event' }"
|
|
168
|
+
},
|
|
169
|
+
"expected": {
|
|
170
|
+
"ok": true,
|
|
171
|
+
"value": [
|
|
172
|
+
"in"
|
|
173
|
+
]
|
|
174
|
+
}
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
"id": "0110-length-is-not-a-root",
|
|
178
|
+
"title": "`.length` is a step, never a name to supply.",
|
|
179
|
+
"since": "0.17.0",
|
|
180
|
+
"tags": [
|
|
181
|
+
"expr",
|
|
182
|
+
"references"
|
|
183
|
+
],
|
|
184
|
+
"input": {
|
|
185
|
+
"expression": "results.length === 0"
|
|
186
|
+
},
|
|
187
|
+
"expected": {
|
|
188
|
+
"ok": true,
|
|
189
|
+
"value": [
|
|
190
|
+
"results"
|
|
191
|
+
]
|
|
192
|
+
}
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
"id": "0201-malformed-does-not-parse",
|
|
196
|
+
"title": "A malformed expression fails here exactly as it fails in parse().",
|
|
197
|
+
"since": "0.17.0",
|
|
198
|
+
"tags": [
|
|
199
|
+
"expr",
|
|
200
|
+
"references"
|
|
201
|
+
],
|
|
202
|
+
"notes": "Same outcome, same reason: this is a question about a syntax tree, and there is no tree. Returning an empty list instead would tell a host 'this expression needs nothing', and it would save a node that can never run.",
|
|
203
|
+
"input": {
|
|
204
|
+
"expression": "in.a &&"
|
|
205
|
+
},
|
|
206
|
+
"expected": {
|
|
207
|
+
"ok": false
|
|
208
|
+
}
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
"id": "0202-a-call-does-not-parse",
|
|
212
|
+
"title": "A call is still refused at parse time.",
|
|
213
|
+
"since": "0.17.0",
|
|
214
|
+
"tags": [
|
|
215
|
+
"expr",
|
|
216
|
+
"references"
|
|
217
|
+
],
|
|
218
|
+
"input": {
|
|
219
|
+
"expression": "count(in.items)"
|
|
220
|
+
},
|
|
221
|
+
"expected": {
|
|
222
|
+
"ok": false
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
]
|
|
226
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "../../../schema/suite-manifest.schema.json",
|
|
3
|
+
"suite": "expr/references",
|
|
4
|
+
"title": "What an expression READS -- the root names a host must be able to supply",
|
|
5
|
+
"since": "0.17.0",
|
|
6
|
+
"caseFormat": "table",
|
|
7
|
+
"cases": "cases.json",
|
|
8
|
+
"contract": {
|
|
9
|
+
"function": "references(expression: string) -> { ok: true, value: string[] } | { ok: false }",
|
|
10
|
+
"summary": "Return the ROOT identifiers an expression reads, unique and sorted, without evaluating anything and without a context. `{ ok: false }` when the expression cannot PARSE -- the same failure `parse()` reports, because this is a static question asked of the same syntax tree. Object-literal KEYS are not references; a computed index IS one.",
|
|
11
|
+
"reference": "node",
|
|
12
|
+
"implementations": [
|
|
13
|
+
{ "language": "node", "package": "@particle-academy/fancy-expr", "symbol": "references" },
|
|
14
|
+
{ "language": "php", "package": "particle-academy/fancy-expr", "symbol": "FancyExpr\\Expr::references" },
|
|
15
|
+
{ "language": "python", "package": "fancy-expr", "symbol": "fancy_expr.references" }
|
|
16
|
+
]
|
|
17
|
+
},
|
|
18
|
+
"notes": [
|
|
19
|
+
"WHY THIS EXISTS, and it is not a convenience. A consumer reported that `{{ $now }}` renders as nothing: `$`-prefixed roots read to an author as ENGINE-PROVIDED, so agents reach for `$now` / `$today` / `$index` the way they reach for the two that exist. A real document shipped titled 'Deal List Export -' with the date silently missing. Their words: an unknown `$` root is detectable at PARSE time in a way `in.genuinely_absent` is not.",
|
|
20
|
+
"THE HOST DECIDES, NOT THIS PACKAGE. `fancy-expr` must never carry a list of magic root names -- it does not know whether `$props` exists, and inventing an allowlist would make it wrong for every host but one. So it answers the only question it CAN answer honestly ('what does this expression read?') and the host compares that against what it actually provides. That keeps the grammar host-agnostic while making the check possible at all.",
|
|
21
|
+
"IT ALSO CATCHES THE SECOND REPORTED SHAPE. The same consumer reported `{{ n2.transcript }}` -- a REAL node id, two hops upstream, which is legal-looking and resolves to nothing because a node id only addresses a direct predecessor. A host holding the predecessor set can refuse that from these roots too. One primitive, both shapes, and neither rule living in this package.",
|
|
22
|
+
"WHAT IT DELIBERATELY CANNOT CATCH: `{{ in.output }}` -- a real port and a field that node never emits. The root is legitimate, so nothing static can tell an absent field from a field that is absent THIS RUN. That is the boundary of the idea and is recorded so nobody reads a green check as covering it.",
|
|
23
|
+
"COMPUTED INDICES COUNT (0105). `items[i]` reads `i`, and an author who typo'd it gets the same silent empty as `$now`. An implementation that walked only `.name` steps would pass every other row here.",
|
|
24
|
+
"OBJECT KEYS DO NOT COUNT (0106). `{ transcript: in.content }` reads `in` and nothing else -- `transcript` is a key being written, not a name being read. An implementation collecting every identifier it sees would report both and make the host reject a valid expression, which is the more damaging direction of wrong."
|
|
25
|
+
]
|
|
26
|
+
}
|