@intentius/chant 0.49.0 → 0.50.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 (247) hide show
  1. package/dist/audit/catalog.d.ts +13 -3
  2. package/dist/audit/catalog.d.ts.map +1 -1
  3. package/dist/audit/core.d.ts +9 -0
  4. package/dist/audit/core.d.ts.map +1 -1
  5. package/dist/audit/discover.d.ts +6 -0
  6. package/dist/audit/discover.d.ts.map +1 -1
  7. package/dist/audit/fetch.d.ts.map +1 -1
  8. package/dist/audit/report-html.d.ts.map +1 -1
  9. package/dist/audit/report-model.d.ts +6 -0
  10. package/dist/audit/report-model.d.ts.map +1 -1
  11. package/dist/audit/report.d.ts.map +1 -1
  12. package/dist/audit/rules-doc.d.ts.map +1 -1
  13. package/dist/audit/secrets.d.ts +95 -0
  14. package/dist/audit/secrets.d.ts.map +1 -0
  15. package/dist/audit/wrangler.d.ts +33 -0
  16. package/dist/audit/wrangler.d.ts.map +1 -0
  17. package/dist/build.d.ts.map +1 -1
  18. package/dist/cli/commands/audit.d.ts +7 -0
  19. package/dist/cli/commands/audit.d.ts.map +1 -1
  20. package/dist/cli/commands/build.d.ts +23 -0
  21. package/dist/cli/commands/build.d.ts.map +1 -1
  22. package/dist/cli/handlers/build.d.ts.map +1 -1
  23. package/dist/cli/handlers/components.d.ts +31 -0
  24. package/dist/cli/handlers/components.d.ts.map +1 -1
  25. package/dist/cli/handlers/lifecycle.d.ts +11 -0
  26. package/dist/cli/handlers/lifecycle.d.ts.map +1 -1
  27. package/dist/cli/handlers/operator.d.ts +32 -0
  28. package/dist/cli/handlers/operator.d.ts.map +1 -0
  29. package/dist/cli/handlers/scenario.d.ts +39 -0
  30. package/dist/cli/handlers/scenario.d.ts.map +1 -0
  31. package/dist/cli/main.d.ts.map +1 -1
  32. package/dist/cli/mcp/server.d.ts +35 -2
  33. package/dist/cli/mcp/server.d.ts.map +1 -1
  34. package/dist/cli/mcp/types.d.ts +29 -1
  35. package/dist/cli/mcp/types.d.ts.map +1 -1
  36. package/dist/cli/registry.d.ts +14 -2
  37. package/dist/cli/registry.d.ts.map +1 -1
  38. package/dist/codegen/docs-rule-scanning.d.ts.map +1 -1
  39. package/dist/components/capability.d.ts +17 -2
  40. package/dist/components/capability.d.ts.map +1 -1
  41. package/dist/components/cli-support.d.ts +7 -0
  42. package/dist/components/cli-support.d.ts.map +1 -1
  43. package/dist/components/component.d.ts +15 -0
  44. package/dist/components/component.d.ts.map +1 -1
  45. package/dist/components/driver.d.ts.map +1 -1
  46. package/dist/components/verbs/index.d.ts +6 -1
  47. package/dist/components/verbs/index.d.ts.map +1 -1
  48. package/dist/components/verbs/run-agent.d.ts +499 -0
  49. package/dist/components/verbs/run-agent.d.ts.map +1 -0
  50. package/dist/components/verbs/sign.d.ts +30 -0
  51. package/dist/components/verbs/sign.d.ts.map +1 -1
  52. package/dist/composite.d.ts +6 -1
  53. package/dist/composite.d.ts.map +1 -1
  54. package/dist/discovery/collect.d.ts.map +1 -1
  55. package/dist/discovery/fold-import.d.ts +15 -1
  56. package/dist/discovery/fold-import.d.ts.map +1 -1
  57. package/dist/discovery/fold-rank.d.ts +66 -0
  58. package/dist/discovery/fold-rank.d.ts.map +1 -0
  59. package/dist/discovery/index.d.ts +15 -0
  60. package/dist/discovery/index.d.ts.map +1 -1
  61. package/dist/discovery/param-deps.d.ts +17 -0
  62. package/dist/discovery/param-deps.d.ts.map +1 -0
  63. package/dist/fold/fold.d.ts +55 -2
  64. package/dist/fold/fold.d.ts.map +1 -1
  65. package/dist/fold/subset.d.ts +21 -14
  66. package/dist/fold/subset.d.ts.map +1 -1
  67. package/dist/lexicon-schema.d.ts +2 -0
  68. package/dist/lexicon-schema.d.ts.map +1 -1
  69. package/dist/lexicon.d.ts +93 -0
  70. package/dist/lexicon.d.ts.map +1 -1
  71. package/dist/lifecycle/converge-ledger.d.ts +90 -0
  72. package/dist/lifecycle/converge-ledger.d.ts.map +1 -0
  73. package/dist/lifecycle/deep-diff.d.ts +18 -0
  74. package/dist/lifecycle/deep-diff.d.ts.map +1 -1
  75. package/dist/lifecycle/deep-observe.d.ts +9 -1
  76. package/dist/lifecycle/deep-observe.d.ts.map +1 -1
  77. package/dist/lifecycle/gate-ledger.d.ts +33 -0
  78. package/dist/lifecycle/gate-ledger.d.ts.map +1 -0
  79. package/dist/lifecycle/git.d.ts +145 -21
  80. package/dist/lifecycle/git.d.ts.map +1 -1
  81. package/dist/lifecycle/index.d.ts +4 -0
  82. package/dist/lifecycle/index.d.ts.map +1 -1
  83. package/dist/lifecycle/lease.d.ts +113 -0
  84. package/dist/lifecycle/lease.d.ts.map +1 -0
  85. package/dist/lifecycle/scenario-eval.d.ts +42 -0
  86. package/dist/lifecycle/scenario-eval.d.ts.map +1 -0
  87. package/dist/lifecycle/scenario.d.ts +163 -0
  88. package/dist/lifecycle/scenario.d.ts.map +1 -0
  89. package/dist/lifecycle/symptoms.d.ts +63 -0
  90. package/dist/lifecycle/symptoms.d.ts.map +1 -0
  91. package/dist/lint/output-docs.d.ts +94 -0
  92. package/dist/lint/output-docs.d.ts.map +1 -0
  93. package/dist/lint/post-synth.d.ts +29 -0
  94. package/dist/lint/post-synth.d.ts.map +1 -1
  95. package/dist/lint/rules/__fixtures__/comp/comp003/pass/agent-turn.component.d.ts +11 -0
  96. package/dist/lint/rules/__fixtures__/comp/comp003/pass/agent-turn.component.d.ts.map +1 -0
  97. package/dist/lsp/lexicon-providers.d.ts +7 -0
  98. package/dist/lsp/lexicon-providers.d.ts.map +1 -1
  99. package/dist/op/activity-contract.d.ts +139 -0
  100. package/dist/op/activity-contract.d.ts.map +1 -0
  101. package/dist/op/builders.d.ts +42 -2
  102. package/dist/op/builders.d.ts.map +1 -1
  103. package/dist/op/converge-rule.d.ts +161 -0
  104. package/dist/op/converge-rule.d.ts.map +1 -0
  105. package/dist/op/generate-pipeline.d.ts +39 -0
  106. package/dist/op/generate-pipeline.d.ts.map +1 -0
  107. package/dist/op/index.d.ts +14 -0
  108. package/dist/op/index.d.ts.map +1 -1
  109. package/dist/op/local-executor.d.ts.map +1 -1
  110. package/dist/op/op-verb-class.d.ts +42 -0
  111. package/dist/op/op-verb-class.d.ts.map +1 -0
  112. package/dist/op/operator.d.ts +128 -0
  113. package/dist/op/operator.d.ts.map +1 -0
  114. package/dist/op/step-output-ref.d.ts +187 -0
  115. package/dist/op/step-output-ref.d.ts.map +1 -0
  116. package/dist/op/types.d.ts +18 -1
  117. package/dist/op/types.d.ts.map +1 -1
  118. package/dist/provenance.d.ts +73 -3
  119. package/dist/provenance.d.ts.map +1 -1
  120. package/dist/runtime-adapter.d.ts +7 -1
  121. package/dist/runtime-adapter.d.ts.map +1 -1
  122. package/dist/serializer.d.ts +18 -0
  123. package/dist/serializer.d.ts.map +1 -1
  124. package/dist/toml.d.ts +40 -5
  125. package/dist/toml.d.ts.map +1 -1
  126. package/package.json +1 -1
  127. package/src/audit/catalog.test.ts +1 -1
  128. package/src/audit/catalog.ts +75 -3
  129. package/src/audit/core.ts +9 -0
  130. package/src/audit/discover.ts +29 -2
  131. package/src/audit/fetch.test.ts +216 -3
  132. package/src/audit/fetch.ts +270 -59
  133. package/src/audit/report-html.ts +5 -2
  134. package/src/audit/report-model.ts +9 -0
  135. package/src/audit/report.test.ts +22 -0
  136. package/src/audit/report.ts +3 -2
  137. package/src/audit/rules-doc.ts +2 -0
  138. package/src/audit/secrets.test.ts +303 -0
  139. package/src/audit/secrets.ts +406 -0
  140. package/src/audit/wrangler.test.ts +230 -0
  141. package/src/audit/wrangler.ts +290 -0
  142. package/src/build.ts +8 -3
  143. package/src/cli/command-group.ts +1 -1
  144. package/src/cli/commands/__fixtures__/schemas/sarif-2.1.0.schema.json +2882 -0
  145. package/src/cli/commands/audit.test.ts +215 -1
  146. package/src/cli/commands/audit.ts +86 -17
  147. package/src/cli/commands/build.test.ts +167 -2
  148. package/src/cli/commands/build.ts +114 -23
  149. package/src/cli/handlers/build.ts +2 -0
  150. package/src/cli/handlers/components.test.ts +199 -1
  151. package/src/cli/handlers/components.ts +160 -3
  152. package/src/cli/handlers/graph.test.ts +20 -0
  153. package/src/cli/handlers/graph.ts +10 -1
  154. package/src/cli/handlers/lifecycle.ts +12 -4
  155. package/src/cli/handlers/operator.test.ts +255 -0
  156. package/src/cli/handlers/operator.ts +240 -0
  157. package/src/cli/handlers/scenario.test.ts +456 -0
  158. package/src/cli/handlers/scenario.ts +330 -0
  159. package/src/cli/main.test.ts +23 -0
  160. package/src/cli/main.ts +72 -1
  161. package/src/cli/mcp/server.test.ts +265 -2
  162. package/src/cli/mcp/server.ts +84 -7
  163. package/src/cli/mcp/types.ts +27 -1
  164. package/src/cli/registry.ts +14 -2
  165. package/src/codegen/docs-rule-scanning.test.ts +42 -0
  166. package/src/codegen/docs-rule-scanning.ts +25 -2
  167. package/src/components/README.md +7 -0
  168. package/src/components/capability.ts +17 -2
  169. package/src/components/cli-support.test.ts +17 -0
  170. package/src/components/cli-support.ts +13 -1
  171. package/src/components/component-schema.test.ts +32 -0
  172. package/src/components/component.schema.json +6 -0
  173. package/src/components/component.test.ts +21 -0
  174. package/src/components/component.ts +15 -0
  175. package/src/components/driver.ts +12 -4
  176. package/src/components/verbs/index.ts +6 -1
  177. package/src/components/verbs/run-agent.test.ts +683 -0
  178. package/src/components/verbs/run-agent.ts +786 -0
  179. package/src/components/verbs/sign.test.ts +19 -0
  180. package/src/components/verbs/sign.ts +34 -2
  181. package/src/composite.ts +31 -2
  182. package/src/discovery/collect.ts +11 -2
  183. package/src/discovery/fold-import.test.ts +54 -0
  184. package/src/discovery/fold-import.ts +178 -38
  185. package/src/discovery/fold-rank.test.ts +197 -0
  186. package/src/discovery/fold-rank.ts +346 -0
  187. package/src/discovery/index.ts +16 -1
  188. package/src/discovery/param-deps.test.ts +118 -0
  189. package/src/discovery/param-deps.ts +170 -0
  190. package/src/fold/fold.test.ts +6 -2
  191. package/src/fold/fold.ts +184 -3
  192. package/src/fold/subset.test.ts +82 -19
  193. package/src/fold/subset.ts +79 -41
  194. package/src/lexicon-schema.ts +3 -0
  195. package/src/lexicon.ts +103 -2
  196. package/src/lifecycle/converge-ledger.test.ts +199 -0
  197. package/src/lifecycle/converge-ledger.ts +179 -0
  198. package/src/lifecycle/deep-diff.test.ts +79 -1
  199. package/src/lifecycle/deep-diff.ts +23 -0
  200. package/src/lifecycle/deep-observe.ts +13 -2
  201. package/src/lifecycle/gate-ledger.test.ts +103 -0
  202. package/src/lifecycle/gate-ledger.ts +140 -0
  203. package/src/lifecycle/git.test.ts +430 -0
  204. package/src/lifecycle/git.ts +446 -84
  205. package/src/lifecycle/index.ts +4 -0
  206. package/src/lifecycle/lease.test.ts +343 -0
  207. package/src/lifecycle/lease.ts +270 -0
  208. package/src/lifecycle/scenario-eval.test.ts +199 -0
  209. package/src/lifecycle/scenario-eval.ts +158 -0
  210. package/src/lifecycle/scenario.test.ts +195 -0
  211. package/src/lifecycle/scenario.ts +321 -0
  212. package/src/lifecycle/symptoms.test.ts +116 -0
  213. package/src/lifecycle/symptoms.ts +126 -0
  214. package/src/lint/output-docs.test.ts +220 -0
  215. package/src/lint/output-docs.ts +204 -0
  216. package/src/lint/post-synth.test.ts +97 -0
  217. package/src/lint/post-synth.ts +45 -0
  218. package/src/lint/rules/__fixtures__/comp/comp003/pass/agent-turn.component.ts +26 -0
  219. package/src/lint/rules/comp/comp.test.ts +49 -1
  220. package/src/lint/rules/evl001-non-literal-expression.test.ts +8 -3
  221. package/src/lint/rules/evl001-non-literal-expression.ts +6 -6
  222. package/src/lsp/lexicon-providers.test.ts +44 -0
  223. package/src/lsp/lexicon-providers.ts +11 -1
  224. package/src/op/activity-contract.test.ts +180 -0
  225. package/src/op/activity-contract.ts +278 -0
  226. package/src/op/builders-exports.test.ts +17 -1
  227. package/src/op/builders.ts +59 -5
  228. package/src/op/converge-rule.test.ts +179 -0
  229. package/src/op/converge-rule.ts +311 -0
  230. package/src/op/generate-pipeline.test.ts +53 -0
  231. package/src/op/generate-pipeline.ts +99 -0
  232. package/src/op/index.ts +30 -0
  233. package/src/op/local-executor.test.ts +92 -0
  234. package/src/op/local-executor.ts +45 -9
  235. package/src/op/op-verb-class.test.ts +126 -0
  236. package/src/op/op-verb-class.ts +115 -0
  237. package/src/op/operator.test.ts +346 -0
  238. package/src/op/operator.ts +213 -0
  239. package/src/op/step-output-ref.test.ts +334 -0
  240. package/src/op/step-output-ref.ts +453 -0
  241. package/src/op/types.ts +18 -1
  242. package/src/provenance.test.ts +151 -4
  243. package/src/provenance.ts +118 -4
  244. package/src/runtime-adapter.ts +31 -10
  245. package/src/serializer.ts +18 -0
  246. package/src/toml.test.ts +157 -384
  247. package/src/toml.ts +371 -5
@@ -0,0 +1,118 @@
1
+ import * as ts from "typescript";
2
+ import { describe, expect, test } from "vitest";
3
+ import { collectConsts } from "../fold/fold";
4
+ import { collectParamDependencies } from "./param-deps";
5
+ import type { PathOrigin } from "../provenance";
6
+
7
+ /**
8
+ * Collect dependencies for the props of the file's single
9
+ * `export const x = new Type({...})`, which is the shape every case here uses.
10
+ */
11
+ function depsOf(source: string, paramLocals = ["params"]): Record<string, PathOrigin> {
12
+ const file = ts.createSourceFile("fixture.ts", source, ts.ScriptTarget.Latest, true);
13
+ const consts = collectConsts(file);
14
+ let props: ts.ObjectLiteralExpression | undefined;
15
+ for (const statement of file.statements) {
16
+ if (!ts.isVariableStatement(statement)) continue;
17
+ for (const decl of statement.declarationList.declarations) {
18
+ if (decl.name.getText() !== "x" || !decl.initializer) continue;
19
+ if (!ts.isNewExpression(decl.initializer)) continue;
20
+ for (const argument of decl.initializer.arguments ?? []) {
21
+ if (ts.isObjectLiteralExpression(argument)) {
22
+ props = argument;
23
+ break;
24
+ }
25
+ }
26
+ }
27
+ }
28
+ if (!props) throw new Error("fixture has no `const x = new Type({...})`");
29
+ return collectParamDependencies(props, consts, new Set(paramLocals));
30
+ }
31
+
32
+ const param = (...names: string[]): PathOrigin => ({ kind: "build-param", params: names });
33
+
34
+ describe("collectParamDependencies", () => {
35
+ test("a direct read is attributed to its own path", () => {
36
+ expect(depsOf(`export const x = new Thing({ replicas: params.replicas, name: "fixed" });`)).toEqual({
37
+ replicas: param("replicas"),
38
+ });
39
+ });
40
+
41
+ test("nested object literals are descended into, dotted", () => {
42
+ const source = `export const x = new Thing({ spec: { template: { image: params.image } } });`;
43
+ expect(depsOf(source)).toEqual({ "spec.template.image": param("image") });
44
+ });
45
+
46
+ test("an expression is attributed to every parameter it can read, not to its value", () => {
47
+ const source = `export const x = new Thing({ replicas: params.tier === "prod" ? params.big : 1 });`;
48
+ expect(depsOf(source)).toEqual({ replicas: param("big", "tier") });
49
+ });
50
+
51
+ test("a parameter hoisted into a const is followed to the field that uses it", () => {
52
+ const source = [
53
+ `const replicas = params.tier === "prod" ? 5 : 1;`,
54
+ `export const x = new Thing({ replicas });`,
55
+ ].join("\n");
56
+ expect(depsOf(source)).toEqual({ replicas: param("tier") });
57
+ });
58
+
59
+ test("const chains are followed transitively, and a cycle terminates", () => {
60
+ const source = [
61
+ `const a = b;`,
62
+ `const b = \`\${params.region}-\${a}\`;`,
63
+ `export const x = new Thing({ zone: a });`,
64
+ ].join("\n");
65
+ expect(depsOf(source)).toEqual({ zone: param("region") });
66
+ });
67
+
68
+ test("template literals and calls are walked", () => {
69
+ const source = `export const x = new Thing({ bucket: \`\${params.env}-assets\`.toLowerCase() });`;
70
+ expect(depsOf(source)).toEqual({ bucket: param("env") });
71
+ });
72
+
73
+ test("an array is attributed whole, never per index", () => {
74
+ const source = `export const x = new Thing({ containers: [{ image: params.image }, { image: "sidecar" }] });`;
75
+ expect(depsOf(source)).toEqual({ containers: param("image") });
76
+ });
77
+
78
+ test("a spread is attributed to the object it spreads into", () => {
79
+ const source = [
80
+ `const base = { region: params.region };`,
81
+ `export const x = new Thing({ ...base, spec: { ...base, replicas: 1 } });`,
82
+ ].join("\n");
83
+ expect(depsOf(source)).toEqual({ "": param("region"), spec: param("region") });
84
+ });
85
+
86
+ test("bracket access with a literal key names the parameter", () => {
87
+ expect(depsOf(`export const x = new Thing({ zone: params["region"] });`)).toEqual({ zone: param("region") });
88
+ });
89
+
90
+ test("a property KEY that happens to match a const is not a reference", () => {
91
+ const source = [`const tier = params.tier;`, `export const x = new Thing({ spec: { tier: "fixed" } });`].join("\n");
92
+ expect(depsOf(source)).toEqual({});
93
+ });
94
+
95
+ test("a bare reference to the whole params object records nothing", () => {
96
+ // It names no single parameter; under-reporting is the safe direction.
97
+ expect(depsOf(`export const x = new Thing({ all: params });`)).toEqual({});
98
+ });
99
+
100
+ test("a file that never imported params records nothing", () => {
101
+ expect(depsOf(`export const x = new Thing({ replicas: params.replicas });`, [])).toEqual({});
102
+ });
103
+
104
+ test("the local name the import bound is what counts, not the word 'params'", () => {
105
+ const source = `export const x = new Thing({ replicas: p.replicas, other: params.replicas });`;
106
+ expect(depsOf(source, ["p"])).toEqual({ replicas: param("replicas") });
107
+ });
108
+
109
+ test("parameter names are sorted and de-duplicated", () => {
110
+ const source = `export const x = new Thing({ n: params.z + params.a + params.z });`;
111
+ expect(depsOf(source)).toEqual({ n: param("a", "z") });
112
+ });
113
+
114
+ test("a computed key is skipped rather than guessed at", () => {
115
+ const source = `export const x = new Thing({ [params.key]: 1, name: params.name });`;
116
+ expect(depsOf(source)).toEqual({ name: param("name") });
117
+ });
118
+ });
@@ -0,0 +1,170 @@
1
+ import * as ts from "typescript";
2
+ import type { PathOrigin } from "../provenance";
3
+
4
+ /**
5
+ * Which build parameters an authored props expression READS, per path (chant
6
+ * #1443) — the declared-side counterpart of `managed-fields.ts`'s per-path live
7
+ * `owners`.
8
+ *
9
+ * This is a syntactic dependency, not a value taint. Fold substitutes
10
+ * `params.<name>` before anything is emitted, so by output time the value is a
11
+ * literal and indistinguishable from one typed by hand; tainting the value
12
+ * through fold's evaluator would mean propagating a tag through every operator
13
+ * it supports, and one operator that forgot would produce silently wrong
14
+ * provenance. Recording which parameters the *expression* mentions cannot
15
+ * degrade that way: a shape this walk does not follow drops a dependency, it
16
+ * never invents one.
17
+ *
18
+ * It also answers the more useful question. For
19
+ * `replicas: params.tier === "prod" ? 5 : 1`, a taint reports that the value
20
+ * came from the literal `5` — true, and no help to anyone about to edit the
21
+ * field. The dependency reports that the field is governed by `tier`.
22
+ *
23
+ * Known under-reporting, all in the safe direction: destructured parameters
24
+ * (`const { tier } = params`) are not tracked, because `collectConsts` only
25
+ * records identifier bindings; a bare reference to the whole `params` object
26
+ * names no single parameter and records nothing; and the run path has no
27
+ * expression to walk at all.
28
+ *
29
+ * The one direction it can over-report is a local binding that shadows a
30
+ * top-level `const` of the same name, since names resolve against the file's
31
+ * consts without scope tracking. That reports a parameter the field could
32
+ * plausibly be governed by rather than one it definitely is, which is the
33
+ * failure the "could affect" reading is written to absorb.
34
+ */
35
+
36
+ /** The property name a member declares, when it is a literal one. */
37
+ function literalName(name: ts.PropertyName | undefined): string | undefined {
38
+ if (!name) return undefined;
39
+ if (ts.isIdentifier(name) || ts.isStringLiteral(name) || ts.isNumericLiteral(name)) return name.text;
40
+ return undefined;
41
+ }
42
+
43
+ /** Strip the wrappers that do not change what an initializer reads. */
44
+ function unwrap(expr: ts.Expression): ts.Expression {
45
+ let current = expr;
46
+ for (;;) {
47
+ if (ts.isParenthesizedExpression(current) || ts.isAsExpression(current) || ts.isNonNullExpression(current)) {
48
+ current = current.expression;
49
+ continue;
50
+ }
51
+ return current;
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Every parameter name `expr` reads, following this file's own top-level
57
+ * `const` bindings so a value hoisted into a local is still attributed
58
+ * (`const replicas = params.tier === "prod" ? 5 : 1` used as `replicas`).
59
+ */
60
+ function readParams(
61
+ expr: ts.Expression,
62
+ consts: ReadonlyMap<string, ts.Expression>,
63
+ paramLocals: ReadonlySet<string>,
64
+ out: Set<string>,
65
+ ): void {
66
+ const followed = new Set<string>();
67
+
68
+ const visit = (node: ts.Node): void => {
69
+ if (ts.isPropertyAccessExpression(node)) {
70
+ if (ts.isIdentifier(node.expression) && paramLocals.has(node.expression.text)) {
71
+ out.add(node.name.text);
72
+ return;
73
+ }
74
+ visit(node.expression);
75
+ return;
76
+ }
77
+
78
+ if (ts.isElementAccessExpression(node)) {
79
+ if (ts.isIdentifier(node.expression) && paramLocals.has(node.expression.text)) {
80
+ if (ts.isStringLiteralLike(node.argumentExpression)) out.add(node.argumentExpression.text);
81
+ } else {
82
+ visit(node.expression);
83
+ }
84
+ visit(node.argumentExpression);
85
+ return;
86
+ }
87
+
88
+ if (ts.isIdentifier(node)) {
89
+ // A bare `params` names no single parameter — see the under-reporting
90
+ // note above.
91
+ if (paramLocals.has(node.text)) return;
92
+ const initializer = consts.get(node.text);
93
+ if (initializer && !followed.has(node.text)) {
94
+ followed.add(node.text);
95
+ visit(initializer);
96
+ }
97
+ return;
98
+ }
99
+
100
+ // A property KEY is not a reference; without this a nested `{ tier: 1 }`
101
+ // would resolve `tier` against `consts` and manufacture a dependency.
102
+ if (ts.isPropertyAssignment(node)) {
103
+ visit(node.initializer);
104
+ return;
105
+ }
106
+
107
+ ts.forEachChild(node, visit);
108
+ };
109
+
110
+ visit(expr);
111
+ }
112
+
113
+ /**
114
+ * Path → build-parameter origin for one resource's props object literal.
115
+ *
116
+ * Paths are dotted property names only, per `EntityProvenance.paths`: an object
117
+ * literal is descended into, and anything else — an array literal included — is
118
+ * attributed whole at its own path. An array's elements are deliberately not
119
+ * indexed, because an index-shaped key would not survive an element moving and
120
+ * would not match the `[#key]` addressing a diff uses.
121
+ *
122
+ * A spread is attributed to the object it spreads INTO, at that object's own
123
+ * path (the entity root for a top-level spread), since which keys it
124
+ * contributes is not knowable here.
125
+ */
126
+ export function collectParamDependencies(
127
+ props: ts.ObjectLiteralExpression,
128
+ consts: ReadonlyMap<string, ts.Expression>,
129
+ paramLocals: ReadonlySet<string>,
130
+ ): Record<string, PathOrigin> {
131
+ const out: Record<string, PathOrigin> = {};
132
+ if (paramLocals.size === 0) return out;
133
+
134
+ const record = (path: string, expr: ts.Expression): void => {
135
+ const found = new Set<string>();
136
+ readParams(expr, consts, paramLocals, found);
137
+ if (found.size === 0) return;
138
+ const existing = out[path];
139
+ const merged =
140
+ existing && existing.kind === "build-param" ? new Set([...existing.params, ...found]) : found;
141
+ out[path] = { kind: "build-param", params: [...merged].sort() };
142
+ };
143
+
144
+ const walk = (object: ts.ObjectLiteralExpression, prefix: string): void => {
145
+ for (const member of object.properties) {
146
+ if (ts.isSpreadAssignment(member)) {
147
+ record(prefix, member.expression);
148
+ continue;
149
+ }
150
+ if (ts.isShorthandPropertyAssignment(member)) {
151
+ const key = member.name.text;
152
+ record(prefix ? `${prefix}.${key}` : key, member.name);
153
+ continue;
154
+ }
155
+ if (!ts.isPropertyAssignment(member)) continue;
156
+ const key = literalName(member.name);
157
+ if (key === undefined) continue;
158
+ const path = prefix ? `${prefix}.${key}` : key;
159
+ const initializer = unwrap(member.initializer);
160
+ if (ts.isObjectLiteralExpression(initializer)) {
161
+ walk(initializer, path);
162
+ continue;
163
+ }
164
+ record(path, member.initializer);
165
+ }
166
+ };
167
+
168
+ walk(props, "");
169
+ return out;
170
+ }
@@ -435,7 +435,11 @@ describe("fold — registered call-form intrinsics (#1044)", () => {
435
435
  expect(() => fold(expr, consts, [REF])).toThrow(FoldError);
436
436
  });
437
437
 
438
- test("an array method taking a closure is rejected — .map is arbitrary JS, not a registered intrinsic", () => {
438
+ test("an array method taking a closure is rejected — the closure argument, not .map itself, is what fails (chant #1966)", () => {
439
+ // chant #1966 — `.map` is now attempted (a real method on a real folded
440
+ // array), so the rejection moves from "no case for this call" to the
441
+ // closure argument itself: a function used as a value is never foldable,
442
+ // with or without a registered intrinsic inside it.
439
443
  const consts = parseConsts(`const cidrs = ["10.0.0.0/24"]; const x = cidrs.map((c) => Ref(c));`);
440
444
  const expr = consts.get("x");
441
445
  if (!expr) throw new Error("fixture error");
@@ -446,7 +450,7 @@ describe("fold — registered call-form intrinsics (#1044)", () => {
446
450
  error = e;
447
451
  }
448
452
  expect(error).toBeInstanceOf(FoldError);
449
- expect((error as FoldError).message).toContain("cidrs.map(...)");
453
+ expect((error as FoldError).message).toContain("a function used as a value is not foldable");
450
454
  });
451
455
 
452
456
  test("a user-defined function is rejected however it's named", () => {
package/src/fold/fold.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import * as ts from "typescript";
2
2
  import { relative } from "node:path";
3
- import { intrinsicCallFolds, intrinsicTagFolds, type IntrinsicDef } from "../lexicon";
3
+ import { intrinsicCallFolds, intrinsicCallFoldsEagerly, intrinsicTagFolds, type IntrinsicDef } from "../lexicon";
4
4
  import {
5
5
  SUPPORTED_BINARY_OPERATORS,
6
6
  SUPPORTED_UNARY_OPERATORS,
@@ -67,7 +67,27 @@ import { isFoldableHelperName } from "./foldable-helpers";
67
67
  * folds its body against the defining module's scope with the folded
68
68
  * arguments bound. Still nothing is imported or run.
69
69
  *
70
- * Everything else — a package's function, a method call, an array `.map`, a
70
+ * A bare composite factory call (`Checkout({...})` on its own) is still out
71
+ * of scope (epic Phase 5, #1023 covers only interpreting a factory's OWN
72
+ * body, not consuming its result as a value elsewhere). chant #1174 adds one
73
+ * narrow exception at the PROPERTY-ACCESS level rather than here: the
74
+ * `<Identifier>(...).step` idiom — see {@link FoldedCompositeStepCall} on the
75
+ * property-access branch below. A call with no `.step` narrowing, or any
76
+ * other member, still throws from this branch exactly as before.
77
+ *
78
+ * chant #1966 adds a fourth call shape, and a method call on top of any of
79
+ * the four: a lexicon-package function its lexicon registered with
80
+ * {@link intrinsicCallFoldsEagerly} (../lexicon.ts) evaluates eagerly —
81
+ * unlike the other three, which envelope for later revival — because its
82
+ * usual use (`` `${matrix("os")}` ``) coerces the result via `String()` at
83
+ * fold time, before any revival would run. A `CallExpression` whose callee is
84
+ * a property access — `github.actor.toString()`, `[...].join(",")`,
85
+ * `matrix("os").toString()` — folds its receiver and calls the named method
86
+ * on it directly, PROVIDED the receiver is a real value and not one of
87
+ * fold's own symbolic envelopes (see {@link isFoldSymbolicEnvelope}); nothing
88
+ * about the method name is otherwise restricted.
89
+ *
90
+ * Everything else — an ordinary package function, an array `.map`, a
71
91
  * registered name shadowed by a local binding — still throws.
72
92
  *
73
93
  * Cross-file identifier resolution (chant #1020): `consts` alone is always
@@ -105,7 +125,8 @@ export type FoldedValue =
105
125
  | FoldedIntrinsic
106
126
  | FoldedHelperCall
107
127
  | SymbolicValue
108
- | FoldedResource;
128
+ | FoldedResource
129
+ | FoldedCompositeStepCall;
109
130
 
110
131
  /**
111
132
  * Symbolic reference produced when a property/element access resolves to an
@@ -225,6 +246,40 @@ export interface FoldedResource {
225
246
  args?: FoldedValue[];
226
247
  }
227
248
 
249
+ /**
250
+ * The result of folding the `<Identifier>(...).step` composite-consumer
251
+ * idiom (chant #1174) — `Checkout({...}).step`, `SetupNode({...}).step`,
252
+ * every lexicon's single-action `Composite()` wrapper embedded inline in a
253
+ * `Job`'s `steps` array, exactly as composites.mdx documents it. This is the
254
+ * SAME shape chant #1544 already carved out of EVL001 as a documented,
255
+ * correct fallback rather than a lint error
256
+ * ({@link "./subset"}'s `allowCompositeStepAccess`) — `fold()` never set
257
+ * that flag, so before this it fell back to run every time, as designed.
258
+ * This is the fold-side counterpart that actually reduces it instead.
259
+ *
260
+ * Symbolic, exactly like {@link FoldedIntrinsic}/{@link FoldedHelperCall}:
261
+ * `fold()` executes nothing here. It records which composite factory the
262
+ * source named, its folded arguments (in source order), and that the result
263
+ * was immediately narrowed to `.step`. ../discovery/fold-import.ts's bridge
264
+ * resolves the callee through the folding file's own imports — a
265
+ * project-file registered `Composite` is interpreted (chant #1023's existing
266
+ * machinery), anything else (every lexicon-package composite, which is what
267
+ * `Checkout`/`SetupNode` are) is imported and invoked for real, exactly as a
268
+ * top-level `export const x = Checkout({...})` already does via
269
+ * `resolveCallExpression` — and then reads `.step` off the REAL result.
270
+ *
271
+ * Deliberately narrower than "any call, any member access": the member name
272
+ * is fixed to `"step"`, matching the one idiom ../fold/subset.ts's EVL
273
+ * carve-out already permits. `fold()` may never accept a shape EVL doesn't
274
+ * (see that module's doc, point 2c, for the direction it is not allowed to
275
+ * be wrong in) — widening past `.step` here without widening the shared
276
+ * predicate in lockstep would open exactly that gap.
277
+ */
278
+ export interface FoldedCompositeStepCall {
279
+ __compositeStep: string;
280
+ args: FoldedValue[];
281
+ }
282
+
228
283
  /**
229
284
  * One entry per exported `const` resource declaration in {@link foldModule}'s
230
285
  * result. The `ok: false` case surfaces the same located, rule-id-tagged
@@ -648,11 +703,57 @@ function resolvesToResource(consts: Map<string, ts.Expression>, ident: ts.Identi
648
703
  return init !== undefined && ts.isNewExpression(init);
649
704
  }
650
705
 
706
+ /**
707
+ * True when `node` is a call through a bare identifier that isn't ALREADY
708
+ * one of `fold()`'s other three call shapes — a registered authoring helper,
709
+ * a registered call-form intrinsic, or a project-local {@link FoldableFunction}
710
+ * — i.e. exactly the callee the `CallExpression` branch below would
711
+ * otherwise throw {@link callExpressionMessage} for. Used only by the
712
+ * `.step` narrowing in the property-access branch (chant #1174,
713
+ * {@link FoldedCompositeStepCall}): checked from the OUTSIDE, at the
714
+ * property-access node, so `Checkout({...})` alone (no `.step`) still falls
715
+ * through to the ordinary `CallExpression` throw, unchanged.
716
+ */
717
+ function isUnclaimedBareCall(
718
+ node: ts.Expression,
719
+ consts: Map<string, ts.Expression>,
720
+ intrinsics: readonly IntrinsicDef[],
721
+ externals?: ReadonlyMap<string, unknown>,
722
+ ): node is ts.CallExpression {
723
+ if (!ts.isCallExpression(node) || !ts.isIdentifier(node.expression)) return false;
724
+ const name = node.expression.text;
725
+ if (consts.has(name)) return false;
726
+ if (isFoldableHelperName(name)) return false;
727
+ if (intrinsics.some((i) => i.name === name && intrinsicCallFolds(i))) return false;
728
+ if (isFoldableFunction(externals?.get(name))) return false;
729
+ return true;
730
+ }
731
+
651
732
  /** True when a folded value is a {@link FoldedResource} envelope (a `new Type(...)` that nothing constructed yet). */
652
733
  function isFoldedResource(value: FoldedValue): value is FoldedResource {
653
734
  return typeof value === "object" && value !== null && !Array.isArray(value) && "__resource" in value;
654
735
  }
655
736
 
737
+ /**
738
+ * True when `value` is one of `fold()`'s own symbolic envelope shapes — a
739
+ * stand-in for a value nothing has constructed or revived yet, not the value
740
+ * itself. A method call (see the `CallExpression` branch's property-access
741
+ * case below) must refuse one rather than silently falling through to
742
+ * `Object.prototype`'s own inherited methods — `toString` chief among them —
743
+ * which would answer with the placeholder's shape instead of what the real,
744
+ * eventually-revived value would produce.
745
+ */
746
+ function isFoldSymbolicEnvelope(value: FoldedValue): boolean {
747
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
748
+ return (
749
+ "__attrRef" in value ||
750
+ "__intrinsic" in value ||
751
+ "__helper" in value ||
752
+ "__resource" in value ||
753
+ "__compositeStep" in value
754
+ );
755
+ }
756
+
656
757
  /**
657
758
  * chant #1535 — an attribute read whose object folded to a resource ENVELOPE
658
759
  * rather than resolving through {@link resolvesToResource}. That happens when
@@ -892,6 +993,15 @@ export function fold(
892
993
  if (isFoldableFunction(external)) {
893
994
  throw foldError(node, `function "${node.text}" used as a value is not foldable`);
894
995
  }
996
+ // chant #1966 — a registered eager-fold lexicon helper (see the
997
+ // CallExpression branch below) is callable, never a bare value:
998
+ // nothing downstream can serialize a function.
999
+ if (
1000
+ typeof external === "function" &&
1001
+ intrinsics.some((i) => i.name === node.text && intrinsicCallFoldsEagerly(i))
1002
+ ) {
1003
+ throw foldError(node, `function "${node.text}" used as a value is not foldable — call it instead`);
1004
+ }
895
1005
  return external as FoldedValue;
896
1006
  }
897
1007
  // chant #1064 — a bare `process` reference is ALWAYS an ambient
@@ -948,6 +1058,21 @@ export function fold(
948
1058
  if (ts.isIdentifier(node.expression) && resolvesToResource(consts, node.expression)) {
949
1059
  return { __attrRef: { entity: node.expression.text, attribute: node.name.text } };
950
1060
  }
1061
+ // chant #1174 — `<Identifier>(...).step`, the composite-consumer idiom
1062
+ // (`Checkout({...}).step`) — see FoldedCompositeStepCall's doc. Checked
1063
+ // here, at the property-access node, rather than inside the
1064
+ // CallExpression branch: a bare `Checkout({...})` with no `.step` still
1065
+ // has no case there and throws exactly as before.
1066
+ if (node.name.text === "step" && isUnclaimedBareCall(node.expression, consts, intrinsics, externals)) {
1067
+ const call = node.expression;
1068
+ const calleeName = (call.expression as ts.Identifier).text;
1069
+ const inside = insideFunctionBody(node, `composite call \`${calleeName}(...).step\``);
1070
+ if (inside) throw inside;
1071
+ return {
1072
+ __compositeStep: calleeName,
1073
+ args: call.arguments.map((arg) => fold(arg, consts, intrinsics, externals)),
1074
+ };
1075
+ }
951
1076
  const obj = fold(node.expression, consts, intrinsics, externals);
952
1077
  if (obj === null || obj === undefined) return undefined;
953
1078
  if (isFoldedResource(obj)) return attrRefOnFoldedResource(node, node.name.text);
@@ -1146,6 +1271,62 @@ export function fold(
1146
1271
  }
1147
1272
  }
1148
1273
 
1274
+ // chant #1966 — the fourth call shape: a lexicon-package function its
1275
+ // lexicon registered with {@link intrinsicCallFoldsEagerly} (../lexicon.ts),
1276
+ // resolved into `externals` by ../discovery/fold-import.ts's
1277
+ // `resolveActiveLexiconExport` exactly like a plain data export
1278
+ // (`Azure.ResourceGroupLocation`, chant #1063), except callable. Unlike
1279
+ // the intrinsic-call shape above, this one is EVALUATED right here rather
1280
+ // than enveloped: a lexicon's own string-building helper (`matrix("os")`,
1281
+ // github lexicon) is typically embedded directly in a template literal
1282
+ // (`` `${matrix("os")}` ``), which coerces its result via native
1283
+ // `String()` at fold time — an envelope deferred to later revival would
1284
+ // stringify as "[object Object]" there. Evaluating eagerly, with the
1285
+ // folded arguments, produces the real, already-live return value instead
1286
+ // — the same guarantee a live external's own getter execution already
1287
+ // gives {@link fold}'s property-access branch.
1288
+ if (
1289
+ ts.isIdentifier(node.expression) &&
1290
+ !consts.has(node.expression.text) &&
1291
+ intrinsics.some((i) => i.name === (node.expression as ts.Identifier).text && intrinsicCallFoldsEagerly(i))
1292
+ ) {
1293
+ const callee = externals?.get(node.expression.text);
1294
+ if (typeof callee !== "function") {
1295
+ throw foldError(node, `"${node.expression.text}" did not resolve to a function — falls back to run`);
1296
+ }
1297
+ const args = node.arguments.map((arg) => fold(arg, consts, intrinsics, externals));
1298
+ return (callee as (...callArgs: unknown[]) => unknown)(...args) as FoldedValue;
1299
+ }
1300
+
1301
+ // chant #1966 — a method call whose RECEIVER is itself foldable: property
1302
+ // access on a live external (`github.actor.toString()`), a call folded by
1303
+ // one of the shapes above (`matrix("os").toString()`), or fold's own
1304
+ // array/object literal (`[...].join(",")`). The method is never checked
1305
+ // by name — only that the receiver is a REAL value (not one of fold's own
1306
+ // symbolic envelopes, see {@link isFoldSymbolicEnvelope}) and that the
1307
+ // named property on it is actually a function. Calling it with the folded
1308
+ // arguments is then no different from what running the file would do:
1309
+ // the receiver is the same real object either way.
1310
+ if (ts.isPropertyAccessExpression(node.expression)) {
1311
+ const methodName = node.expression.name.text;
1312
+ const receiver = fold(node.expression.expression, consts, intrinsics, externals);
1313
+ if (receiver === null || receiver === undefined) {
1314
+ throw foldError(node, `cannot call ".${methodName}(...)" on ${String(receiver)}`);
1315
+ }
1316
+ if (isFoldSymbolicEnvelope(receiver)) {
1317
+ throw foldError(
1318
+ node,
1319
+ `method call \`.${methodName}(...)\` on an unresolved value is not foldable — falls back to run`,
1320
+ );
1321
+ }
1322
+ const method = (receiver as Record<string, unknown>)[methodName];
1323
+ if (typeof method !== "function") {
1324
+ throw foldError(node, `"${methodName}" is not a callable method on the folded value — falls back to run`);
1325
+ }
1326
+ const args = node.arguments.map((arg) => fold(arg, consts, intrinsics, externals));
1327
+ return (method as (...methodArgs: unknown[]) => unknown).apply(receiver, args) as FoldedValue;
1328
+ }
1329
+
1149
1330
  throw foldError(node, callExpressionMessage(node));
1150
1331
  }
1151
1332