@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,171 @@
1
+ /**
2
+ * foldable-helpers — the closed, declared allowlist of chant's OWN authoring
3
+ * helpers that a call expression may fold through (chant #1082, epic #1019).
4
+ *
5
+ * ## Why this exists
6
+ *
7
+ * `fold()` has no general case for a `CallExpression`: a function call as a
8
+ * value is structurally unrepresentable there, because folding it would mean
9
+ * executing code, which is the one thing the fold path exists to avoid. That
10
+ * rule is right for user code and stays right — a user's own function, an
11
+ * arrow function, a method call (`naming.name(...)`) all keep failing exactly
12
+ * as before.
13
+ *
14
+ * But it also blocks chant's own documented authoring API. `phase("Apply",
15
+ * [...])` is how the component contract says to write a component; `output(ref,
16
+ * "oX")` is how a lexicon output is authored. An application cannot avoid them
17
+ * and still use components or outputs, so every file that uses one falls back
18
+ * to run no matter how statically evaluable the rest of it is.
19
+ *
20
+ * #1044 already settled the shape of the answer one level down, for lexicon
21
+ * intrinsics: a call-shaped thing may fold ONLY when it is registered, opt-in,
22
+ * per-helper — never because it merely looks like a call. This module is the
23
+ * same decision applied one level up, to chant's own helpers, with the
24
+ * registration written here by hand instead of coming from a lexicon manifest.
25
+ *
26
+ * ## What "registered" buys, and what still has to be true
27
+ *
28
+ * A name in this list is NOT permission to invoke whatever it happens to be
29
+ * bound to. Folding a helper call is a two-key operation:
30
+ *
31
+ * 1. **Shape + name** (here, and in `fold()`/`findSubsetViolation`): the
32
+ * callee is a bare identifier whose text is in {@link
33
+ * FOLDABLE_AUTHORING_HELPERS} and which is not shadowed by a local
34
+ * `const`. `fold()` reduces the call to a symbolic
35
+ * `{ __helper, args }` envelope — it still executes nothing.
36
+ * 2. **Provenance + invocation** (`../discovery/fold-import.ts`): the
37
+ * envelope is revived by resolving that name through the folding FILE'S
38
+ * OWN `import` bindings and checking the import actually comes from chant
39
+ * ({@link isChantOwnedSpecifier}, or a path inside chant-core's own tree
40
+ * for in-repo/absolute-specifier callers). Only then is the real function
41
+ * invoked, with the real folded arguments. A same-named helper imported
42
+ * from somewhere else, or declared in the file itself, resolves to
43
+ * nothing chant owns and the whole file falls back to run.
44
+ *
45
+ * So the function that runs is always the same function the run path would
46
+ * have called, from the same module the source itself imported — fold does not
47
+ * substitute its own reimplementation, which is why this list cannot drift
48
+ * from the helpers' real behavior.
49
+ *
50
+ * ## Admission criteria
51
+ *
52
+ * A helper belongs here only if all of these hold:
53
+ *
54
+ * - chant owns and documents it as authoring surface;
55
+ * - it is a pure function of its arguments — no I/O, no `process.env`, no
56
+ * module-level mutable state, no observable side effect;
57
+ * - calling it early (at fold time) is indistinguishable from calling it
58
+ * during a real run of the file.
59
+ *
60
+ * Deliberately NOT admitted — see this module's tests and the #1082 PR body:
61
+ *
62
+ * - `env()` (`../env.ts`) reads `process.env`. Folding it would bake one
63
+ * run's environment into a statically-derived value. It is exactly the
64
+ * kind of call fold must keep rejecting.
65
+ * - `Op()` (`../op/builders.ts`) constructs an `OpResource` — a `Declarable`.
66
+ * `fold()` already rejects a nested `new Type(...)` used as a value for a
67
+ * real, differential-caught reason (the envelope leaks into
68
+ * serialization); a factory that returns one is the same hazard wearing a
69
+ * call.
70
+ * - `propagate()`, `withDefaults()`, `resource()`, `mergeDefaults()`
71
+ * (`../composite.ts`) are composite *definition* helpers, not value-position
72
+ * helpers. `propagate()` in particular mutates its argument in place, and
73
+ * the composite spine in fold-import.ts already resolves it live.
74
+ * - `createResource()`/`createProperty()` (`../runtime.ts`) are used at a
75
+ * lexicon module's top level to build classes, never as a value inside a
76
+ * project file.
77
+ * - Lexicon intrinsics in CALL form (`Ref(...)`, `Join(...)`) are lexicon
78
+ * surface, not chant's own, and are #1044's registry (`IntrinsicDef`,
79
+ * `../lexicon.ts`) to admit — not this one. Only their tagged-template
80
+ * form folds today.
81
+ */
82
+
83
+ /**
84
+ * One registered helper. `module` and `note` carry no runtime behavior — they
85
+ * are the audit trail for why this entry passed the admission criteria above,
86
+ * kept next to the name it justifies rather than in a comment that can drift
87
+ * away from the list.
88
+ */
89
+ export interface FoldableHelperDef {
90
+ /** The exported name, matched against the callee identifier's text. */
91
+ readonly name: string;
92
+ /** Where chant defines it (a path under `packages/core/src`, for the audit trail). */
93
+ readonly module: string;
94
+ /** Why it qualifies — what it returns and why calling it at fold time is safe. */
95
+ readonly note: string;
96
+ }
97
+
98
+ /**
99
+ * The allowlist. Adding an entry is a deliberate act: it must satisfy every
100
+ * admission criterion in this module's doc, and it widens what `fold()` and
101
+ * `findSubsetViolation` accept for EVERY project, so it belongs in a PR that
102
+ * says so.
103
+ *
104
+ * Two names below are defined TWICE in chant, by different modules, with
105
+ * different return types (`phase`/`gate` by both the component contract and
106
+ * the Op builders; `stackOutput` by both the component contract and the
107
+ * cross-stack output primitive). That is fine and needs no disambiguation
108
+ * here: registration is by name, but the function actually invoked is the one
109
+ * the folding file itself imported (see step 2 in the module doc), so each
110
+ * file gets its own. Both definitions of each name independently satisfy the
111
+ * criteria, which is what makes registering the shared name safe.
112
+ */
113
+ export const FOLDABLE_AUTHORING_HELPERS: readonly FoldableHelperDef[] = [
114
+ {
115
+ name: "phase",
116
+ module: "components/component.ts, op/builders.ts",
117
+ note: "Returns a plain `{ phase, steps, parallel? }` / `{ name, steps, parallel? }` object literal built from its arguments. No state, no I/O.",
118
+ },
119
+ {
120
+ name: "gate",
121
+ module: "components/component.ts, op/builders.ts",
122
+ note: "Returns a plain `{ kind: 'gate', signalName, ... }` object literal built from its arguments.",
123
+ },
124
+ {
125
+ name: "activity",
126
+ module: "op/builders.ts",
127
+ note: "Returns a plain `{ kind: 'activity', fn, args?, profile? }` object literal built from its arguments.",
128
+ },
129
+ {
130
+ name: "stackOutput",
131
+ module: "components/component.ts, stack-output.ts",
132
+ note: "Component form returns a plain `{ stackOutput: { stack, name } }` literal. Cross-stack form derives a `StackOutput` from a real `AttrRef`/`Intrinsic` and throws on anything else — so a fold that only has a symbolic reference fails loudly into run-fallback rather than producing a wrong output.",
133
+ },
134
+ {
135
+ name: "output",
136
+ module: "lexicon-output.ts",
137
+ note: "Constructs a `LexiconOutput` from a real `AttrRef`/`Intrinsic` and a name. Pure, but identity-sensitive: it reads through the ref's `WeakRef` to its parent entity. Only folds when the ref argument revives to a REAL live reference (see fold-import.ts's `requireLiveRefs`); a symbolic `{ __attrRef }` envelope is rejected, not silently wrapped.",
138
+ },
139
+ ];
140
+
141
+ const HELPER_NAMES: ReadonlySet<string> = new Set(FOLDABLE_AUTHORING_HELPERS.map((h) => h.name));
142
+
143
+ /**
144
+ * True when `name` is a registered foldable authoring helper. Name-only — this
145
+ * is the shape-level half of the check (step 1 in the module doc). It says
146
+ * nothing about where the name is bound; `../discovery/fold-import.ts` decides
147
+ * that before anything is invoked.
148
+ */
149
+ export function isFoldableHelperName(name: string): boolean {
150
+ return HELPER_NAMES.has(name);
151
+ }
152
+
153
+ /**
154
+ * Package specifiers chant itself publishes. A registered helper name only
155
+ * folds when the folding file imported it from one of these — or, for in-repo
156
+ * and test callers that import chant-core by relative/absolute path, from
157
+ * inside chant-core's own tree (checked separately, in fold-import.ts, since
158
+ * only that module knows how to resolve a specifier to a path).
159
+ *
160
+ * Lexicon packages are included because several core helpers are re-exported
161
+ * through them and that is the documented import in real projects — e.g.
162
+ * `import { output } from "@intentius/chant-lexicon-aws"`.
163
+ */
164
+ const CHANT_PACKAGE_SPECIFIERS: readonly string[] = ["@intentius/chant", "@intentius/chant-lexicon-"];
165
+
166
+ /** True for a bare specifier that names a chant-published package (or one of its subpaths). */
167
+ export function isChantOwnedSpecifier(specifier: string): boolean {
168
+ return CHANT_PACKAGE_SPECIFIERS.some(
169
+ (prefix) => specifier === prefix || specifier.startsWith(prefix.endsWith("-") ? prefix : `${prefix}/`),
170
+ );
171
+ }
@@ -0,0 +1,210 @@
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("Registered intrinsic calls", () => {
139
+ // chant #1044 — the one doc snippet that needs the registry to classify:
140
+ // `Ref(...)` is only in the subset because a lexicon opted its call form
141
+ // in, and `findSubsetViolation` answers exactly that question when it is
142
+ // given the registry (subset.ts module doc, point 2c). Without one it
143
+ // would report a violation, which is the pre-#1044 answer EVL still gets.
144
+ const consts = parseConsts(extractFencedBlock("Registered intrinsic calls"));
145
+ const intrinsics = [
146
+ { name: "Sub", isTag: true },
147
+ { name: "Ref", isTag: false, foldsAsCall: true },
148
+ ];
149
+ expect(findSubsetViolation(resourceArg(consts, "store"), intrinsics)).toBeUndefined();
150
+ expect(findSubsetViolation(resourceArg(consts, "store"))).toBeDefined();
151
+ });
152
+
153
+ test("Typed property-kind constructors", () => {
154
+ const consts = parseConsts(extractFencedBlock("Typed property-kind constructors"));
155
+ expect(findSubsetViolation(resourceArg(consts, "config"))).toBeUndefined();
156
+ expect(findSubsetViolation(resourceArg(consts, "access"))).toBeUndefined();
157
+ });
158
+
159
+ test("Registered authoring helpers", () => {
160
+ // chant #1082 — not a `new Type({...})` declaration, so classify the
161
+ // exported component object literal directly. `findSubsetViolation`
162
+ // checks the helper NAME only (subset.ts module doc, point 2b); the
163
+ // doc's own snippet imports them from chant, which is what the bridge
164
+ // additionally verifies at fold time.
165
+ const consts = parseConsts(extractFencedBlock("Registered authoring helpers"));
166
+ const web = consts.get("web");
167
+ if (!web) throw new Error(`subset-doc-parity: "web" did not parse as a const declaration`);
168
+ expect(findSubsetViolation(web)).toBeUndefined();
169
+ });
170
+
171
+ test("Nullish coalescing for defaults", () => {
172
+ // Not a standalone statement in the doc (a single object-literal
173
+ // property, deliberately shown as a fragment) — wrapped in an object
174
+ // literal to parse, which also happens to be exactly the shape
175
+ // `findSubsetViolation` classifies a resource prop through.
176
+ const fragment = extractFencedBlock("Nullish coalescing for defaults").trim();
177
+ const consts = parseConsts(`const _wrapped = { ${fragment} };`);
178
+ const wrapped = consts.get("_wrapped");
179
+ if (!wrapped) throw new Error("subset-doc-parity: nullish-coalescing fragment failed to parse");
180
+ expect(findSubsetViolation(wrapped)).toBeUndefined();
181
+ });
182
+ });
183
+
184
+ describe("subset-doc-parity — unsupported patterns in typescript-as-data.mdx classify as rejected", () => {
185
+ test("Function calls as values", () => {
186
+ const consts = parseConsts(extractFencedBlock("Function calls as values"));
187
+ expect(findSubsetViolation(resourceArg(consts, "store"))).toBeDefined();
188
+ });
189
+
190
+ test("Dynamic property access", () => {
191
+ // A bare `const`, not a resource declaration — check the initializer
192
+ // directly rather than through `resourceArg`.
193
+ const consts = parseConsts(extractFencedBlock("Dynamic property access"));
194
+ const init = consts.get("name");
195
+ if (!init) throw new Error(`subset-doc-parity: "name" did not parse as a const declaration`);
196
+ expect(findSubsetViolation(init)).toBeDefined();
197
+ });
198
+
199
+ test("Spread from dynamic sources", () => {
200
+ // Documented as EVL004 (a stricter, value-level narrowing rule subset.ts
201
+ // doesn't model — see subset.ts's module doc, item 3) but this
202
+ // particular example — spreading a CALL's result — is also rejected by
203
+ // subset.ts's shape classifier on its own terms: a call expression
204
+ // nested inside a spread is still a call expression. Asserting only
205
+ // "rejected", not a specific rule id, since attributing a rule id here
206
+ // is EVL004's job, not subset.ts's.
207
+ const consts = parseConsts(extractFencedBlock("Spread from dynamic sources"));
208
+ expect(findSubsetViolation(resourceArg(consts, "store"))).toBeDefined();
209
+ });
210
+ });