@intentius/chant 0.19.1 → 0.21.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.
Files changed (164) hide show
  1. package/dist/build-params.d.ts +108 -0
  2. package/dist/build-params.d.ts.map +1 -0
  3. package/dist/build.d.ts +79 -1
  4. package/dist/build.d.ts.map +1 -1
  5. package/dist/cli/commands/build.d.ts +31 -0
  6. package/dist/cli/commands/build.d.ts.map +1 -1
  7. package/dist/cli/commands/check-lexicon-examples.d.ts +41 -0
  8. package/dist/cli/commands/check-lexicon-examples.d.ts.map +1 -0
  9. package/dist/cli/commands/check-lexicon-intrinsics.d.ts +71 -0
  10. package/dist/cli/commands/check-lexicon-intrinsics.d.ts.map +1 -0
  11. package/dist/cli/commands/check-lexicon.d.ts +1 -1
  12. package/dist/cli/commands/check-lexicon.d.ts.map +1 -1
  13. package/dist/cli/commands/lint.d.ts +27 -0
  14. package/dist/cli/commands/lint.d.ts.map +1 -1
  15. package/dist/cli/handlers/build.d.ts.map +1 -1
  16. package/dist/cli/handlers/components.d.ts.map +1 -1
  17. package/dist/cli/handlers/lint.d.ts.map +1 -1
  18. package/dist/cli/handlers/run-client.d.ts +1 -1
  19. package/dist/cli/handlers/run-client.d.ts.map +1 -1
  20. package/dist/cli/handlers/run.d.ts.map +1 -1
  21. package/dist/cli/main.d.ts.map +1 -1
  22. package/dist/cli/plugins.d.ts +16 -0
  23. package/dist/cli/plugins.d.ts.map +1 -1
  24. package/dist/cli/registry.d.ts +10 -0
  25. package/dist/cli/registry.d.ts.map +1 -1
  26. package/dist/codegen/docs-sections.d.ts.map +1 -1
  27. package/dist/codegen/docs-types.d.ts +2 -0
  28. package/dist/codegen/docs-types.d.ts.map +1 -1
  29. package/dist/components/cli-support.d.ts +12 -5
  30. package/dist/components/cli-support.d.ts.map +1 -1
  31. package/dist/components/discover.d.ts +62 -7
  32. package/dist/components/discover.d.ts.map +1 -1
  33. package/dist/components/sandbox/driver.d.ts +12 -0
  34. package/dist/components/sandbox/driver.d.ts.map +1 -0
  35. package/dist/components/sandbox/run.d.ts +42 -0
  36. package/dist/components/sandbox/run.d.ts.map +1 -0
  37. package/dist/composite.d.ts +5 -0
  38. package/dist/composite.d.ts.map +1 -1
  39. package/dist/config.d.ts +71 -0
  40. package/dist/config.d.ts.map +1 -1
  41. package/dist/declarable.d.ts +16 -0
  42. package/dist/declarable.d.ts.map +1 -1
  43. package/dist/discovery/collect.d.ts.map +1 -1
  44. package/dist/discovery/entity-wire-codec.d.ts +166 -0
  45. package/dist/discovery/entity-wire-codec.d.ts.map +1 -0
  46. package/dist/discovery/entity-wire.d.ts +50 -0
  47. package/dist/discovery/entity-wire.d.ts.map +1 -0
  48. package/dist/discovery/fold-import.d.ts +239 -0
  49. package/dist/discovery/fold-import.d.ts.map +1 -0
  50. package/dist/discovery/index.d.ts +74 -1
  51. package/dist/discovery/index.d.ts.map +1 -1
  52. package/dist/discovery/sandbox/bundle.d.ts +18 -0
  53. package/dist/discovery/sandbox/bundle.d.ts.map +1 -0
  54. package/dist/discovery/sandbox/child-errors.d.ts +15 -0
  55. package/dist/discovery/sandbox/child-errors.d.ts.map +1 -0
  56. package/dist/discovery/sandbox/driver.d.ts +13 -0
  57. package/dist/discovery/sandbox/driver.d.ts.map +1 -0
  58. package/dist/discovery/sandbox/run.d.ts +69 -0
  59. package/dist/discovery/sandbox/run.d.ts.map +1 -0
  60. package/dist/errors.d.ts +9 -1
  61. package/dist/errors.d.ts.map +1 -1
  62. package/dist/fold/fold.d.ts +299 -0
  63. package/dist/fold/fold.d.ts.map +1 -0
  64. package/dist/fold/foldable-helpers.d.ts +121 -0
  65. package/dist/fold/foldable-helpers.d.ts.map +1 -0
  66. package/dist/fold/subset.d.ts +134 -0
  67. package/dist/fold/subset.d.ts.map +1 -0
  68. package/dist/index.d.ts +2 -0
  69. package/dist/index.d.ts.map +1 -1
  70. package/dist/lexicon-output.d.ts +7 -2
  71. package/dist/lexicon-output.d.ts.map +1 -1
  72. package/dist/lexicon-schema.d.ts +4 -2
  73. package/dist/lexicon-schema.d.ts.map +1 -1
  74. package/dist/lexicon.d.ts +131 -1
  75. package/dist/lexicon.d.ts.map +1 -1
  76. package/dist/lifecycle/release-ledger.d.ts +11 -0
  77. package/dist/lifecycle/release-ledger.d.ts.map +1 -1
  78. package/dist/lint/component-checks.d.ts +7 -1
  79. package/dist/lint/component-checks.d.ts.map +1 -1
  80. package/dist/lint/rules/evl001-non-literal-expression.d.ts.map +1 -1
  81. package/dist/lint/rules/evl003-dynamic-property-access.d.ts.map +1 -1
  82. package/dist/params.d.ts +60 -0
  83. package/dist/params.d.ts.map +1 -0
  84. package/dist/provenance.d.ts +21 -0
  85. package/dist/provenance.d.ts.map +1 -1
  86. package/dist/runtime.d.ts +10 -1
  87. package/dist/runtime.d.ts.map +1 -1
  88. package/package.json +2 -1
  89. package/src/build-params.test.ts +144 -0
  90. package/src/build-params.ts +207 -0
  91. package/src/build.test.ts +38 -0
  92. package/src/build.ts +144 -7
  93. package/src/cli/commands/build.test.ts +220 -2
  94. package/src/cli/commands/build.ts +111 -3
  95. package/src/cli/commands/check-lexicon-examples.test.ts +92 -0
  96. package/src/cli/commands/check-lexicon-examples.ts +103 -0
  97. package/src/cli/commands/check-lexicon-intrinsics.test.ts +179 -0
  98. package/src/cli/commands/check-lexicon-intrinsics.ts +348 -0
  99. package/src/cli/commands/check-lexicon.test.ts +34 -0
  100. package/src/cli/commands/check-lexicon.ts +137 -1
  101. package/src/cli/commands/lint.ts +31 -3
  102. package/src/cli/commands/onboard.ts +1 -1
  103. package/src/cli/component-security-boundary.test.ts +170 -0
  104. package/src/cli/handlers/build.ts +24 -3
  105. package/src/cli/handlers/components.ts +9 -2
  106. package/src/cli/handlers/dev.ts +1 -1
  107. package/src/cli/handlers/graph.ts +7 -5
  108. package/src/cli/handlers/lifecycle.ts +2 -2
  109. package/src/cli/handlers/lint.ts +2 -0
  110. package/src/cli/handlers/misc.ts +2 -2
  111. package/src/cli/handlers/run-client.ts +1 -1
  112. package/src/cli/handlers/run.ts +20 -5
  113. package/src/cli/main.test.ts +22 -0
  114. package/src/cli/main.ts +39 -0
  115. package/src/cli/plugins.ts +20 -4
  116. package/src/cli/registry.ts +10 -0
  117. package/src/cli/security-boundary.test.ts +135 -0
  118. package/src/codegen/docs-sections.test.ts +67 -0
  119. package/src/codegen/docs-sections.ts +7 -3
  120. package/src/codegen/docs-types.ts +2 -0
  121. package/src/components/cli-support.ts +22 -10
  122. package/src/components/discover.ts +127 -25
  123. package/src/components/sandbox/driver.ts +114 -0
  124. package/src/components/sandbox/run.test.ts +185 -0
  125. package/src/components/sandbox/run.ts +177 -0
  126. package/src/composite.test.ts +21 -0
  127. package/src/composite.ts +20 -1
  128. package/src/config.ts +81 -0
  129. package/src/declarable.ts +20 -0
  130. package/src/discovery/collect.ts +17 -3
  131. package/src/discovery/entity-wire-codec.ts +487 -0
  132. package/src/discovery/entity-wire.test.ts +240 -0
  133. package/src/discovery/entity-wire.ts +67 -0
  134. package/src/discovery/fold-import.test.ts +1598 -0
  135. package/src/discovery/fold-import.ts +1998 -0
  136. package/src/discovery/index.test.ts +191 -1
  137. package/src/discovery/index.ts +242 -1
  138. package/src/discovery/sandbox/bundle.ts +218 -0
  139. package/src/discovery/sandbox/child-errors.ts +65 -0
  140. package/src/discovery/sandbox/driver.ts +147 -0
  141. package/src/discovery/sandbox/run.test.ts +179 -0
  142. package/src/discovery/sandbox/run.ts +196 -0
  143. package/src/errors.ts +9 -1
  144. package/src/fold/fold.test.ts +812 -0
  145. package/src/fold/fold.ts +805 -0
  146. package/src/fold/foldable-helpers.ts +171 -0
  147. package/src/fold/subset-doc-parity.test.ts +210 -0
  148. package/src/fold/subset.test.ts +352 -0
  149. package/src/fold/subset.ts +383 -0
  150. package/src/index.ts +2 -0
  151. package/src/lexicon-output.ts +7 -2
  152. package/src/lexicon-schema.test.ts +57 -0
  153. package/src/lexicon-schema.ts +8 -1
  154. package/src/lexicon.ts +132 -1
  155. package/src/lifecycle/git.test.ts +10 -5
  156. package/src/lifecycle/release-ledger.test.ts +28 -0
  157. package/src/lifecycle/release-ledger.ts +11 -0
  158. package/src/lint/component-checks.ts +8 -1
  159. package/src/lint/rules/evl001-non-literal-expression.ts +26 -110
  160. package/src/lint/rules/evl003-dynamic-property-access.ts +11 -2
  161. package/src/params.test.ts +22 -0
  162. package/src/params.ts +66 -0
  163. package/src/provenance.ts +22 -0
  164. package/src/runtime.ts +11 -2
@@ -0,0 +1,352 @@
1
+ import { describe, test, expect } from "vitest";
2
+ import * as ts from "typescript";
3
+ import { fold, foldResource, collectConsts, FoldError } from "./fold";
4
+ import { findSubsetViolation } from "./subset";
5
+ import { evl001NonLiteralExpressionRule } from "../lint/rules/evl001-non-literal-expression";
6
+ import { evl003DynamicPropertyAccessRule } from "../lint/rules/evl003-dynamic-property-access";
7
+ import { evl004SpreadNonConstRule } from "../lint/rules/evl004-spread-non-const";
8
+ import type { LintContext } from "../lint/rule";
9
+ import type { IntrinsicDef } from "../lexicon";
10
+
11
+ /**
12
+ * subset.test.ts — the equivalence property test for #1024: for a corpus of
13
+ * supported AND unsupported expression shapes, `fold()` and the EVL rules
14
+ * (EVL001 general, EVL003 for dynamic element-access keys) must agree —
15
+ * "EVL-flags <=> fold-rejects" — citing the same rule id and the same
16
+ * source position for every rejection.
17
+ *
18
+ * Every case here is a single-file, self-contained snippet (no imports, no
19
+ * unresolved cross-file symbols) so it stays inside the part of the subset
20
+ * that genuinely IS shared. A handful of real, environment/value-dependent
21
+ * exceptions are NOT unifiable through shape alone — see subset.ts's module
22
+ * doc for why — and are verified separately below, as documented
23
+ * divergences, rather than folded into the main equivalence loop.
24
+ */
25
+
26
+ interface SubsetCase {
27
+ name: string;
28
+ /** The expression under test, plugged in as `new Thing({ x: <expr> })`. */
29
+ expr: string;
30
+ /** Extra top-level `const` declarations the expression may reference. */
31
+ preamble?: string;
32
+ intrinsics?: IntrinsicDef[];
33
+ }
34
+
35
+ const SUPPORTED_CASES: SubsetCase[] = [
36
+ { name: "string literal", expr: `"x"` },
37
+ { name: "numeric literal", expr: `42` },
38
+ { name: "boolean literal", expr: `true` },
39
+ { name: "null", expr: `null` },
40
+ { name: "undefined", expr: `undefined` },
41
+ { name: "template literal interpolating a const", preamble: `const name = "world";`, expr: "`hello-${name}`" },
42
+ { name: "nested object literal with shorthand", preamble: `const c = 2;`, expr: `{ a: 1, b: { c } }` },
43
+ { name: "object spread from a const object", preamble: `const base = { a: 1 };`, expr: `{ ...base, b: 2 }` },
44
+ { name: "array spread from a const array", preamble: `const arr = [2, 3];`, expr: `[1, ...arr, 4]` },
45
+ { name: "property access chain", preamble: `const config = { role: "arn" };`, expr: `config.role` },
46
+ { name: "element access with a literal key", preamble: `const config = { role: "arn" };`, expr: `config["role"]` },
47
+ { name: "logical-not unary", preamble: `const flag = true;`, expr: `!flag` },
48
+ { name: "numeric negation unary", preamble: `const n = 5;`, expr: `-n` },
49
+ { name: "whitelisted binary operator (+)", preamble: `const n = 5;`, expr: `n + 1` },
50
+ { name: "whitelisted binary operator (===)", preamble: `const n = 5;`, expr: `n === 5` },
51
+ { name: "conditional with both branches foldable", preamble: `const flag = true;`, expr: `flag ? "yes" : "no"` },
52
+ { name: "as cast", expr: `(1 + 1) as number` },
53
+ { name: "satisfies", expr: `"ok" satisfies string` },
54
+ { name: "parenthesized", expr: `(("nested"))` },
55
+ { name: "non-null assertion", preamble: `const n = 5;`, expr: `n!` },
56
+ {
57
+ name: "registered intrinsic tagged template with foldable interior",
58
+ preamble: `const name = "prefix";`,
59
+ expr: "Sub`${name}-data`",
60
+ intrinsics: [{ name: "Sub", isTag: true }],
61
+ },
62
+ {
63
+ // chant #1082 — a registered chant authoring helper is the one call shape
64
+ // that folds, so EVL001 must not flag it either.
65
+ name: "registered authoring helper call with foldable arguments",
66
+ preamble: `const stack = "web";`,
67
+ expr: `phase("Apply", [{ kind: "cfn-deploy", stack }])`,
68
+ },
69
+ {
70
+ name: "registered authoring helper nested inside another",
71
+ expr: `phase("Outer", [phase("Inner", []), gate("approve")])`,
72
+ },
73
+ ];
74
+
75
+ const UNSUPPORTED_CASES: SubsetCase[] = [
76
+ { name: "function call as a value", expr: `getName()` },
77
+ { name: "method call as a value", expr: `config.getName()` },
78
+ // chant #1082 — a registered helper NAME reached through a namespace is
79
+ // still a method call, and still rejected by both sides.
80
+ { name: "registered helper name reached as a method", expr: `helpers.phase("Apply", [])` },
81
+ // ...and a registered helper with an unfoldable argument is rejected on the
82
+ // argument, by both sides, at the argument's own position.
83
+ { name: "registered helper with an unfoldable argument", expr: `phase("Apply", [getName()])` },
84
+ { name: "computed/dynamic object-literal key", expr: `{ [dynKey]: 1 }` },
85
+ { name: "dynamic element-access key", expr: `config[dynKey]` },
86
+ { name: "non-whitelisted binary operator (%)", expr: `n % 2` },
87
+ { name: "non-whitelisted unary operator (~)", expr: `~n` },
88
+ { name: "unsupported object member (accessor)", expr: `{ get y() { return 1; } }` },
89
+ { name: "unsupported expression kind (arrow function)", expr: `() => 1` },
90
+ { name: "template literal with an unfoldable interpolation", expr: "`pre-${getName()}`" },
91
+ ];
92
+
93
+ interface RunResult {
94
+ foldError: FoldError | undefined;
95
+ evl001Diags: ReturnType<typeof evl001NonLiteralExpressionRule.check>;
96
+ evl003Diags: ReturnType<typeof evl003DynamicPropertyAccessRule.check>;
97
+ }
98
+
99
+ function run(c: SubsetCase): RunResult {
100
+ const source = `${c.preamble ?? ""}\nconst bad = new Thing({ x: ${c.expr} });`;
101
+ const sourceFile = ts.createSourceFile("t.ts", source, ts.ScriptTarget.Latest, /* setParentNodes */ true);
102
+ const consts = collectConsts(sourceFile);
103
+ const badInit = consts.get("bad");
104
+ if (!badInit || !ts.isNewExpression(badInit)) {
105
+ throw new Error(`fixture error: "bad" did not parse as a NewExpression in case "${c.name}"`);
106
+ }
107
+
108
+ let foldError: FoldError | undefined;
109
+ try {
110
+ foldResource(badInit, consts, c.intrinsics ?? []);
111
+ } catch (e) {
112
+ if (e instanceof FoldError) {
113
+ foldError = e;
114
+ } else {
115
+ throw e;
116
+ }
117
+ }
118
+
119
+ const context: LintContext = { sourceFile, entities: [], filePath: "t.ts", lexicon: undefined };
120
+ return {
121
+ foldError,
122
+ evl001Diags: evl001NonLiteralExpressionRule.check(context),
123
+ evl003Diags: evl003DynamicPropertyAccessRule.check(context),
124
+ };
125
+ }
126
+
127
+ describe("subset equivalence — supported cases: fold succeeds AND EVL is clean", () => {
128
+ for (const c of SUPPORTED_CASES) {
129
+ test(c.name, () => {
130
+ const { foldError, evl001Diags, evl003Diags } = run(c);
131
+ expect(foldError, `fold() unexpectedly rejected: ${foldError?.message}`).toBeUndefined();
132
+ expect(evl001Diags, "EVL001 unexpectedly flagged a fold-supported construct").toHaveLength(0);
133
+ expect(evl003Diags, "EVL003 unexpectedly flagged a fold-supported construct").toHaveLength(0);
134
+ });
135
+ }
136
+ });
137
+
138
+ describe("subset equivalence — unsupported cases: fold rejects AND EVL flags the same rule id + position", () => {
139
+ for (const c of UNSUPPORTED_CASES) {
140
+ test(c.name, () => {
141
+ const { foldError, evl001Diags, evl003Diags } = run(c);
142
+ expect(foldError, "fold() unexpectedly accepted an unsupported construct").toBeInstanceOf(FoldError);
143
+ const err = foldError as FoldError;
144
+
145
+ const matching = err.ruleId === "EVL003" ? evl003Diags : evl001Diags;
146
+ expect(
147
+ matching.length,
148
+ `expected ${err.ruleId} to flag the same construct fold() rejected ("${err.message}")`,
149
+ ).toBeGreaterThan(0);
150
+ expect(matching[0].ruleId).toBe(err.ruleId);
151
+ expect(matching[0].line, "EVL diagnostic line must match FoldError.line").toBe(err.line);
152
+ expect(matching[0].column, "EVL diagnostic column must match FoldError.column").toBe(err.column);
153
+ });
154
+ }
155
+ });
156
+
157
+ /**
158
+ * Documented, out-of-scope divergences (see subset.ts's module doc). These
159
+ * are NOT bugs introduced by #1024 — they're inherent to a syntax-only
160
+ * lint rule vs. an evaluator, and are asserted here so a future change that
161
+ * accidentally "fixes" (or silently regresses) one of them gets caught.
162
+ */
163
+ describe("documented divergences — NOT unified by design (see subset.ts module doc)", () => {
164
+ test("identifier resolution: fold rejects an unresolved bare identifier; EVL001 does not (shape-only)", () => {
165
+ const source = `const bad = new Thing({ x: missingVar });`;
166
+ const sourceFile = ts.createSourceFile("t.ts", source, ts.ScriptTarget.Latest, true);
167
+ const consts = collectConsts(sourceFile);
168
+ const badInit = consts.get("bad") as ts.NewExpression;
169
+
170
+ expect(() => foldResource(badInit, consts, [])).toThrow(FoldError);
171
+
172
+ const context: LintContext = { sourceFile, entities: [], filePath: "t.ts", lexicon: undefined };
173
+ expect(evl001NonLiteralExpressionRule.check(context)).toHaveLength(0);
174
+ });
175
+
176
+ test("intrinsic tag registration: fold rejects an unregistered tag; EVL001 does not (no lexicon manifest at lint time)", () => {
177
+ const source = "const bad = new Thing({ x: Unknown`plain` });";
178
+ const sourceFile = ts.createSourceFile("t.ts", source, ts.ScriptTarget.Latest, true);
179
+ const consts = collectConsts(sourceFile);
180
+ const badInit = consts.get("bad") as ts.NewExpression;
181
+
182
+ expect(() => foldResource(badInit, consts, [])).toThrow(FoldError);
183
+
184
+ const context: LintContext = { sourceFile, entities: [], filePath: "t.ts", lexicon: undefined };
185
+ expect(evl001NonLiteralExpressionRule.check(context)).toHaveLength(0);
186
+ });
187
+
188
+ test("spread-source runtime type: fold rejects spreading a const number; no EVL rule catches it (needs evaluation)", () => {
189
+ const source = `
190
+ const n = 5;
191
+ const bad = new Thing({ x: { ...n } });
192
+ `;
193
+ const sourceFile = ts.createSourceFile("t.ts", source, ts.ScriptTarget.Latest, true);
194
+ const consts = collectConsts(sourceFile);
195
+ const badInit = consts.get("bad") as ts.NewExpression;
196
+
197
+ expect(() => foldResource(badInit, consts, [])).toThrow(FoldError);
198
+
199
+ const context: LintContext = { sourceFile, entities: [], filePath: "t.ts", lexicon: undefined };
200
+ expect(evl001NonLiteralExpressionRule.check(context)).toHaveLength(0);
201
+ expect(evl004SpreadNonConstRule.check(context)).toHaveLength(0);
202
+ });
203
+
204
+ test("&&/||/?? short-circuit: fold folds only the taken side; EVL is flow-insensitive and flags the untaken side too", () => {
205
+ // fold(): the left side is falsy, so `sideEffect()` is never folded — succeeds.
206
+ const shortCircuitSrc = ts.createSourceFile(
207
+ "t.ts",
208
+ `const x = false && sideEffect();`,
209
+ ts.ScriptTarget.Latest,
210
+ true,
211
+ );
212
+ const shortCircuitConsts = collectConsts(shortCircuitSrc);
213
+ const foldedValue = fold(shortCircuitConsts.get("x") as ts.Expression, shortCircuitConsts);
214
+ expect(foldedValue).toBe(false);
215
+
216
+ // EVL001 has no evaluator — it requires every operand to be shape-valid,
217
+ // so it flags the untaken `sideEffect()` branch even though fold()
218
+ // never touches it.
219
+ const source = `const bad = new Thing({ x: false && sideEffect() });`;
220
+ const sourceFile = ts.createSourceFile("t.ts", source, ts.ScriptTarget.Latest, true);
221
+ const context: LintContext = { sourceFile, entities: [], filePath: "t.ts", lexicon: undefined };
222
+ expect(evl001NonLiteralExpressionRule.check(context).length).toBeGreaterThan(0);
223
+ });
224
+
225
+ test("tagged-template interior: fold rejects an unfoldable interpolation in a registered intrinsic tag; EVL is lenient (no registry, interiors opaque)", () => {
226
+ // `Sub`${getName()}`` — fold, with the intrinsic registry, folds the Sub
227
+ // tag's interior and rejects the getName() call. EVL has no registry and
228
+ // can't tell an intrinsic call (Ref, legit) from a plain one, so it treats
229
+ // tagged-template interiors as opaque and does not flag — otherwise it would
230
+ // false-flag `Sub`${Ref(env)}`` and break every intrinsic-using example.
231
+ const source = "const bad = new Thing({ x: Sub`${getName()}` });";
232
+ const sourceFile = ts.createSourceFile("t.ts", source, ts.ScriptTarget.Latest, true);
233
+ const consts = collectConsts(sourceFile);
234
+ const badInit = consts.get("bad") as ts.NewExpression;
235
+
236
+ expect(() => foldResource(badInit, consts, [{ name: "Sub", isTag: true }])).toThrow(FoldError);
237
+
238
+ const context: LintContext = { sourceFile, entities: [], filePath: "t.ts", lexicon: undefined };
239
+ expect(evl001NonLiteralExpressionRule.check(context)).toHaveLength(0);
240
+ });
241
+
242
+ test("authoring-helper shadowing: fold rejects a registered name bound to a local const; EVL001 does not (shape-only, no binding resolution)", () => {
243
+ // chant #1082 — `phase` is registered, but here it's the file's own local
244
+ // arrow function, so the local binding wins and fold() rejects. EVL has no
245
+ // binding resolver (subset.ts module doc, point 1) and stays permissive —
246
+ // the same direction as every other divergence here.
247
+ const source = `
248
+ const phase = (n) => ({ phase: n });
249
+ const bad = new Thing({ x: phase("Apply") });
250
+ `;
251
+ const sourceFile = ts.createSourceFile("t.ts", source, ts.ScriptTarget.Latest, true);
252
+ const consts = collectConsts(sourceFile);
253
+ const badInit = consts.get("bad") as ts.NewExpression;
254
+
255
+ expect(() => foldResource(badInit, consts, [])).toThrow(FoldError);
256
+
257
+ const context: LintContext = { sourceFile, entities: [], filePath: "t.ts", lexicon: undefined };
258
+ expect(evl001NonLiteralExpressionRule.check(context)).toHaveLength(0);
259
+ });
260
+
261
+ test("nested resource construction: fold rejects a nested `new Type()` used as a value (falls back to run); EVL001 allows it statically", () => {
262
+ // A top-level `new Type()` folds (fold-import constructs a real Declarable),
263
+ // but a NESTED one as a property value can only fold to a {__resource,props}
264
+ // envelope that is never constructed, so it would serialize wrong (real
265
+ // fold-vs-run drift, caught by the #1025 differential on gitlab). fold()
266
+ // therefore rejects it, falling the file back to run; EVL allows it (a valid
267
+ // TS constructor call), so this is an inherent, out-of-scope divergence.
268
+ const source = `const bad = new Thing({ x: new Inner({ y: 1 }) });`;
269
+ const sourceFile = ts.createSourceFile("t.ts", source, ts.ScriptTarget.Latest, true);
270
+ const consts = collectConsts(sourceFile);
271
+ const badInit = consts.get("bad") as ts.NewExpression;
272
+
273
+ expect(() => foldResource(badInit, consts, [])).toThrow(FoldError);
274
+
275
+ const context: LintContext = { sourceFile, entities: [], filePath: "t.ts", lexicon: undefined };
276
+ expect(evl001NonLiteralExpressionRule.check(context)).toHaveLength(0);
277
+ });
278
+ });
279
+
280
+ /**
281
+ * chant #1044 — the shared predicate's optional intrinsic registry.
282
+ *
283
+ * `findSubsetViolation` answers "is this shape foldable?" for two kinds of
284
+ * caller: `fold()`'s own EVL twin, which has no registry, and a tool that
285
+ * does (a control plane deciding whether a repository needs a sandboxed
286
+ * child process). The parameter is what lets the second kind get fold()'s
287
+ * real answer without running fold, while the first keeps the answer it
288
+ * always had.
289
+ */
290
+ describe("findSubsetViolation — optional intrinsic registry (#1044)", () => {
291
+ const REF: IntrinsicDef[] = [{ name: "Ref", isTag: false, foldsAsCall: true }];
292
+
293
+ /** The `x` initializer of `const bad = new Thing({ x: <expr> });`. */
294
+ function propValue(expr: string, preamble = ""): ts.Expression {
295
+ const sourceFile = ts.createSourceFile(
296
+ "t.ts",
297
+ `${preamble}\nconst bad = new Thing({ x: ${expr} });`,
298
+ ts.ScriptTarget.Latest,
299
+ true,
300
+ );
301
+ const consts = collectConsts(sourceFile);
302
+ const init = consts.get("bad") as ts.NewExpression;
303
+ const props = init.arguments![0] as ts.ObjectLiteralExpression;
304
+ return (props.properties[0] as ts.PropertyAssignment).initializer;
305
+ }
306
+
307
+ test("with the registry supplied, an opted-in intrinsic call is not a violation — the same answer fold() gives", () => {
308
+ expect(findSubsetViolation(propValue(`Ref(env)`), REF)).toBeUndefined();
309
+ const consts = collectConsts(
310
+ ts.createSourceFile("t.ts", `const env = "p"; const x = Ref(env);`, ts.ScriptTarget.Latest, true),
311
+ );
312
+ expect(fold(consts.get("x") as ts.Expression, consts, REF)).toEqual({ __intrinsic: "Ref", args: ["p"] });
313
+ });
314
+
315
+ test("with NO registry, every call stays a violation — the pre-#1044 answer, and the safe one", () => {
316
+ const v = findSubsetViolation(propValue(`Ref(env)`));
317
+ expect(v?.ruleId).toBe("EVL001");
318
+ expect(v?.message).toContain("function call as a value is not foldable: Ref(...)");
319
+ });
320
+
321
+ test("the registry doesn't widen anything else: a name in it without the opt-in, a method call, and .map all stay violations", () => {
322
+ const notOptedIn: IntrinsicDef[] = [{ name: "Reference", isTag: false }];
323
+ expect(findSubsetViolation(propValue(`Reference("db")`), notOptedIn)).toBeDefined();
324
+ expect(findSubsetViolation(propValue(`aws.Ref("db")`), REF)).toBeDefined();
325
+ expect(findSubsetViolation(propValue(`cidrs.map((c) => c)`, `const cidrs = [];`), REF)).toBeDefined();
326
+ expect(findSubsetViolation(propValue(`makeName("a")`), REF)).toBeDefined();
327
+ });
328
+
329
+ test("arguments are still classified on their own terms, at their own position", () => {
330
+ const v = findSubsetViolation(propValue(`Ref(getName())`), REF);
331
+ expect(v?.message).toContain("getName(...)");
332
+ });
333
+
334
+ test("registry-less EVL is now STRICTER than fold on an opted-in call — a known divergence, not a hole", () => {
335
+ // chant #1044 — EVL001 has no registry (see subset.ts module doc, point
336
+ // 2c), so it still flags `Ref(...)` in a resource's props exactly as it
337
+ // did before this change, while fold() — which is always given one —
338
+ // folds it. Recorded here so the divergence is a tracked property with a
339
+ // test rather than a surprise; closing it means handing the lint engine
340
+ // the active lexicons' intrinsics, which is a change to lint's own
341
+ // surface and deliberately not part of #1044.
342
+ const source = `const bad = new Thing({ x: Ref(env) });`;
343
+ const sourceFile = ts.createSourceFile("t.ts", source, ts.ScriptTarget.Latest, true);
344
+ const consts = collectConsts(sourceFile);
345
+ const badInit = consts.get("bad") as ts.NewExpression;
346
+
347
+ expect(() => foldResource(badInit, consts, REF)).not.toThrow();
348
+
349
+ const context: LintContext = { sourceFile, entities: [], filePath: "t.ts", lexicon: undefined };
350
+ expect(evl001NonLiteralExpressionRule.check(context).length).toBeGreaterThan(0);
351
+ });
352
+ });