@intentius/chant 0.19.0 → 0.20.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 (156) 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 +54 -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/components/cli-support.d.ts +12 -5
  28. package/dist/components/cli-support.d.ts.map +1 -1
  29. package/dist/components/discover.d.ts +62 -7
  30. package/dist/components/discover.d.ts.map +1 -1
  31. package/dist/components/sandbox/driver.d.ts +12 -0
  32. package/dist/components/sandbox/driver.d.ts.map +1 -0
  33. package/dist/components/sandbox/run.d.ts +42 -0
  34. package/dist/components/sandbox/run.d.ts.map +1 -0
  35. package/dist/composite.d.ts +5 -0
  36. package/dist/composite.d.ts.map +1 -1
  37. package/dist/config.d.ts +71 -0
  38. package/dist/config.d.ts.map +1 -1
  39. package/dist/discovery/collect.d.ts.map +1 -1
  40. package/dist/discovery/entity-wire-codec.d.ts +166 -0
  41. package/dist/discovery/entity-wire-codec.d.ts.map +1 -0
  42. package/dist/discovery/entity-wire.d.ts +50 -0
  43. package/dist/discovery/entity-wire.d.ts.map +1 -0
  44. package/dist/discovery/fold-import.d.ts +210 -0
  45. package/dist/discovery/fold-import.d.ts.map +1 -0
  46. package/dist/discovery/index.d.ts +74 -1
  47. package/dist/discovery/index.d.ts.map +1 -1
  48. package/dist/discovery/sandbox/bundle.d.ts +18 -0
  49. package/dist/discovery/sandbox/bundle.d.ts.map +1 -0
  50. package/dist/discovery/sandbox/child-errors.d.ts +15 -0
  51. package/dist/discovery/sandbox/child-errors.d.ts.map +1 -0
  52. package/dist/discovery/sandbox/driver.d.ts +13 -0
  53. package/dist/discovery/sandbox/driver.d.ts.map +1 -0
  54. package/dist/discovery/sandbox/run.d.ts +69 -0
  55. package/dist/discovery/sandbox/run.d.ts.map +1 -0
  56. package/dist/errors.d.ts +9 -1
  57. package/dist/errors.d.ts.map +1 -1
  58. package/dist/fold/fold.d.ts +226 -0
  59. package/dist/fold/fold.d.ts.map +1 -0
  60. package/dist/fold/subset.d.ts +102 -0
  61. package/dist/fold/subset.d.ts.map +1 -0
  62. package/dist/index.d.ts +2 -0
  63. package/dist/index.d.ts.map +1 -1
  64. package/dist/lexicon-output.d.ts +7 -2
  65. package/dist/lexicon-output.d.ts.map +1 -1
  66. package/dist/lexicon-schema.d.ts +2 -2
  67. package/dist/lexicon-schema.d.ts.map +1 -1
  68. package/dist/lexicon.d.ts +81 -1
  69. package/dist/lexicon.d.ts.map +1 -1
  70. package/dist/lifecycle/release-ledger.d.ts +11 -0
  71. package/dist/lifecycle/release-ledger.d.ts.map +1 -1
  72. package/dist/lint/component-checks.d.ts +7 -1
  73. package/dist/lint/component-checks.d.ts.map +1 -1
  74. package/dist/lint/rules/evl001-non-literal-expression.d.ts.map +1 -1
  75. package/dist/lint/rules/evl003-dynamic-property-access.d.ts.map +1 -1
  76. package/dist/params.d.ts +60 -0
  77. package/dist/params.d.ts.map +1 -0
  78. package/dist/provenance.d.ts +21 -0
  79. package/dist/provenance.d.ts.map +1 -1
  80. package/dist/terraform/aws-resources.d.ts.map +1 -1
  81. package/package.json +2 -1
  82. package/src/build-params.test.ts +144 -0
  83. package/src/build-params.ts +207 -0
  84. package/src/build.test.ts +38 -0
  85. package/src/build.ts +144 -7
  86. package/src/cli/commands/build.test.ts +220 -2
  87. package/src/cli/commands/build.ts +111 -3
  88. package/src/cli/commands/check-lexicon-examples.test.ts +92 -0
  89. package/src/cli/commands/check-lexicon-examples.ts +103 -0
  90. package/src/cli/commands/check-lexicon-intrinsics.test.ts +145 -0
  91. package/src/cli/commands/check-lexicon-intrinsics.ts +312 -0
  92. package/src/cli/commands/check-lexicon.test.ts +34 -0
  93. package/src/cli/commands/check-lexicon.ts +119 -1
  94. package/src/cli/commands/lint.ts +31 -3
  95. package/src/cli/commands/onboard.ts +1 -1
  96. package/src/cli/component-security-boundary.test.ts +170 -0
  97. package/src/cli/handlers/build.ts +24 -3
  98. package/src/cli/handlers/components.ts +9 -2
  99. package/src/cli/handlers/dev.ts +1 -1
  100. package/src/cli/handlers/graph.ts +7 -5
  101. package/src/cli/handlers/lifecycle.ts +2 -2
  102. package/src/cli/handlers/lint.ts +2 -0
  103. package/src/cli/handlers/misc.ts +2 -2
  104. package/src/cli/handlers/run-client.ts +1 -1
  105. package/src/cli/handlers/run.ts +20 -5
  106. package/src/cli/main.test.ts +22 -0
  107. package/src/cli/main.ts +39 -0
  108. package/src/cli/plugins.ts +20 -4
  109. package/src/cli/registry.ts +10 -0
  110. package/src/cli/security-boundary.test.ts +135 -0
  111. package/src/codegen/docs-sections.test.ts +61 -0
  112. package/src/codegen/docs-sections.ts +7 -3
  113. package/src/components/cli-support.ts +22 -10
  114. package/src/components/discover.ts +127 -25
  115. package/src/components/sandbox/driver.ts +114 -0
  116. package/src/components/sandbox/run.test.ts +185 -0
  117. package/src/components/sandbox/run.ts +177 -0
  118. package/src/composite.test.ts +21 -0
  119. package/src/composite.ts +20 -1
  120. package/src/config.ts +81 -0
  121. package/src/discovery/collect.ts +17 -3
  122. package/src/discovery/entity-wire-codec.ts +485 -0
  123. package/src/discovery/entity-wire.test.ts +240 -0
  124. package/src/discovery/entity-wire.ts +67 -0
  125. package/src/discovery/fold-import.test.ts +1026 -0
  126. package/src/discovery/fold-import.ts +1805 -0
  127. package/src/discovery/index.test.ts +191 -1
  128. package/src/discovery/index.ts +233 -1
  129. package/src/discovery/sandbox/bundle.ts +218 -0
  130. package/src/discovery/sandbox/child-errors.ts +65 -0
  131. package/src/discovery/sandbox/driver.ts +147 -0
  132. package/src/discovery/sandbox/run.test.ts +179 -0
  133. package/src/discovery/sandbox/run.ts +196 -0
  134. package/src/errors.ts +9 -1
  135. package/src/fold/fold.test.ts +535 -0
  136. package/src/fold/fold.ts +648 -0
  137. package/src/fold/subset-doc-parity.test.ts +183 -0
  138. package/src/fold/subset.test.ts +241 -0
  139. package/src/fold/subset.ts +302 -0
  140. package/src/index.ts +2 -0
  141. package/src/lexicon-output.ts +7 -2
  142. package/src/lexicon-schema.test.ts +14 -0
  143. package/src/lexicon-schema.ts +3 -1
  144. package/src/lexicon.ts +82 -1
  145. package/src/lifecycle/git.test.ts +10 -5
  146. package/src/lifecycle/release-ledger.test.ts +28 -0
  147. package/src/lifecycle/release-ledger.ts +11 -0
  148. package/src/lint/component-checks.ts +8 -1
  149. package/src/lint/rules/evl001-non-literal-expression.ts +26 -110
  150. package/src/lint/rules/evl003-dynamic-property-access.ts +11 -2
  151. package/src/params.test.ts +22 -0
  152. package/src/params.ts +66 -0
  153. package/src/provenance.ts +22 -0
  154. package/src/terraform/adopt-state.test.ts +1 -1
  155. package/src/terraform/aws-resources.test.ts +11 -4
  156. package/src/terraform/aws-resources.ts +60 -0
@@ -0,0 +1,183 @@
1
+ import { describe, test, expect } from "vitest";
2
+ import * as ts from "typescript";
3
+ import { readFileSync } from "fs";
4
+ import { fileURLToPath } from "url";
5
+ import { join } from "path";
6
+ import { collectConsts } from "./fold";
7
+ import { findSubsetViolation } from "./subset";
8
+
9
+ /**
10
+ * subset-doc-parity.test.ts — chant #1062 (epic #1019).
11
+ *
12
+ * `docs/.../concepts/typescript-as-data.mdx`'s "Supported Patterns" and
13
+ * "Unsupported Patterns" sections are prose over `findSubsetViolation`
14
+ * (./subset.ts), the real, code-defined classifier. #1062 exists because
15
+ * that prose already drifted from the code once (see subset.ts's own
16
+ * module doc, and #1061's fix) and nothing stopped it.
17
+ *
18
+ * Full generation was considered and rejected: `findSubsetViolation` is a
19
+ * recursive-descent classifier over `ts.Node` kinds, not a flat table — its
20
+ * value is precisely the nuance a table would lose (flow-insensitive
21
+ * short-circuit handling, opaque tagged-template interiors, which EVL rule
22
+ * id a rejection maps to, …). Mechanically reverse-engineering that nuance
23
+ * from the function body would mean either emitting a bare list of
24
+ * `ts.SyntaxKind` names (useless to a reader) or writing a second, brittle
25
+ * introspector that breaks on any harmless refactor of subset.ts — a worse
26
+ * failure mode than the one this issue exists to fix.
27
+ *
28
+ * Instead: this test pulls each pattern's example STRAIGHT OUT of the doc's
29
+ * own fenced code block (by heading, via {@link extractFencedBlock}) — no
30
+ * second copy of the snippet lives in this file — and runs it through
31
+ * `findSubsetViolation` for real. A heading that's renamed or removed fails
32
+ * loudly (`heading not found`); a snippet whose real verdict no longer
33
+ * matches what the doc claims for it fails just as loudly. That is the
34
+ * concrete failure mode from the day this issue was filed, closed for good.
35
+ *
36
+ * Scope: only patterns `findSubsetViolation` itself decides (EVL001/EVL003 —
37
+ * see subset.ts's module doc for why those are the two it owns). Left out,
38
+ * deliberately:
39
+ * - "Control flow around resources" (EVL002) and the `let`/`var`, `class`,
40
+ * `require()`, top-level `await`, decorator bullets under "Other
41
+ * unsupported patterns" — these are rejected at `foldModule`'s top-level
42
+ * statement scan, before any expression ever reaches
43
+ * `findSubsetViolation`. Not this module's concern, and not real,
44
+ * parseable TypeScript in the doc's own illustrative snippet (a bare
45
+ * `export` inside an `if` block isn't valid syntax to begin with).
46
+ * - "Import and re-export" / "Cross-file resource references" / "Typed
47
+ * property-kind constructors" are exercised below, but only prove the
48
+ * trivial case: `findSubsetViolation` treats every identifier as
49
+ * shape-valid regardless of whether it resolves locally, across a
50
+ * module boundary, or not at all (subset.ts's module doc, point 1) — a
51
+ * documented, intentional asymmetry, not something this guard can
52
+ * usefully narrow further without a binding resolver of its own.
53
+ */
54
+
55
+ const repoRoot = fileURLToPath(new URL("../../../../", import.meta.url));
56
+ const docPath = join(repoRoot, "docs", "src", "content", "docs", "concepts", "typescript-as-data.mdx");
57
+ const doc = readFileSync(docPath, "utf-8");
58
+
59
+ /** Pull the first ```typescript fenced block following `### {heading}`. */
60
+ function extractFencedBlock(heading: string): string {
61
+ const headingMarker = `### ${heading}`;
62
+ const headingIdx = doc.indexOf(headingMarker);
63
+ if (headingIdx === -1) {
64
+ throw new Error(
65
+ `subset-doc-parity: heading "${headingMarker}" not found in ${docPath} — was it renamed or removed? Update this test to match.`,
66
+ );
67
+ }
68
+ const fenceStart = doc.indexOf("```typescript", headingIdx);
69
+ if (fenceStart === -1) {
70
+ throw new Error(`subset-doc-parity: no \`\`\`typescript block found after "${headingMarker}"`);
71
+ }
72
+ const codeStart = doc.indexOf("\n", fenceStart) + 1;
73
+ const fenceEnd = doc.indexOf("```", codeStart);
74
+ if (fenceEnd === -1) {
75
+ throw new Error(`subset-doc-parity: unterminated code fence after "${headingMarker}"`);
76
+ }
77
+ return doc.slice(codeStart, fenceEnd);
78
+ }
79
+
80
+ /** Parse `source` and return its top-level `const` bindings, keyed by name. */
81
+ function parseConsts(source: string): Map<string, ts.Expression> {
82
+ const sourceFile = ts.createSourceFile("doc-example.ts", source, ts.ScriptTarget.Latest, /* setParentNodes */ true);
83
+ return collectConsts(sourceFile);
84
+ }
85
+
86
+ /** The object-literal argument of `const <name> = new Type({...})`. */
87
+ function resourceArg(consts: Map<string, ts.Expression>, name: string): ts.Expression {
88
+ const init = consts.get(name);
89
+ if (!init || !ts.isNewExpression(init) || !init.arguments?.[0]) {
90
+ throw new Error(`subset-doc-parity: "${name}" did not parse as a resource declaration with an object-literal arg`);
91
+ }
92
+ return init.arguments[0];
93
+ }
94
+
95
+ describe("subset-doc-parity — supported patterns in typescript-as-data.mdx classify as fold-clean", () => {
96
+ test("Resource declarations", () => {
97
+ const consts = parseConsts(extractFencedBlock("Resource declarations"));
98
+ expect(findSubsetViolation(resourceArg(consts, "store"))).toBeUndefined();
99
+ });
100
+
101
+ test("Literal values", () => {
102
+ const consts = parseConsts(extractFencedBlock("Literal values"));
103
+ expect(findSubsetViolation(resourceArg(consts, "service"))).toBeUndefined();
104
+ });
105
+
106
+ test("Const variable references", () => {
107
+ const consts = parseConsts(extractFencedBlock("Const variable references"));
108
+ expect(findSubsetViolation(resourceArg(consts, "store"))).toBeUndefined();
109
+ });
110
+
111
+ test("Spread from const sources", () => {
112
+ const consts = parseConsts(extractFencedBlock("Spread from const sources"));
113
+ expect(findSubsetViolation(resourceArg(consts, "service"))).toBeUndefined();
114
+ });
115
+
116
+ test("Import and re-export", () => {
117
+ // Trivial by construction — see this file's module doc.
118
+ const consts = parseConsts(extractFencedBlock("Import and re-export"));
119
+ expect(findSubsetViolation(resourceArg(consts, "store"))).toBeUndefined();
120
+ });
121
+
122
+ test("Cross-file resource references", () => {
123
+ // Trivial by construction — see this file's module doc.
124
+ const consts = parseConsts(extractFencedBlock("Cross-file resource references"));
125
+ expect(findSubsetViolation(resourceArg(consts, "service"))).toBeUndefined();
126
+ });
127
+
128
+ test("Intrinsic tagged templates", () => {
129
+ // subset.ts treats any tagged template's TAG as shape-valid regardless
130
+ // of lexicon registration (that check is fold()'s job, not subset.ts's —
131
+ // see subset.ts's module doc, point 2) — only the interpolated values
132
+ // are classified, and this example's interpolation is a plain property
133
+ // access chain.
134
+ const consts = parseConsts(extractFencedBlock("Intrinsic tagged templates"));
135
+ expect(findSubsetViolation(resourceArg(consts, "store"))).toBeUndefined();
136
+ });
137
+
138
+ test("Typed property-kind constructors", () => {
139
+ const consts = parseConsts(extractFencedBlock("Typed property-kind constructors"));
140
+ expect(findSubsetViolation(resourceArg(consts, "config"))).toBeUndefined();
141
+ expect(findSubsetViolation(resourceArg(consts, "access"))).toBeUndefined();
142
+ });
143
+
144
+ test("Nullish coalescing for defaults", () => {
145
+ // Not a standalone statement in the doc (a single object-literal
146
+ // property, deliberately shown as a fragment) — wrapped in an object
147
+ // literal to parse, which also happens to be exactly the shape
148
+ // `findSubsetViolation` classifies a resource prop through.
149
+ const fragment = extractFencedBlock("Nullish coalescing for defaults").trim();
150
+ const consts = parseConsts(`const _wrapped = { ${fragment} };`);
151
+ const wrapped = consts.get("_wrapped");
152
+ if (!wrapped) throw new Error("subset-doc-parity: nullish-coalescing fragment failed to parse");
153
+ expect(findSubsetViolation(wrapped)).toBeUndefined();
154
+ });
155
+ });
156
+
157
+ describe("subset-doc-parity — unsupported patterns in typescript-as-data.mdx classify as rejected", () => {
158
+ test("Function calls as values", () => {
159
+ const consts = parseConsts(extractFencedBlock("Function calls as values"));
160
+ expect(findSubsetViolation(resourceArg(consts, "store"))).toBeDefined();
161
+ });
162
+
163
+ test("Dynamic property access", () => {
164
+ // A bare `const`, not a resource declaration — check the initializer
165
+ // directly rather than through `resourceArg`.
166
+ const consts = parseConsts(extractFencedBlock("Dynamic property access"));
167
+ const init = consts.get("name");
168
+ if (!init) throw new Error(`subset-doc-parity: "name" did not parse as a const declaration`);
169
+ expect(findSubsetViolation(init)).toBeDefined();
170
+ });
171
+
172
+ test("Spread from dynamic sources", () => {
173
+ // Documented as EVL004 (a stricter, value-level narrowing rule subset.ts
174
+ // doesn't model — see subset.ts's module doc, item 3) but this
175
+ // particular example — spreading a CALL's result — is also rejected by
176
+ // subset.ts's shape classifier on its own terms: a call expression
177
+ // nested inside a spread is still a call expression. Asserting only
178
+ // "rejected", not a specific rule id, since attributing a rule id here
179
+ // is EVL004's job, not subset.ts's.
180
+ const consts = parseConsts(extractFencedBlock("Spread from dynamic sources"));
181
+ expect(findSubsetViolation(resourceArg(consts, "store"))).toBeDefined();
182
+ });
183
+ });
@@ -0,0 +1,241 @@
1
+ import { describe, test, expect } from "vitest";
2
+ import * as ts from "typescript";
3
+ import { fold, foldResource, collectConsts, FoldError } from "./fold";
4
+ import { evl001NonLiteralExpressionRule } from "../lint/rules/evl001-non-literal-expression";
5
+ import { evl003DynamicPropertyAccessRule } from "../lint/rules/evl003-dynamic-property-access";
6
+ import { evl004SpreadNonConstRule } from "../lint/rules/evl004-spread-non-const";
7
+ import type { LintContext } from "../lint/rule";
8
+ import type { IntrinsicDef } from "../lexicon";
9
+
10
+ /**
11
+ * subset.test.ts — the equivalence property test for #1024: for a corpus of
12
+ * supported AND unsupported expression shapes, `fold()` and the EVL rules
13
+ * (EVL001 general, EVL003 for dynamic element-access keys) must agree —
14
+ * "EVL-flags <=> fold-rejects" — citing the same rule id and the same
15
+ * source position for every rejection.
16
+ *
17
+ * Every case here is a single-file, self-contained snippet (no imports, no
18
+ * unresolved cross-file symbols) so it stays inside the part of the subset
19
+ * that genuinely IS shared. A handful of real, environment/value-dependent
20
+ * exceptions are NOT unifiable through shape alone — see subset.ts's module
21
+ * doc for why — and are verified separately below, as documented
22
+ * divergences, rather than folded into the main equivalence loop.
23
+ */
24
+
25
+ interface SubsetCase {
26
+ name: string;
27
+ /** The expression under test, plugged in as `new Thing({ x: <expr> })`. */
28
+ expr: string;
29
+ /** Extra top-level `const` declarations the expression may reference. */
30
+ preamble?: string;
31
+ intrinsics?: IntrinsicDef[];
32
+ }
33
+
34
+ const SUPPORTED_CASES: SubsetCase[] = [
35
+ { name: "string literal", expr: `"x"` },
36
+ { name: "numeric literal", expr: `42` },
37
+ { name: "boolean literal", expr: `true` },
38
+ { name: "null", expr: `null` },
39
+ { name: "undefined", expr: `undefined` },
40
+ { name: "template literal interpolating a const", preamble: `const name = "world";`, expr: "`hello-${name}`" },
41
+ { name: "nested object literal with shorthand", preamble: `const c = 2;`, expr: `{ a: 1, b: { c } }` },
42
+ { name: "object spread from a const object", preamble: `const base = { a: 1 };`, expr: `{ ...base, b: 2 }` },
43
+ { name: "array spread from a const array", preamble: `const arr = [2, 3];`, expr: `[1, ...arr, 4]` },
44
+ { name: "property access chain", preamble: `const config = { role: "arn" };`, expr: `config.role` },
45
+ { name: "element access with a literal key", preamble: `const config = { role: "arn" };`, expr: `config["role"]` },
46
+ { name: "logical-not unary", preamble: `const flag = true;`, expr: `!flag` },
47
+ { name: "numeric negation unary", preamble: `const n = 5;`, expr: `-n` },
48
+ { name: "whitelisted binary operator (+)", preamble: `const n = 5;`, expr: `n + 1` },
49
+ { name: "whitelisted binary operator (===)", preamble: `const n = 5;`, expr: `n === 5` },
50
+ { name: "conditional with both branches foldable", preamble: `const flag = true;`, expr: `flag ? "yes" : "no"` },
51
+ { name: "as cast", expr: `(1 + 1) as number` },
52
+ { name: "satisfies", expr: `"ok" satisfies string` },
53
+ { name: "parenthesized", expr: `(("nested"))` },
54
+ { name: "non-null assertion", preamble: `const n = 5;`, expr: `n!` },
55
+ {
56
+ name: "registered intrinsic tagged template with foldable interior",
57
+ preamble: `const name = "prefix";`,
58
+ expr: "Sub`${name}-data`",
59
+ intrinsics: [{ name: "Sub", isTag: true }],
60
+ },
61
+ ];
62
+
63
+ const UNSUPPORTED_CASES: SubsetCase[] = [
64
+ { name: "function call as a value", expr: `getName()` },
65
+ { name: "method call as a value", expr: `config.getName()` },
66
+ { name: "computed/dynamic object-literal key", expr: `{ [dynKey]: 1 }` },
67
+ { name: "dynamic element-access key", expr: `config[dynKey]` },
68
+ { name: "non-whitelisted binary operator (%)", expr: `n % 2` },
69
+ { name: "non-whitelisted unary operator (~)", expr: `~n` },
70
+ { name: "unsupported object member (accessor)", expr: `{ get y() { return 1; } }` },
71
+ { name: "unsupported expression kind (arrow function)", expr: `() => 1` },
72
+ { name: "template literal with an unfoldable interpolation", expr: "`pre-${getName()}`" },
73
+ ];
74
+
75
+ interface RunResult {
76
+ foldError: FoldError | undefined;
77
+ evl001Diags: ReturnType<typeof evl001NonLiteralExpressionRule.check>;
78
+ evl003Diags: ReturnType<typeof evl003DynamicPropertyAccessRule.check>;
79
+ }
80
+
81
+ function run(c: SubsetCase): RunResult {
82
+ const source = `${c.preamble ?? ""}\nconst bad = new Thing({ x: ${c.expr} });`;
83
+ const sourceFile = ts.createSourceFile("t.ts", source, ts.ScriptTarget.Latest, /* setParentNodes */ true);
84
+ const consts = collectConsts(sourceFile);
85
+ const badInit = consts.get("bad");
86
+ if (!badInit || !ts.isNewExpression(badInit)) {
87
+ throw new Error(`fixture error: "bad" did not parse as a NewExpression in case "${c.name}"`);
88
+ }
89
+
90
+ let foldError: FoldError | undefined;
91
+ try {
92
+ foldResource(badInit, consts, c.intrinsics ?? []);
93
+ } catch (e) {
94
+ if (e instanceof FoldError) {
95
+ foldError = e;
96
+ } else {
97
+ throw e;
98
+ }
99
+ }
100
+
101
+ const context: LintContext = { sourceFile, entities: [], filePath: "t.ts", lexicon: undefined };
102
+ return {
103
+ foldError,
104
+ evl001Diags: evl001NonLiteralExpressionRule.check(context),
105
+ evl003Diags: evl003DynamicPropertyAccessRule.check(context),
106
+ };
107
+ }
108
+
109
+ describe("subset equivalence — supported cases: fold succeeds AND EVL is clean", () => {
110
+ for (const c of SUPPORTED_CASES) {
111
+ test(c.name, () => {
112
+ const { foldError, evl001Diags, evl003Diags } = run(c);
113
+ expect(foldError, `fold() unexpectedly rejected: ${foldError?.message}`).toBeUndefined();
114
+ expect(evl001Diags, "EVL001 unexpectedly flagged a fold-supported construct").toHaveLength(0);
115
+ expect(evl003Diags, "EVL003 unexpectedly flagged a fold-supported construct").toHaveLength(0);
116
+ });
117
+ }
118
+ });
119
+
120
+ describe("subset equivalence — unsupported cases: fold rejects AND EVL flags the same rule id + position", () => {
121
+ for (const c of UNSUPPORTED_CASES) {
122
+ test(c.name, () => {
123
+ const { foldError, evl001Diags, evl003Diags } = run(c);
124
+ expect(foldError, "fold() unexpectedly accepted an unsupported construct").toBeInstanceOf(FoldError);
125
+ const err = foldError as FoldError;
126
+
127
+ const matching = err.ruleId === "EVL003" ? evl003Diags : evl001Diags;
128
+ expect(
129
+ matching.length,
130
+ `expected ${err.ruleId} to flag the same construct fold() rejected ("${err.message}")`,
131
+ ).toBeGreaterThan(0);
132
+ expect(matching[0].ruleId).toBe(err.ruleId);
133
+ expect(matching[0].line, "EVL diagnostic line must match FoldError.line").toBe(err.line);
134
+ expect(matching[0].column, "EVL diagnostic column must match FoldError.column").toBe(err.column);
135
+ });
136
+ }
137
+ });
138
+
139
+ /**
140
+ * Documented, out-of-scope divergences (see subset.ts's module doc). These
141
+ * are NOT bugs introduced by #1024 — they're inherent to a syntax-only
142
+ * lint rule vs. an evaluator, and are asserted here so a future change that
143
+ * accidentally "fixes" (or silently regresses) one of them gets caught.
144
+ */
145
+ describe("documented divergences — NOT unified by design (see subset.ts module doc)", () => {
146
+ test("identifier resolution: fold rejects an unresolved bare identifier; EVL001 does not (shape-only)", () => {
147
+ const source = `const bad = new Thing({ x: missingVar });`;
148
+ const sourceFile = ts.createSourceFile("t.ts", source, ts.ScriptTarget.Latest, true);
149
+ const consts = collectConsts(sourceFile);
150
+ const badInit = consts.get("bad") as ts.NewExpression;
151
+
152
+ expect(() => foldResource(badInit, consts, [])).toThrow(FoldError);
153
+
154
+ const context: LintContext = { sourceFile, entities: [], filePath: "t.ts", lexicon: undefined };
155
+ expect(evl001NonLiteralExpressionRule.check(context)).toHaveLength(0);
156
+ });
157
+
158
+ test("intrinsic tag registration: fold rejects an unregistered tag; EVL001 does not (no lexicon manifest at lint time)", () => {
159
+ const source = "const bad = new Thing({ x: Unknown`plain` });";
160
+ const sourceFile = ts.createSourceFile("t.ts", source, ts.ScriptTarget.Latest, true);
161
+ const consts = collectConsts(sourceFile);
162
+ const badInit = consts.get("bad") as ts.NewExpression;
163
+
164
+ expect(() => foldResource(badInit, consts, [])).toThrow(FoldError);
165
+
166
+ const context: LintContext = { sourceFile, entities: [], filePath: "t.ts", lexicon: undefined };
167
+ expect(evl001NonLiteralExpressionRule.check(context)).toHaveLength(0);
168
+ });
169
+
170
+ test("spread-source runtime type: fold rejects spreading a const number; no EVL rule catches it (needs evaluation)", () => {
171
+ const source = `
172
+ const n = 5;
173
+ const bad = new Thing({ x: { ...n } });
174
+ `;
175
+ const sourceFile = ts.createSourceFile("t.ts", source, ts.ScriptTarget.Latest, true);
176
+ const consts = collectConsts(sourceFile);
177
+ const badInit = consts.get("bad") as ts.NewExpression;
178
+
179
+ expect(() => foldResource(badInit, consts, [])).toThrow(FoldError);
180
+
181
+ const context: LintContext = { sourceFile, entities: [], filePath: "t.ts", lexicon: undefined };
182
+ expect(evl001NonLiteralExpressionRule.check(context)).toHaveLength(0);
183
+ expect(evl004SpreadNonConstRule.check(context)).toHaveLength(0);
184
+ });
185
+
186
+ test("&&/||/?? short-circuit: fold folds only the taken side; EVL is flow-insensitive and flags the untaken side too", () => {
187
+ // fold(): the left side is falsy, so `sideEffect()` is never folded — succeeds.
188
+ const shortCircuitSrc = ts.createSourceFile(
189
+ "t.ts",
190
+ `const x = false && sideEffect();`,
191
+ ts.ScriptTarget.Latest,
192
+ true,
193
+ );
194
+ const shortCircuitConsts = collectConsts(shortCircuitSrc);
195
+ const foldedValue = fold(shortCircuitConsts.get("x") as ts.Expression, shortCircuitConsts);
196
+ expect(foldedValue).toBe(false);
197
+
198
+ // EVL001 has no evaluator — it requires every operand to be shape-valid,
199
+ // so it flags the untaken `sideEffect()` branch even though fold()
200
+ // never touches it.
201
+ const source = `const bad = new Thing({ x: false && sideEffect() });`;
202
+ const sourceFile = ts.createSourceFile("t.ts", source, ts.ScriptTarget.Latest, true);
203
+ const context: LintContext = { sourceFile, entities: [], filePath: "t.ts", lexicon: undefined };
204
+ expect(evl001NonLiteralExpressionRule.check(context).length).toBeGreaterThan(0);
205
+ });
206
+
207
+ test("tagged-template interior: fold rejects an unfoldable interpolation in a registered intrinsic tag; EVL is lenient (no registry, interiors opaque)", () => {
208
+ // `Sub`${getName()}`` — fold, with the intrinsic registry, folds the Sub
209
+ // tag's interior and rejects the getName() call. EVL has no registry and
210
+ // can't tell an intrinsic call (Ref, legit) from a plain one, so it treats
211
+ // tagged-template interiors as opaque and does not flag — otherwise it would
212
+ // false-flag `Sub`${Ref(env)}`` and break every intrinsic-using example.
213
+ const source = "const bad = new Thing({ x: Sub`${getName()}` });";
214
+ const sourceFile = ts.createSourceFile("t.ts", source, ts.ScriptTarget.Latest, true);
215
+ const consts = collectConsts(sourceFile);
216
+ const badInit = consts.get("bad") as ts.NewExpression;
217
+
218
+ expect(() => foldResource(badInit, consts, [{ name: "Sub", isTag: true }])).toThrow(FoldError);
219
+
220
+ const context: LintContext = { sourceFile, entities: [], filePath: "t.ts", lexicon: undefined };
221
+ expect(evl001NonLiteralExpressionRule.check(context)).toHaveLength(0);
222
+ });
223
+
224
+ test("nested resource construction: fold rejects a nested `new Type()` used as a value (falls back to run); EVL001 allows it statically", () => {
225
+ // A top-level `new Type()` folds (fold-import constructs a real Declarable),
226
+ // but a NESTED one as a property value can only fold to a {__resource,props}
227
+ // envelope that is never constructed, so it would serialize wrong (real
228
+ // fold-vs-run drift, caught by the #1025 differential on gitlab). fold()
229
+ // therefore rejects it, falling the file back to run; EVL allows it (a valid
230
+ // TS constructor call), so this is an inherent, out-of-scope divergence.
231
+ const source = `const bad = new Thing({ x: new Inner({ y: 1 }) });`;
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, [])).toThrow(FoldError);
237
+
238
+ const context: LintContext = { sourceFile, entities: [], filePath: "t.ts", lexicon: undefined };
239
+ expect(evl001NonLiteralExpressionRule.check(context)).toHaveLength(0);
240
+ });
241
+ });