@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
@@ -122,6 +122,25 @@ describe("buildProvenanceStatement — SLSA in-toto statement from #614 provenan
122
122
  }),
123
123
  ).toThrow(SignTargetNotDigestError);
124
124
  });
125
+
126
+ it("reserved parameter keys win over caller-supplied collisions (#1943)", () => {
127
+ const statement = buildProvenanceStatement({
128
+ imageRef: DIGEST_REF,
129
+ provenance: { sourceRef: "abc123def", artifactDigest: DIGEST_REF.split("@")[1]! },
130
+ builderId: "builder",
131
+ externalParameters: { sourceRef: "spoofed", agent: "reviewer" },
132
+ internalParameters: { artifactDigest: "spoofed", spriteId: "s-1" },
133
+ });
134
+
135
+ expect(statement.predicate.buildDefinition.externalParameters).toEqual({
136
+ sourceRef: "abc123def",
137
+ agent: "reviewer",
138
+ });
139
+ expect(statement.predicate.buildDefinition.internalParameters).toEqual({
140
+ artifactDigest: DIGEST_REF.split("@")[1]!,
141
+ spriteId: "s-1",
142
+ });
143
+ });
125
144
  });
126
145
 
127
146
  describe("attest-provenance — keyless cosign attest (#622)", () => {
@@ -40,6 +40,19 @@
40
40
  * `requireTool` throws `ToolNotAvailableError` with an actionable message if
41
41
  * `cosign` is absent, and no test here ever spawns a real process or talks
42
42
  * to Rekor/Fulcio.
43
+ *
44
+ * **#1943 extension (`run-agent` interop).** `BuildProvenanceStatementInput`
45
+ * gained two optional merge fields, `externalParameters`/`internalParameters`
46
+ * — additive only, every existing call site's statement is byte-identical
47
+ * without them. `run-agent` (./run-agent.ts's `buildRunAgentProvenanceStatement`)
48
+ * is the first caller to use them, plus `buildType` (already overridable,
49
+ * unchanged here) set to a `run-agent`-specific recipe URI. `predicateType`
50
+ * stays `"https://slsa.dev/provenance/v1"` for every caller, `run-agent`
51
+ * included — see ./run-agent.ts's own doc comment ("Attestation interop")
52
+ * for why a turn is attested as an SLSA-shaped provenance predicate rather
53
+ * than under a minted `run-agent`-specific `predicateType`, and why that
54
+ * decision means `sign`/`attestProvenance`/`../verbs/verify.ts` needed zero
55
+ * changes to their `cosign` invocations to attest/verify a turn.
43
56
  */
44
57
 
45
58
  import type { Capability } from "../capability";
@@ -235,6 +248,23 @@ export interface BuildProvenanceStatementInput {
235
248
  finishedOn?: string;
236
249
  /** CI invocation/run id, when available (e.g. a GitHub Actions run id). */
237
250
  invocationId?: string;
251
+ /**
252
+ * Additional `buildDefinition.externalParameters` merged in alongside the
253
+ * `{ sourceRef }` default (#1943: `run-agent`'s provenance statement folds
254
+ * in the fountain-supplied *declared* facts — the agent selector today,
255
+ * more once fountain `Agent` resolution lands — without run-agent needing
256
+ * its own predicate builder). `sourceRef` is reserved by this function: a
257
+ * caller-supplied `sourceRef` is overridden, never signed; any other key
258
+ * name is the caller's choice.
259
+ */
260
+ externalParameters?: Record<string, unknown>;
261
+ /**
262
+ * Additional `buildDefinition.internalParameters` merged in alongside the
263
+ * `{ artifactDigest }` default (#1943: `run-agent` folds in what actually
264
+ * happened — sprite id, checkpoint id, turn status/exit code — the
265
+ * *imperative* facts a build's `internalParameters` are for).
266
+ */
267
+ internalParameters?: Record<string, unknown>;
238
268
  }
239
269
 
240
270
  const DEFAULT_BUILD_TYPE = "https://chant.dev/build-archive/v1";
@@ -263,8 +293,10 @@ export function buildProvenanceStatement(input: BuildProvenanceStatementInput):
263
293
  predicate: {
264
294
  buildDefinition: {
265
295
  buildType: input.buildType ?? DEFAULT_BUILD_TYPE,
266
- externalParameters: { sourceRef: input.provenance.sourceRef },
267
- internalParameters: { artifactDigest: input.provenance.artifactDigest },
296
+ // Reserved keys spread LAST so a caller-supplied key can never clobber
297
+ // them into a signed statement that disagrees with resolvedDependencies.
298
+ externalParameters: { ...input.externalParameters, sourceRef: input.provenance.sourceRef },
299
+ internalParameters: { ...input.internalParameters, artifactDigest: input.provenance.artifactDigest },
268
300
  resolvedDependencies: [{ uri: input.provenance.sourceRef, digest: { sha256: digest } }],
269
301
  },
270
302
  runDetails: {
package/src/composite.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { isDeclarable, type Declarable } from "./declarable";
2
- import { setProvenance } from "./provenance";
2
+ import { setPathProvenance, setProvenance } from "./provenance";
3
3
 
4
4
  /**
5
5
  * Marker symbol for Composite type identification.
@@ -150,10 +150,16 @@ export function Composite<P, M extends CompositeFactoryMembers = CompositeMember
150
150
  /**
151
151
  * Expands a CompositeInstance into a flat Map of prefixed entity names to Declarables.
152
152
  * Handles nested composites recursively.
153
+ *
154
+ * @param instanceName - The composite INSTANCE every member belongs to (chant
155
+ * #1443), which at the top call is the export name and stays that name
156
+ * through nesting — `prefix` grows per member and is not an instance
157
+ * identity. Defaults to `prefix` so the top call needs no extra argument.
153
158
  */
154
159
  export function expandComposite(
155
160
  prefix: string,
156
161
  instance: CompositeInstance,
162
+ instanceName: string = prefix,
157
163
  ): Map<string, Declarable> {
158
164
  const result = new Map<string, Declarable>();
159
165
  const shared = (instance as unknown as Record<symbol, unknown>)[SHARED_PROPS] as Record<string, unknown> | undefined;
@@ -164,7 +170,7 @@ export function expandComposite(
164
170
  const fullName = `${prefix}${memberName[0].toUpperCase()}${memberName.slice(1)}`;
165
171
 
166
172
  if (isCompositeInstance(member)) {
167
- const nested = expandComposite(fullName, member);
173
+ const nested = expandComposite(fullName, member, instanceName);
168
174
  for (const [nestedName, nestedEntity] of nested) {
169
175
  // Inner composite already stamped; `??=` keeps the most-specific one.
170
176
  if (compositeName) setProvenance(nestedEntity, { composite: compositeName });
@@ -176,6 +182,15 @@ export function expandComposite(
176
182
  }
177
183
  }
178
184
 
185
+ // Whole-entity origin (chant #1443). Recorded after the loop so a nested
186
+ // member carries its INNER composite here — the recursive call above already
187
+ // ran and `setPathProvenance` keeps the first writer.
188
+ if (compositeName) {
189
+ for (const entity of result.values()) {
190
+ setPathProvenance(entity, "", { kind: "composite", composite: compositeName, instance: instanceName });
191
+ }
192
+ }
193
+
179
194
  if (shared) {
180
195
  for (const entity of result.values()) {
181
196
  if ("props" in entity) {
@@ -211,6 +226,20 @@ export function expandComposite(
211
226
  Object.defineProperty(entity, "props", {
212
227
  value: merged, enumerable: false, configurable: true,
213
228
  });
229
+ // A key the member did not set is the propagating instance's alone, so
230
+ // it is attributable exactly (chant #1443). A key both sides wrote —
231
+ // an override, or the array concatenation above — is left to the
232
+ // whole-entity origin rather than credited to one contributor.
233
+ if (compositeName) {
234
+ for (const k of Object.keys(shared)) {
235
+ if (shared[k] === undefined || existing[k] !== undefined) continue;
236
+ setPathProvenance(entity, k, {
237
+ kind: "composite",
238
+ composite: compositeName,
239
+ instance: instanceName,
240
+ });
241
+ }
242
+ }
214
243
  }
215
244
  }
216
245
  }
@@ -34,6 +34,15 @@ function stackPrefix(file: string, buildRoot: string | undefined): string {
34
34
  return segments.map((s) => s.charAt(0).toUpperCase() + s.slice(1)).join("");
35
35
  }
36
36
 
37
+ /**
38
+ * Whole-entity origin for a declarable a file exported directly (chant #1443).
39
+ * A composite's members get theirs from `expandComposite`, which knows the
40
+ * composite and the instance; a direct export has nothing between the source
41
+ * file and the resource. Merged key-by-key, so a path the fold path already
42
+ * attributed to a build parameter is not overwritten.
43
+ */
44
+ const AUTHORED_ROOT: EntityProvenance["paths"] = { "": { kind: "authored" } };
45
+
37
46
  /** One entity to be placed into the map, produced by {@link enumerateEntries}. */
38
47
  interface PendingEntry {
39
48
  /** The un-disambiguated map key this entity would take (export name, indexed
@@ -73,12 +82,12 @@ function enumerateEntries(
73
82
  for (const [rawName, value] of sortedExports) {
74
83
  const name = exportKey(rawName, file);
75
84
  if (isDeclarable(value)) {
76
- entries.push({ bareKey: name, value, file, provenance: { sourceFile: file } });
85
+ entries.push({ bareKey: name, value, file, provenance: { sourceFile: file, paths: AUTHORED_ROOT } });
77
86
  } else if (Array.isArray(value)) {
78
87
  for (let i = 0; i < value.length; i++) {
79
88
  const item = value[i];
80
89
  if (isDeclarable(item)) {
81
- entries.push({ bareKey: `${name}_${i}`, value: item, file, provenance: { sourceFile: file } });
90
+ entries.push({ bareKey: `${name}_${i}`, value: item, file, provenance: { sourceFile: file, paths: AUTHORED_ROOT } });
82
91
  } else if (isCompositeInstance(item)) {
83
92
  const indexedName = `${name}_${i}`;
84
93
  for (const [expandedName, entity] of expandComposite(indexedName, item)) {
@@ -9,6 +9,7 @@ import { isCompositeInstance } from "../composite";
9
9
  import { isAttrRefLike } from "../utils";
10
10
  import { isIntrinsic } from "../intrinsic";
11
11
  import { params as sharedParams } from "../params";
12
+ import { getPathProvenance } from "../provenance";
12
13
  import type { AttrRef } from "../attrref";
13
14
  import type { IntrinsicDef } from "../lexicon";
14
15
 
@@ -1153,6 +1154,59 @@ describe("tryFoldFile — build-time parameters (chant #1064)", () => {
1153
1154
 
1154
1155
  setBuildParams({});
1155
1156
  });
1157
+
1158
+ // chant #1443 — fold substitutes `params.<name>` away, so the emitted value
1159
+ // is a literal and unattributable. These record which parameters the
1160
+ // AUTHORED expression read, before that happens.
1161
+ test("a folded resource records which build parameters each of its paths reads", async () => {
1162
+ await writeResourceDefs();
1163
+ const file = join(testDir, "main.ts");
1164
+ await writeFile(
1165
+ file,
1166
+ `
1167
+ import { Bucket } from "./resources";
1168
+ import { params } from ${JSON.stringify(paramsPath)};
1169
+ const suffix = params.env === "prod" ? "" : "-dev";
1170
+ export const bucket = new Bucket({
1171
+ name: \`assets\${suffix}\`,
1172
+ fixed: "never-moves",
1173
+ nested: { region: params.region },
1174
+ });
1175
+ `,
1176
+ );
1177
+
1178
+ const session = createFoldSession([], { env: "prod", region: "us-east-1" });
1179
+ const result = await tryFoldFile(file, [], session);
1180
+
1181
+ expect(result.ok).toBe(true);
1182
+ if (!result.ok) return;
1183
+ const [, entity] = result.entities[0];
1184
+ expect(getPathProvenance(entity as object)).toEqual({
1185
+ name: { kind: "build-param", params: ["env"] },
1186
+ "nested.region": { kind: "build-param", params: ["region"] },
1187
+ });
1188
+ });
1189
+
1190
+ test("a folded resource that reads no parameter records no path origins", async () => {
1191
+ await writeResourceDefs();
1192
+ const file = join(testDir, "main.ts");
1193
+ await writeFile(
1194
+ file,
1195
+ `
1196
+ import { Bucket } from "./resources";
1197
+ import { params } from ${JSON.stringify(paramsPath)};
1198
+ export const other = params.env;
1199
+ export const bucket = new Bucket({ name: "static" });
1200
+ `,
1201
+ );
1202
+
1203
+ const result = await tryFoldFile(file, [], createFoldSession([], { env: "prod" }));
1204
+
1205
+ expect(result.ok).toBe(true);
1206
+ if (!result.ok) return;
1207
+ const entity = result.entities.find(([name]) => name === "bucket")![1];
1208
+ expect(getPathProvenance(entity as object)).toBeUndefined();
1209
+ });
1156
1210
  });
1157
1211
 
1158
1212
  /**
@@ -22,6 +22,7 @@ import {
22
22
  type FoldedIntrinsic,
23
23
  type FoldedHelperCall,
24
24
  type SymbolicValue,
25
+ type FoldedCompositeStepCall,
25
26
  } from "../fold/fold";
26
27
  import { isChantOwnedSpecifier, isFoldableHelperName } from "../fold/foldable-helpers";
27
28
  import {
@@ -37,7 +38,9 @@ import {
37
38
  SUPPORTED_UNARY_OPERATORS,
38
39
  } from "../fold/subset";
39
40
  import { importModule } from "./import";
40
- import type { IntrinsicDef } from "../lexicon";
41
+ import { collectParamDependencies } from "./param-deps";
42
+ import { setPathProvenance } from "../provenance";
43
+ import { intrinsicCallFoldsEagerly, type IntrinsicDef } from "../lexicon";
41
44
  import type { BuildParamValue } from "../build-params";
42
45
 
43
46
  /**
@@ -1096,19 +1099,25 @@ function activeLexiconPackage(specifier: string, lexiconPackages: ReadonlySet<st
1096
1099
  * and intrinsic functions; the only thing new is that a plain DATA export is
1097
1100
  * now reachable too.
1098
1101
  *
1099
- * Callable exports are deliberately excluded. A lexicon's functions — its
1102
+ * Callable exports are otherwise excluded. A lexicon's functions — its
1100
1103
  * resource classes, composite factories, intrinsic implementations — already
1101
1104
  * have dedicated resolution paths that know how to INVOKE them
1102
1105
  * (`resolveResourceEntity`, `resolveCallExpression`, `reviveFoldedValue`), and
1103
- * binding them as plain identifier values here would widen what folds in ways
1104
- * this issue neither needs nor measured: none of the references #1063 exists
1105
- * to unblock (`Azure`, `GCP`, `S3Actions`, gitlab's `CI`) is a function.
1106
+ * binding an arbitrary one as a plain identifier value here would widen what
1107
+ * folds in unmeasured ways. The one exception (chant #1966) is a function its
1108
+ * lexicon registered with {@link IntrinsicDef.foldsEagerly} — the same
1109
+ * per-name, lexicon-declared opt-in `foldsAsCall` is for an intrinsic's
1110
+ * envelope-and-revive form, admitting this one because `fold()`
1111
+ * (../fold/fold.ts) needs to CALL it eagerly — synchronously, before
1112
+ * revival — to support a lexicon's own string-building helpers used inline
1113
+ * (`matrix("os").toString()`, `` `${inputs("x")}` ``), where an envelope
1114
+ * would still be a plain object when the enclosing template folds.
1106
1115
  *
1107
1116
  * Returns `undefined` — never throws — for a package that isn't an active
1108
- * lexicon, an export the package doesn't have, a callable export, or an
1109
- * import that fails. The binding is then simply absent from `externals`,
1110
- * exactly as before, and `fold()`'s ordinary "unresolved identifier" failure
1111
- * still fires if the name is actually referenced.
1117
+ * lexicon, an export the package doesn't have, a non-admitted callable
1118
+ * export, or an import that fails. The binding is then simply absent from
1119
+ * `externals`, exactly as before, and `fold()`'s ordinary "unresolved
1120
+ * identifier" failure still fires if the name is actually referenced.
1112
1121
  */
1113
1122
  async function resolveActiveLexiconExport(
1114
1123
  binding: ImportBinding,
@@ -1133,7 +1142,10 @@ async function resolveActiveLexiconExport(
1133
1142
 
1134
1143
  if (!(binding.imported in mod)) return undefined;
1135
1144
  const value = mod[binding.imported];
1136
- if (typeof value === "function") return undefined;
1145
+ if (typeof value === "function") {
1146
+ const eager = session.intrinsics.some((i) => i.name === binding.imported && intrinsicCallFoldsEagerly(i));
1147
+ if (!eager) return undefined;
1148
+ }
1137
1149
  return { value };
1138
1150
  }
1139
1151
 
@@ -1415,19 +1427,19 @@ async function resolveImportedCall(
1415
1427
  }
1416
1428
 
1417
1429
  /**
1418
- * Arm 2 — import the module the callee came from and call it, in this process.
1419
- * Unchanged from #1022 in both behavior and ORDER (refusal, resolve, import,
1420
- * callable check, arguments, call); #1023 only factored it out so the
1421
- * interpretation arm above can fall back into it.
1422
- *
1423
- * @param args - Already-evaluated arguments, when the caller has them.
1430
+ * Arm 2 — import the module the callee came from and call it, in this
1431
+ * process, given ALREADY-RESOLVED argument values. Factored out of
1432
+ * {@link invokeImportedCallee} (chant #1174) so a caller with no raw
1433
+ * `ts.CallExpression` node — {@link resolveCompositeCall}, the nested-value
1434
+ * counterpart to {@link resolveCallExpression} — can reach the identical
1435
+ * refusal/resolve/import/callable-check/call sequence #1022 established,
1436
+ * without inventing a second copy of it.
1424
1437
  */
1425
- async function invokeImportedCallee(
1426
- node: ts.CallExpression,
1438
+ async function invokeResolvedCallee(
1427
1439
  calleeName: string,
1428
1440
  binding: ImportBinding,
1429
1441
  ctx: ResolveCtx,
1430
- args?: readonly unknown[],
1442
+ args: readonly unknown[],
1431
1443
  ): Promise<unknown> {
1432
1444
  // chant #1093 — THE gap this check exists for. Invoking the callee runs
1433
1445
  // project code (the factory body, and its whole module's top level) in the
@@ -1473,12 +1485,67 @@ async function invokeImportedCallee(
1473
1485
  throw cheapError(`"${binding.imported}" from "${binding.specifier}" is not a function`);
1474
1486
  }
1475
1487
 
1476
- const callArgs = args ?? (await resolveCallArguments(node, calleeName, ctx));
1477
-
1478
1488
  executionCounts.factoryInvocations += 1;
1479
1489
  if (isProjectFileSpecifier(binding.specifier)) executionCounts.projectFactoryInvocations += 1;
1480
1490
 
1481
- return (Fn as (...fnArgs: unknown[]) => unknown)(...callArgs);
1491
+ return (Fn as (...fnArgs: unknown[]) => unknown)(...args);
1492
+ }
1493
+
1494
+ /**
1495
+ * Arm 2's original entry point — a raw call-expression node in hand. Resolves
1496
+ * arguments (unless the caller already has them) and delegates to
1497
+ * {@link invokeResolvedCallee}. Unchanged from #1022 in both behavior and
1498
+ * ORDER (refusal, resolve, import, callable check, arguments, call); #1023
1499
+ * factored the body out once already, #1174 a second time.
1500
+ *
1501
+ * @param args - Already-evaluated arguments, when the caller has them.
1502
+ */
1503
+ async function invokeImportedCallee(
1504
+ node: ts.CallExpression,
1505
+ calleeName: string,
1506
+ binding: ImportBinding,
1507
+ ctx: ResolveCtx,
1508
+ args?: readonly unknown[],
1509
+ ): Promise<unknown> {
1510
+ const callArgs = args ?? (await resolveCallArguments(node, calleeName, ctx));
1511
+ return invokeResolvedCallee(calleeName, binding, ctx, callArgs);
1512
+ }
1513
+
1514
+ /**
1515
+ * Resolve a composite-factory call used as a NESTED value — chant #1174's
1516
+ * `<Identifier>(...).step` idiom ({@link FoldedCompositeStepCall}) — given
1517
+ * its callee NAME and already-REVIVED argument values. The nested-value
1518
+ * counterpart to {@link resolveCallExpression}, which the top-level
1519
+ * declarator path (`export const x = Checkout({...})`) already used: the
1520
+ * raw `ts.CallExpression` node isn't needed here because
1521
+ * {@link resolveInterpretableFactory} only needs the import `binding`, and
1522
+ * {@link interpretCompositeFactory}/{@link invokeResolvedCallee} only need
1523
+ * the resolved argument values — which `fold()`'s envelope already folded
1524
+ * and {@link reviveFoldedValue} has already revived by the time this runs.
1525
+ *
1526
+ * Same two arms, same order, as the top-level path: interpret a project-file
1527
+ * registered `Composite` (chant #1023) when its body qualifies, otherwise
1528
+ * import and invoke for real — which is the arm every lexicon-package
1529
+ * composite (`Checkout`, `SetupNode`, …) always takes, exactly as it does
1530
+ * today for a top-level `export const x = Checkout({...})`. An unbound
1531
+ * callee name throws the same "function call as a value" message `fold()`
1532
+ * itself would have, for a caller with no `FoldedCompositeStepCall` special
1533
+ * case to fall into.
1534
+ */
1535
+ async function resolveCompositeCall(calleeName: string, args: readonly unknown[], ctx: ResolveCtx): Promise<unknown> {
1536
+ const binding = ctx.imports.get(calleeName);
1537
+ if (!binding) throw cheapError(`unresolved identifier: ${calleeName}`);
1538
+
1539
+ const factory = await resolveInterpretableFactory(binding, ctx);
1540
+ if (factory) {
1541
+ const interpreted = await interpretCompositeFactory(factory, args, ctx);
1542
+ if (interpreted) return interpreted.value;
1543
+ // Declined — falls through to arm 2 with the SAME arguments, so a nested
1544
+ // composite-call argument (or a live cross-file reference) is not
1545
+ // resolved a second time. Identical discipline to `resolveImportedCall`.
1546
+ }
1547
+
1548
+ return invokeResolvedCallee(calleeName, binding, ctx, args);
1482
1549
  }
1483
1550
 
1484
1551
  /**
@@ -2502,6 +2569,24 @@ async function reviveFoldedValue(value: FoldedValue, ctx: ResolveCtx, requireLiv
2502
2569
  return reviveHelperCall(value as FoldedHelperCall, ctx);
2503
2570
  }
2504
2571
 
2572
+ if ("__compositeStep" in value) {
2573
+ // chant #1174 — `<Identifier>(...).step`, see {@link FoldedCompositeStepCall}.
2574
+ // Arguments revive with `requireLiveRefs: false` — the same rule
2575
+ // `resolveCallArguments` applies to a NON-helper callee's arguments
2576
+ // (a composite factory stores its props, it doesn't inspect them the way
2577
+ // an intrinsic/helper does), so a `{__attrRef}` among them stays the
2578
+ // symbolic envelope the composite's own resource construction resolves
2579
+ // by name, exactly as a top-level resource's props would.
2580
+ const call = value as FoldedCompositeStepCall;
2581
+ const revivedArgs: unknown[] = [];
2582
+ for (const a of call.args) revivedArgs.push(await reviveFoldedValue(a, ctx, false));
2583
+ const result = await resolveCompositeCall(call.__compositeStep, revivedArgs, ctx);
2584
+ if (!isIndexableObject(result)) {
2585
+ throw cheapError(`composite call \`${call.__compositeStep}(...)\` did not resolve to an object with a "step" member`);
2586
+ }
2587
+ return (result as Record<string, unknown>).step;
2588
+ }
2589
+
2505
2590
  if ("__attrRef" in value) {
2506
2591
  if (requireLiveRefs) {
2507
2592
  throw cheapError(
@@ -2889,6 +2974,46 @@ async function constructFoldedResource(
2889
2974
  * performs unconditionally for the same file, and through the same
2890
2975
  * already-memoized `importModule`.
2891
2976
  */
2977
+ /**
2978
+ * The local names this file bound to the build's parameter object (chant
2979
+ * #1443). Object identity against {@link FoldSession.buildParams}, which
2980
+ * `buildExternals` substituted directly, so an unrelated import that happens to
2981
+ * be called `params` is not mistaken for it.
2982
+ */
2983
+ function paramLocalNames(ctx: ResolveCtx): Set<string> {
2984
+ const out = new Set<string>();
2985
+ const buildParams = ctx.session.buildParams;
2986
+ if (!buildParams) return out;
2987
+ for (const [name, value] of ctx.externals) {
2988
+ if (value === buildParams) out.add(name);
2989
+ }
2990
+ return out;
2991
+ }
2992
+
2993
+ /**
2994
+ * chant #1443 — record which build parameters each of a resource's authored
2995
+ * property expressions reads, before fold substitutes them away. Best-effort
2996
+ * and additive: a file with no `params` import, or a constructor called with no
2997
+ * object literal, records nothing.
2998
+ */
2999
+ function stampParamDependencies(entity: unknown, node: ts.NewExpression, ctx: ResolveCtx): void {
3000
+ if (typeof entity !== "object" || entity === null) return;
3001
+ const paramLocals = paramLocalNames(ctx);
3002
+ if (paramLocals.size === 0) return;
3003
+ // The same argument `foldResource` treats as props: the first object literal.
3004
+ let propsArg: ts.ObjectLiteralExpression | undefined;
3005
+ for (const argument of node.arguments ?? []) {
3006
+ if (ts.isObjectLiteralExpression(argument)) {
3007
+ propsArg = argument;
3008
+ break;
3009
+ }
3010
+ }
3011
+ if (!propsArg) return;
3012
+ for (const [path, origin] of Object.entries(collectParamDependencies(propsArg, ctx.consts, paramLocals))) {
3013
+ setPathProvenance(entity, path, origin);
3014
+ }
3015
+ }
3016
+
2892
3017
  async function preresolveResourceConsts(ctx: ResolveCtx): Promise<Map<ts.Expression, unknown>> {
2893
3018
  const built = new Map<ts.Expression, unknown>();
2894
3019
  for (const [name, initializer] of ctx.consts) {
@@ -2896,6 +3021,7 @@ async function preresolveResourceConsts(ctx: ResolveCtx): Promise<Map<ts.Express
2896
3021
  try {
2897
3022
  const spec = foldResource(initializer, ctx.consts, ctx.intrinsics, ctx.externals);
2898
3023
  const instance = await constructFoldedResource(spec, ctx, false);
3024
+ stampParamDependencies(instance, initializer, ctx);
2899
3025
  built.set(initializer, instance);
2900
3026
  ctx.externals.set(name, instance);
2901
3027
  } catch {
@@ -2933,6 +3059,7 @@ async function resolveResourceEntity(
2933
3059
 
2934
3060
  try {
2935
3061
  const entity = (await instantiateFoldedResource(spec.__resource, ctorArgs, ctx, ` for "${name}"`)) as Declarable;
3062
+ stampParamDependencies(entity, node, ctx);
2936
3063
  return { ok: true, entity };
2937
3064
  } catch (err) {
2938
3065
  return { ok: false, reason: err instanceof Error ? err.message : String(err) };
@@ -3552,22 +3679,21 @@ export async function tryFoldFile(
3552
3679
  * so the exact same forced-taint safety net still applies to that remaining
3553
3680
  * boundary, unchanged.
3554
3681
  */
3555
- export async function planFoldTaint(
3556
- files: readonly string[],
3557
- wouldFold: ReadonlyMap<string, boolean>,
3558
- liveSources?: ReadonlyMap<string, ReadonlySet<string>>,
3559
- ): Promise<Set<string>> {
3682
+ /**
3683
+ * file -> set of OTHER files in `files` that it relatively imports OR
3684
+ * re-exports from, regardless of that file's own fold outcome. Extracted
3685
+ * from {@link planFoldTaint} (chant #1083) so a second consumer — the fold
3686
+ * blocker dominator ranking (`./fold-rank.ts`) — can walk the exact same
3687
+ * project-file edges instead of re-parsing every file with its own
3688
+ * specifier-resolution logic. chant #1020 — this counts a namespace import
3689
+ * (`import * as ns from "./x"`, previously skipped — see `collectImports`)
3690
+ * and a re-export (`export { x } from "./y"`, which `collectImports` never
3691
+ * saw at all, since it only looks at `import` declarations): both are real
3692
+ * cross-file value dependencies fold's own resolution follows, so both need
3693
+ * the same edge an ordinary named import already gets.
3694
+ */
3695
+ export async function buildProjectImportEdges(files: readonly string[]): Promise<Map<string, Set<string>>> {
3560
3696
  const fileSet = new Set(files);
3561
-
3562
- // file -> set of OTHER discovered files it relatively imports OR re-exports
3563
- // from, regardless of that file's own fold outcome (taint needs the full
3564
- // edge set to propagate transitively). chant #1020 — this now ALSO counts
3565
- // a namespace import (`import * as ns from "./x"`, previously skipped —
3566
- // see `collectImports`) and a re-export (`export { x } from "./y"`, which
3567
- // `collectImports` never saw at all, since it only looks at `import`
3568
- // declarations): both are real cross-file value dependencies my own new
3569
- // resolution follows, so both need the same identity guarantee an
3570
- // ordinary named import already got.
3571
3697
  const edges = new Map<string, Set<string>>();
3572
3698
  for (const file of files) {
3573
3699
  const targets = new Set<string>();
@@ -3600,10 +3726,24 @@ export async function planFoldTaint(
3600
3726
  } catch {
3601
3727
  // Unreadable/unparseable — no edges contributed; tryFoldFile's own
3602
3728
  // top-level catch already turns this into a "run" decision for the
3603
- // file itself, which is enough to seed it as tainted below.
3729
+ // file itself, which is enough to seed it as tainted below (or, for
3730
+ // the dominator ranking, leaves it a childless node).
3604
3731
  }
3605
3732
  edges.set(file, targets);
3606
3733
  }
3734
+ return edges;
3735
+ }
3736
+
3737
+ export async function planFoldTaint(
3738
+ files: readonly string[],
3739
+ wouldFold: ReadonlyMap<string, boolean>,
3740
+ liveSources?: ReadonlyMap<string, ReadonlySet<string>>,
3741
+ ): Promise<Set<string>> {
3742
+ const fileSet = new Set(files);
3743
+
3744
+ // file -> set of OTHER discovered files it relatively imports OR re-exports
3745
+ // from — see {@link buildProjectImportEdges}.
3746
+ const edges = await buildProjectImportEdges(files);
3607
3747
 
3608
3748
  // chant #1044 — reverse edges: consumed-file -> the folded files that
3609
3749
  // captured its objects. Same taint set, same fixpoint walk; see this