@intentius/chant 0.124.44 → 0.124.46

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 (249) hide show
  1. package/dist/capabilities/aws.paths +148 -0
  2. package/dist/capabilities/base-mappings.json +122 -0
  3. package/dist/capabilities/catalog.d.ts +89 -0
  4. package/dist/capabilities/catalog.d.ts.map +1 -0
  5. package/dist/capabilities/catalog.js +204 -0
  6. package/dist/capabilities/language.d.ts +103 -0
  7. package/dist/capabilities/language.d.ts.map +1 -0
  8. package/dist/capabilities/language.js +335 -0
  9. package/dist/capabilities/matcher.d.ts +65 -0
  10. package/dist/capabilities/matcher.d.ts.map +1 -0
  11. package/dist/capabilities/matcher.js +299 -0
  12. package/dist/capabilities/view.d.ts +207 -0
  13. package/dist/capabilities/view.d.ts.map +1 -0
  14. package/dist/capabilities/view.js +484 -0
  15. package/dist/ci-identity.d.ts +183 -12
  16. package/dist/ci-identity.d.ts.map +1 -1
  17. package/dist/ci-identity.js +362 -53
  18. package/dist/cli/commands/lint.d.ts.map +1 -1
  19. package/dist/cli/commands/lint.js +1 -0
  20. package/dist/cli/handlers/build.d.ts.map +1 -1
  21. package/dist/cli/handlers/build.js +17 -1
  22. package/dist/cli/handlers/declared-graph.d.ts +44 -0
  23. package/dist/cli/handlers/declared-graph.d.ts.map +1 -0
  24. package/dist/cli/handlers/declared-graph.js +87 -0
  25. package/dist/cli/handlers/graph.d.ts +25 -0
  26. package/dist/cli/handlers/graph.d.ts.map +1 -1
  27. package/dist/cli/handlers/graph.js +93 -87
  28. package/dist/cli/handlers/pr-env.d.ts +8 -2
  29. package/dist/cli/handlers/pr-env.d.ts.map +1 -1
  30. package/dist/cli/handlers/pr-env.js +127 -225
  31. package/dist/cli/handlers/pr.d.ts +3 -0
  32. package/dist/cli/handlers/pr.d.ts.map +1 -1
  33. package/dist/cli/handlers/pr.js +1 -0
  34. package/dist/cli/handlers/search-count.d.ts +89 -0
  35. package/dist/cli/handlers/search-count.d.ts.map +1 -0
  36. package/dist/cli/handlers/search-count.js +132 -0
  37. package/dist/cli/handlers/search.d.ts +358 -6
  38. package/dist/cli/handlers/search.d.ts.map +1 -1
  39. package/dist/cli/handlers/search.js +1065 -155
  40. package/dist/cli/main.d.ts.map +1 -1
  41. package/dist/cli/main.js +23 -3
  42. package/dist/cli/mcp/server.d.ts.map +1 -1
  43. package/dist/cli/mcp/server.js +3 -0
  44. package/dist/cli/mcp/tools/capabilities.d.ts +7 -0
  45. package/dist/cli/mcp/tools/capabilities.d.ts.map +1 -0
  46. package/dist/cli/mcp/tools/capabilities.js +27 -0
  47. package/dist/cli/mcp/workspace-tools.d.ts +3 -1
  48. package/dist/cli/mcp/workspace-tools.d.ts.map +1 -1
  49. package/dist/cli/mcp/workspace-tools.js +47 -2
  50. package/dist/cli/parse-args.d.ts.map +1 -1
  51. package/dist/cli/parse-args.js +64 -17
  52. package/dist/cli/registry.d.ts +22 -0
  53. package/dist/cli/registry.d.ts.map +1 -1
  54. package/dist/components/cli-support.d.ts +2 -0
  55. package/dist/components/cli-support.d.ts.map +1 -1
  56. package/dist/components/cli-support.js +4 -2
  57. package/dist/components/fan-out-support.d.ts +3 -0
  58. package/dist/components/fan-out-support.d.ts.map +1 -1
  59. package/dist/components/fan-out-support.js +1 -1
  60. package/dist/components/pr-env-ports.d.ts +29 -0
  61. package/dist/components/pr-env-ports.d.ts.map +1 -0
  62. package/dist/components/pr-env-ports.js +77 -0
  63. package/dist/components/pr-env-run.d.ts +254 -0
  64. package/dist/components/pr-env-run.d.ts.map +1 -0
  65. package/dist/components/pr-env-run.js +376 -0
  66. package/dist/components/pr-environment.d.ts +91 -15
  67. package/dist/components/pr-environment.d.ts.map +1 -1
  68. package/dist/components/pr-environment.js +95 -9
  69. package/dist/components/pr-pipeline.d.ts +10 -1
  70. package/dist/components/pr-pipeline.d.ts.map +1 -1
  71. package/dist/config-environments.d.ts +92 -0
  72. package/dist/config-environments.d.ts.map +1 -0
  73. package/dist/config-environments.js +102 -0
  74. package/dist/config.d.ts +24 -84
  75. package/dist/config.d.ts.map +1 -1
  76. package/dist/config.js +9 -96
  77. package/dist/graph-detail.d.ts.map +1 -1
  78. package/dist/graph-detail.js +15 -3
  79. package/dist/graph-effective.d.ts +33 -2
  80. package/dist/graph-effective.d.ts.map +1 -1
  81. package/dist/graph-effective.js +215 -68
  82. package/dist/graph-ir.d.ts +23 -0
  83. package/dist/graph-ir.d.ts.map +1 -1
  84. package/dist/graph-ir.js +130 -0
  85. package/dist/graph-sub-template.d.ts +11 -0
  86. package/dist/graph-sub-template.d.ts.map +1 -0
  87. package/dist/graph-sub-template.js +19 -0
  88. package/dist/lexicon.d.ts +9 -0
  89. package/dist/lexicon.d.ts.map +1 -1
  90. package/dist/lifecycle/observe.d.ts +28 -0
  91. package/dist/lifecycle/observe.d.ts.map +1 -1
  92. package/dist/lifecycle/observe.js +23 -1
  93. package/dist/lint/rule.d.ts +6 -0
  94. package/dist/lint/rule.d.ts.map +1 -1
  95. package/dist/live-endpoint.d.ts +1 -1
  96. package/dist/live-endpoint.d.ts.map +1 -1
  97. package/dist/live-endpoint.js +1 -1
  98. package/dist/observation.d.ts +48 -1
  99. package/dist/observation.d.ts.map +1 -1
  100. package/dist/observation.js +21 -5
  101. package/dist/op/activities/activity-contracts.d.ts +4 -4
  102. package/dist/op/op-waves.d.ts +17 -0
  103. package/dist/op/op-waves.d.ts.map +1 -1
  104. package/dist/op/op-waves.js +23 -1
  105. package/dist/pr-forge.d.ts +15 -1
  106. package/dist/pr-forge.d.ts.map +1 -1
  107. package/dist/pr-forge.js +28 -16
  108. package/dist/pr-requires.d.ts +5 -1
  109. package/dist/pr-requires.d.ts.map +1 -1
  110. package/dist/wave-runtime.mjs +251 -250
  111. package/dist/workspace/capabilities-contract.d.ts +66 -0
  112. package/dist/workspace/capabilities-contract.d.ts.map +1 -0
  113. package/dist/workspace/capabilities-contract.js +107 -0
  114. package/dist/workspace/capabilities.schema.json +229 -0
  115. package/dist/workspace/ci-cloud-identity.d.ts +3 -1
  116. package/dist/workspace/ci-cloud-identity.d.ts.map +1 -1
  117. package/dist/workspace/ci-cloud-identity.js +16 -1
  118. package/dist/workspace/ci-run.d.ts +35 -8
  119. package/dist/workspace/ci-run.d.ts.map +1 -1
  120. package/dist/workspace/ci-run.js +95 -21
  121. package/dist/workspace/ci-secrets.d.ts.map +1 -1
  122. package/dist/workspace/ci-secrets.js +4 -3
  123. package/dist/workspace/compose-graph.d.ts +9 -0
  124. package/dist/workspace/compose-graph.d.ts.map +1 -1
  125. package/dist/workspace/compose-graph.js +3 -1
  126. package/dist/workspace/conformance/index.d.ts +1 -1
  127. package/dist/workspace/conformance/index.d.ts.map +1 -1
  128. package/dist/workspace/conformance/index.js +39 -2
  129. package/dist/workspace/declaration.d.ts +10 -0
  130. package/dist/workspace/declaration.d.ts.map +1 -1
  131. package/dist/workspace/declaration.js +17 -1
  132. package/dist/workspace/declaration.schema.json +36 -1
  133. package/dist/workspace/graph-cache.d.ts +8 -0
  134. package/dist/workspace/graph-cache.d.ts.map +1 -1
  135. package/dist/workspace/graph-cache.js +27 -3
  136. package/dist/workspace/graph-cli.d.ts +39 -2
  137. package/dist/workspace/graph-cli.d.ts.map +1 -1
  138. package/dist/workspace/graph-cli.js +66 -24
  139. package/dist/workspace/graph.schema.json +148 -0
  140. package/dist/workspace/kind-readers.d.ts +3 -0
  141. package/dist/workspace/kind-readers.d.ts.map +1 -1
  142. package/dist/workspace/kind-readers.js +7 -1
  143. package/dist/workspace/member-commands.d.ts +10 -1
  144. package/dist/workspace/member-commands.d.ts.map +1 -1
  145. package/dist/workspace/member-commands.js +17 -1
  146. package/dist/workspace/nested-graph.d.ts +2 -0
  147. package/dist/workspace/nested-graph.d.ts.map +1 -1
  148. package/dist/workspace/nested-graph.js +32 -1
  149. package/dist/workspace/reason-codes.d.ts +10 -0
  150. package/dist/workspace/reason-codes.d.ts.map +1 -1
  151. package/dist/workspace/reason-codes.js +12 -0
  152. package/dist/workspace/search-cli.d.ts +204 -0
  153. package/dist/workspace/search-cli.d.ts.map +1 -0
  154. package/dist/workspace/search-cli.js +466 -0
  155. package/dist/workspace/search-contract.d.ts +56 -0
  156. package/dist/workspace/search-contract.d.ts.map +1 -0
  157. package/dist/workspace/search-contract.js +70 -0
  158. package/dist/workspace/search.schema.json +822 -0
  159. package/package.json +1 -1
  160. package/src/__snapshots__/okf.test.ts.snap +10 -0
  161. package/src/capabilities/aws.paths +148 -0
  162. package/src/capabilities/base-mappings.json +122 -0
  163. package/src/capabilities/catalog.ts +230 -0
  164. package/src/capabilities/language.test.ts +191 -0
  165. package/src/capabilities/language.ts +373 -0
  166. package/src/capabilities/matcher.ts +315 -0
  167. package/src/capabilities/view-compose.test.ts +76 -0
  168. package/src/capabilities/view.ts +631 -0
  169. package/src/ci-identity-targets.test.ts +275 -0
  170. package/src/ci-identity.ts +394 -61
  171. package/src/cli/commands/lint.ts +1 -0
  172. package/src/cli/handlers/build.ts +15 -1
  173. package/src/cli/handlers/declared-graph.ts +99 -0
  174. package/src/cli/handlers/graph.test.ts +94 -3
  175. package/src/cli/handlers/graph.ts +123 -96
  176. package/src/cli/handlers/pr-env.ts +135 -227
  177. package/src/cli/handlers/pr.ts +4 -0
  178. package/src/cli/handlers/search-build-roots.test.ts +144 -0
  179. package/src/cli/handlers/search-count.test.ts +191 -0
  180. package/src/cli/handlers/search-count.ts +165 -0
  181. package/src/cli/handlers/search-json.test.ts +333 -0
  182. package/src/cli/handlers/search-live.test.ts +387 -0
  183. package/src/cli/handlers/search-paths.test.ts +235 -0
  184. package/src/cli/handlers/search.ts +1312 -166
  185. package/src/cli/main.test.ts +28 -3
  186. package/src/cli/main.ts +23 -3
  187. package/src/cli/mcp/server.test.ts +3 -3
  188. package/src/cli/mcp/server.ts +3 -0
  189. package/src/cli/mcp/tools/capabilities.ts +32 -0
  190. package/src/cli/mcp/workspace-tools.e2e.test.ts +21 -1
  191. package/src/cli/mcp/workspace-tools.ts +47 -2
  192. package/src/cli/parse-args.ts +52 -13
  193. package/src/cli/registry.ts +22 -0
  194. package/src/components/cli-support.ts +6 -1
  195. package/src/components/fan-out-support.ts +4 -1
  196. package/src/components/pr-env-ports.ts +89 -0
  197. package/src/components/pr-env-run-bundle.test.ts +54 -0
  198. package/src/components/pr-env-run.test.ts +290 -0
  199. package/src/components/pr-env-run.ts +563 -0
  200. package/src/components/pr-environment.ts +161 -13
  201. package/src/components/pr-pipeline.ts +10 -1
  202. package/src/config-environments.ts +133 -0
  203. package/src/config.ts +37 -124
  204. package/src/graph-detail.ts +15 -3
  205. package/src/graph-effective.ts +226 -75
  206. package/src/graph-ir.test.ts +163 -0
  207. package/src/graph-ir.ts +147 -0
  208. package/src/graph-sub-template.ts +22 -0
  209. package/src/lexicon.ts +10 -0
  210. package/src/lifecycle/observe.test.ts +27 -1
  211. package/src/lifecycle/observe.ts +43 -1
  212. package/src/lint/rule.ts +6 -0
  213. package/src/live-endpoint.ts +1 -1
  214. package/src/observation.test.ts +25 -0
  215. package/src/observation.ts +69 -5
  216. package/src/op/op-waves.ts +38 -1
  217. package/src/pr-forge-bundle.test.ts +1 -1
  218. package/src/pr-forge.ts +44 -17
  219. package/src/pr-requires.test.ts +43 -6
  220. package/src/pr-requires.ts +5 -1
  221. package/src/workspace/capabilities-contract.test.ts +88 -0
  222. package/src/workspace/capabilities-contract.ts +132 -0
  223. package/src/workspace/capabilities.schema.json +229 -0
  224. package/src/workspace/ci-cloud-identity.test.ts +20 -0
  225. package/src/workspace/ci-cloud-identity.ts +14 -1
  226. package/src/workspace/ci-run.test.ts +74 -1
  227. package/src/workspace/ci-run.ts +122 -29
  228. package/src/workspace/ci-secrets.test.ts +1 -1
  229. package/src/workspace/ci-secrets.ts +4 -3
  230. package/src/workspace/compose-graph.ts +10 -1
  231. package/src/workspace/conformance/conformance.e2e.test.ts +14 -1
  232. package/src/workspace/conformance/index.ts +31 -2
  233. package/src/workspace/declaration.schema.json +36 -1
  234. package/src/workspace/declaration.test.ts +7 -0
  235. package/src/workspace/declaration.ts +24 -2
  236. package/src/workspace/graph-cache.ts +27 -3
  237. package/src/workspace/graph-cli.ts +96 -24
  238. package/src/workspace/graph.schema.json +148 -0
  239. package/src/workspace/kind-readers.e2e.test.ts +28 -0
  240. package/src/workspace/kind-readers.test.ts +1 -1
  241. package/src/workspace/kind-readers.ts +7 -1
  242. package/src/workspace/member-commands.ts +22 -1
  243. package/src/workspace/nested-graph.ts +32 -1
  244. package/src/workspace/reason-codes.test.ts +7 -0
  245. package/src/workspace/reason-codes.ts +12 -0
  246. package/src/workspace/search-cli.e2e.test.ts +305 -0
  247. package/src/workspace/search-cli.ts +591 -0
  248. package/src/workspace/search-contract.ts +95 -0
  249. package/src/workspace/search.schema.json +822 -0
@@ -32,14 +32,20 @@
32
32
  *
33
33
  * terragucci's `ephemeral.ts` gives each copy a suffixed Terraform state key.
34
34
  * That part is Terraform's own and stays there; chant keys ownership on the
35
- * marker its lexicons stamp and read back (`../lifecycle/teardown.ts`).
35
+ * marker its lexicons stamp and read back (`../lifecycle/teardown.ts`). A
36
+ * product supplies the rest as adapters (#4330): its plan and apply
37
+ * (`./pr-env-run.ts`), and a teardown for what no lexicon tears down
38
+ * ({@link PrEnvTeardownAdapter}), which reads the marker back from the copy
39
+ * as a lexicon does.
36
40
  */
37
41
 
38
- import { environmentEndpoint, environmentName, isEnvironmentPattern, isOwnershipParamRef, matchesDeclaredEnvironment, resolveOwnershipStack, type ChantConfig, type EnvironmentDeclaration } from "../config";
42
+ import type { ChantConfig } from "../config";
43
+ import { environmentEndpoint, environmentName, isEnvironmentPattern, isOwnershipParamRef, matchesDeclaredEnvironment, resolveOwnershipStack, type EnvironmentDeclaration } from "../config-environments";
39
44
  import type { OwnershipMarker } from "../ownership";
40
45
  import { executeTeardown, planTeardown, type TeardownPlan, type TeardownPlanEntry, type TeardownReport } from "../lifecycle/teardown";
41
- import type { ObservationLexicon } from "../lexicon";
46
+ import type { ObservationLexicon, TeardownCandidate, TeardownEnumeration, TeardownExecution } from "../lexicon";
42
47
  import { applyLiveEndpoint } from "../live-endpoint";
48
+ import type { ComponentEnvironmentDeclaration } from "./component";
43
49
 
44
50
  /** The gate a pull request's environment waits on, under the pull request's op `pr-<n>`. */
45
51
  export const PR_ENV_GATE = "pr-env";
@@ -167,6 +173,35 @@ export function derivePrEnvironment(config: ChantConfig, template: string, pr: n
167
173
  return { env, template, stack, param: own.param, environments, ...(endpoint ? { endpoint } : {}) };
168
174
  }
169
175
 
176
+ /** The placeholder a template's site URL or domain names the environment with (#4330). */
177
+ export const PR_ENV_SITE_PLACEHOLDER = "{env}";
178
+
179
+ /**
180
+ * A component's site in `env`, a pull request's environment, carried over
181
+ * from its site in `template` (#4330). An entry the component declares for
182
+ * `env` itself wins. Otherwise the template's runtime and lifecycle remote
183
+ * carry over as they are. Its URL and domain carry over only when they name
184
+ * the environment with `{env}`, which becomes `pr-<n>`: a literal URL or
185
+ * domain is the template's own release, so it is left out rather than
186
+ * pointing pr-<n> at it. Undefined when neither is declared.
187
+ */
188
+ export function prEnvironmentSite(declared: readonly ComponentEnvironmentDeclaration[] | undefined, template: string, env: string): ComponentEnvironmentDeclaration | undefined {
189
+ const own = declared?.find((d) => d.name === env);
190
+ if (own) return { ...own };
191
+ const from = declared?.find((d) => d.name === template);
192
+ if (!from) return undefined;
193
+ const named = (v: string | undefined): string | undefined => (v && v.includes(PR_ENV_SITE_PLACEHOLDER) ? v.split(PR_ENV_SITE_PLACEHOLDER).join(env) : undefined);
194
+ const url = named(from.url);
195
+ const domain = named(from.domain);
196
+ return {
197
+ name: env,
198
+ ...(from.runtime ? { runtime: from.runtime } : {}),
199
+ ...(url ? { url } : {}),
200
+ ...(domain ? { domain } : {}),
201
+ ...(from.lifecycle ? { lifecycle: from.lifecycle } : {}),
202
+ };
203
+ }
204
+
170
205
  // ── records ────────────────────────────────────────────────────────────────
171
206
 
172
207
  /** One line of `_pr-envs/records.jsonl`: an apply, or a teardown. */
@@ -186,6 +221,12 @@ export interface PrEnvRecord {
186
221
  commit: string;
187
222
  /** The components applied (up) or the lexicons torn down (down). */
188
223
  components: string[];
224
+ /**
225
+ * Up (#4330): where each component's copy lives, by component, as the
226
+ * adapter that applied it says (a Terraform root's suffixed state key).
227
+ * A teardown adapter reads it back from the live environment.
228
+ */
229
+ locations?: Record<string, string>;
189
230
  /** The change-set digest applied; empty for a teardown. */
190
231
  planDigest: string;
191
232
  /** `incomplete`: a teardown that left something, so the environment stays live and the sweep tries again. */
@@ -229,6 +270,8 @@ export interface LivePrEnvironment {
229
270
  commit: string;
230
271
  /** Every component an apply of it ever touched. */
231
272
  components: string[];
273
+ /** Where each component's copy lives, merged over every up (#4330). */
274
+ locations?: Record<string, string>;
232
275
  applied: string;
233
276
  expiresAt: string;
234
277
  /** Its last teardown left something, so the sweep tries again. */
@@ -245,6 +288,7 @@ export function livePrEnvironments(records: readonly PrEnvRecord[]): Map<string,
245
288
  for (const r of records) {
246
289
  if (r.kind === "up") {
247
290
  const was = live.get(r.env);
291
+ const locations = { ...(was?.locations ?? {}), ...(r.locations ?? {}) };
248
292
  live.set(r.env, {
249
293
  pr: r.pr,
250
294
  env: r.env,
@@ -253,6 +297,7 @@ export function livePrEnvironments(records: readonly PrEnvRecord[]): Map<string,
253
297
  marker: { ...r.marker },
254
298
  commit: r.commit,
255
299
  components: [...new Set([...(was?.components ?? []), ...r.components])],
300
+ ...(Object.keys(locations).length > 0 ? { locations } : {}),
256
301
  applied: r.timestamp,
257
302
  expiresAt: r.expiresAt ?? r.timestamp,
258
303
  });
@@ -302,12 +347,61 @@ export function ownedEntries(plan: TeardownPlan, marker: OwnershipMarker): { pla
302
347
  return { plan: { ...plan, entries: plan.entries.filter(owns) }, dropped: plan.entries.filter((e) => !owns(e)) };
303
348
  }
304
349
 
350
+ /**
351
+ * A teardown a product supplies for what no lexicon tears down (#4330), such
352
+ * as a Terraform root's copy under a suffixed state key: chant has no
353
+ * Terraform runner, so terragucci supplies this. It takes part in the same
354
+ * two halves a lexicon's teardown does (`../lifecycle/teardown.ts`): `owned`
355
+ * names what the environment owns, each with the marker read back from it,
356
+ * and `teardown` deletes the candidates chant hands back. Every candidate
357
+ * whose marker is not exactly `{ stack, env: "pr-<n>" }` is dropped before
358
+ * `teardown` sees it, and a candidate it says nothing about counts as failed.
359
+ */
360
+ export interface PrEnvTeardownAdapter {
361
+ /** The name outcomes are attributed to, as a lexicon's are. */
362
+ name: string;
363
+ /**
364
+ * The lexicons this adapter tears down in place of (`terraform`). Their own
365
+ * teardown is left out, so a lexicon with no teardown execution does not
366
+ * leave the environment incomplete when the adapter covers it.
367
+ */
368
+ lexicons?: string[];
369
+ /** What the environment owns: candidates with their read-back markers, and what could not be read. */
370
+ owned(input: PrEnvTeardownAdapterInput): Promise<TeardownEnumeration>;
371
+ /** Delete the candidates: one outcome per candidate, keyed by name. */
372
+ teardown(input: PrEnvTeardownAdapterInput & { candidates: TeardownCandidate[] }): Promise<TeardownExecution>;
373
+ }
374
+
375
+ /** What a {@link PrEnvTeardownAdapter} is told about the environment. */
376
+ export interface PrEnvTeardownAdapterInput {
377
+ /** `pr-<n>`. */
378
+ environment: string;
379
+ /** The marker everything is selected on. */
380
+ marker: OwnershipMarker;
381
+ /** The environment as its records say it stands: the commit last applied, its components and their locations. */
382
+ live: LivePrEnvironment;
383
+ }
384
+
385
+ /** An adapter in the shape `planTeardown` and `executeTeardown` call: a lexicon with only its two teardown halves. */
386
+ function adapterLexicon(adapter: PrEnvTeardownAdapter, live: LivePrEnvironment): ObservationLexicon {
387
+ const shaped: Pick<ObservationLexicon, "name" | "teardownOwned" | "executeTeardown"> = {
388
+ name: adapter.name,
389
+ teardownOwned: (o) => adapter.owned({ environment: o.environment, marker: o.marker, live }),
390
+ executeTeardown: (o) => adapter.teardown({ environment: o.environment, marker: o.marker, live, candidates: o.candidates }),
391
+ };
392
+ return shaped as ObservationLexicon;
393
+ }
394
+
305
395
  /** What {@link tearDownPrEnvironment} needs. */
306
396
  export interface PrEnvTeardownInput {
307
397
  pr: number;
308
398
  live: LivePrEnvironment;
309
- config: ChantConfig;
310
- plugins: ObservationLexicon[];
399
+ /** The project's config, for its environments and stacks. Absent: a product with no chant.config.ts, whose adapters do the teardown. */
400
+ config?: ChantConfig;
401
+ /** The lexicons whose teardown runs. Default none. */
402
+ plugins?: ObservationLexicon[];
403
+ /** Teardowns a product supplies (#4330), run after the lexicons'. */
404
+ adapters?: PrEnvTeardownAdapter[];
311
405
  warn?: (message: string) => void;
312
406
  }
313
407
 
@@ -316,18 +410,30 @@ export interface PrEnvTeardownInput {
316
410
  * before any read when {@link teardownRefusal} says so; otherwise the live
317
411
  * resources under the record's marker are planned, every entry whose marker
318
412
  * is not exactly `{ stack, env: "pr-<n>" }` is dropped ({@link ownedEntries}),
319
- * and only the rest go to each lexicon's teardown. The endpoint is the one
320
- * the environment was applied at: its own `pr-*` entry's, else the template's.
413
+ * and only the rest go to each lexicon's or adapter's teardown. The endpoint
414
+ * is the one the environment was applied at: its own `pr-*` entry's, else the
415
+ * template's. A lexicon an adapter covers (`lexicons`) is left out.
321
416
  */
322
417
  export async function tearDownPrEnvironment(input: PrEnvTeardownInput): Promise<{ refusal: string } | { report: TeardownReport; dropped: number; marker: { stack: string; env: string } }> {
323
- const { pr, live, config, plugins } = input;
418
+ const { pr, live } = input;
419
+ const config = input.config ?? {};
324
420
  const refusal = teardownRefusal(pr, live, config.environments);
325
421
  if (refusal) return { refusal };
422
+ const adapters = input.adapters ?? [];
423
+ const covered = new Set(adapters.flatMap((a) => a.lexicons ?? []));
424
+ const names = new Set<string>();
425
+ for (const a of adapters) {
426
+ if (names.has(a.name)) return { refusal: `two teardown adapters are named ${a.name}, so their outcomes could not be told apart; nothing was torn down` };
427
+ names.add(a.name);
428
+ }
429
+ const lexicons = (input.plugins ?? []).filter((x) => !covered.has(x.name));
430
+ if (lexicons.some((x) => names.has(x.name))) return { refusal: `a teardown adapter shares its name with a lexicon (${[...names].filter((n) => lexicons.some((x) => x.name === n)).join(", ")}), so their outcomes could not be told apart; nothing was torn down` };
326
431
  const env = prEnvironmentName(pr);
327
432
  const marker = { stack: live.marker.stack, env };
328
433
  const endpoint = environmentEndpoint(config.environments, env) ?? environmentEndpoint(config.environments, live.template);
329
- const applied = applyLiveEndpoint(endpoint ? [{ name: env, endpoint }] : [], env, plugins.filter((x) => x.teardownOwned || x.describeResources || x.executeTeardown));
434
+ const applied = applyLiveEndpoint(endpoint ? [{ name: env, endpoint }] : [], env, lexicons.filter((x) => x.teardownOwned || x.describeResources || x.executeTeardown));
330
435
  if (applied.notice) input.warn?.(applied.notice);
436
+ const plugins = [...lexicons, ...adapters.map((a) => adapterLexicon(a, live))];
331
437
  const deployedStacks = (config.stacks ?? []).map((s) => ({ name: s.name, ...(s.region ? { region: s.region } : {}) }));
332
438
  try {
333
439
  const planned = await planTeardown({ environment: env, stack: marker.stack, plugins, ...(deployedStacks.length > 0 ? { deployedStacks } : {}) });
@@ -405,6 +511,7 @@ export function acceptHandedRecord(raw: unknown, check: HandedRecordCheck): { re
405
511
  marker: { stack: r.marker.stack, env },
406
512
  commit: check.head,
407
513
  components: [...r.components],
514
+ ...(handedLocations(r.locations) ? { locations: handedLocations(r.locations)! } : {}),
408
515
  planDigest: typeof r.planDigest === "string" ? r.planDigest : "",
409
516
  result: r.result,
410
517
  by: check.by,
@@ -417,6 +524,21 @@ export function acceptHandedRecord(raw: unknown, check: HandedRecordCheck): { re
417
524
  };
418
525
  }
419
526
 
527
+ /** A handed-on report row's site, kept only as its three strings. */
528
+ function handedSite(raw: unknown): PrEnvReportRow["site"] | undefined {
529
+ if (typeof raw !== "object" || raw === null) return undefined;
530
+ const r = raw as Record<string, unknown>;
531
+ const site = Object.fromEntries((["runtime", "url", "domain"] as const).filter((k) => typeof r[k] === "string").map((k) => [k, r[k] as string]));
532
+ return Object.keys(site).length > 0 ? site : undefined;
533
+ }
534
+
535
+ /** A handed-on record's locations, kept only as a map of strings to strings. */
536
+ function handedLocations(raw: unknown): Record<string, string> | undefined {
537
+ if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return undefined;
538
+ const out = Object.fromEntries(Object.entries(raw as Record<string, unknown>).filter((e): e is [string, string] => typeof e[1] === "string"));
539
+ return Object.keys(out).length > 0 ? out : undefined;
540
+ }
541
+
420
542
  /**
421
543
  * The report `pr-env up` handed on, checked as data before the post job
422
544
  * renders a note from it: an `up` report for this pull request's `pr-<n>`.
@@ -445,7 +567,16 @@ export function handedPrEnvReport(raw: unknown, check: { pr: number; head: strin
445
567
  status: r.status,
446
568
  ...(typeof r.message === "string" ? { message: r.message } : {}),
447
569
  ...(typeof r.endpoint === "string" ? { endpoint: r.endpoint } : {}),
448
- components: (Array.isArray(r.components) ? r.components : []).map((c) => ({ component: str(c?.component), status: str(c?.status), create: num(c?.create), update: num(c?.update), replace: num(c?.replace), delete: num(c?.delete) })),
570
+ components: (Array.isArray(r.components) ? r.components : []).map((c) => ({
571
+ component: str(c?.component),
572
+ status: str(c?.status),
573
+ create: num(c?.create),
574
+ update: num(c?.update),
575
+ replace: num(c?.replace),
576
+ delete: num(c?.delete),
577
+ ...(typeof c?.location === "string" ? { location: c.location } : {}),
578
+ ...(handedSite(c?.site) ? { site: handedSite(c?.site)! } : {}),
579
+ })),
449
580
  ...(Array.isArray(r.approvedBy) ? { approvedBy: r.approvedBy.filter((a): a is string => typeof a === "string") } : {}),
450
581
  },
451
582
  };
@@ -453,6 +584,20 @@ export function handedPrEnvReport(raw: unknown, check: { pr: number; head: strin
453
584
 
454
585
  // ── the note ───────────────────────────────────────────────────────────────
455
586
 
587
+ /** One component's row in the report and the note. */
588
+ export interface PrEnvReportRow {
589
+ component: string;
590
+ status: string;
591
+ create: number;
592
+ update: number;
593
+ replace: number;
594
+ delete: number;
595
+ /** Where its copy lives, as the adapter says (#4330). */
596
+ location?: string;
597
+ /** Its site in pr-<n>: the runtime, URL and domain carried over from the template's (#4330, {@link prEnvironmentSite}). */
598
+ site?: { runtime?: string; url?: string; domain?: string };
599
+ }
600
+
456
601
  /** What one `pr-env up` or `down` run came to, as `pr-env.json` holds it. */
457
602
  export interface PrEnvReport {
458
603
  stage: "up" | "down";
@@ -467,7 +612,7 @@ export interface PrEnvReport {
467
612
  status: "applied" | "nothing" | "waiting" | "changed" | "plan-failed" | "failed" | "refused" | "destroyed" | "incomplete" | "absent";
468
613
  message?: string;
469
614
  endpoint?: string;
470
- components: Array<{ component: string; status: string; create: number; update: number; replace: number; delete: number }>;
615
+ components: PrEnvReportRow[];
471
616
  approvedBy?: string[];
472
617
  expiresAt?: string;
473
618
  outcomes?: PrEnvRecord["outcomes"];
@@ -505,8 +650,11 @@ export function renderPrEnvNote(r: PrEnvReport, approver = "<you>"): string {
505
650
  }
506
651
  if (r.message) out.push("", `> ${r.message.split("\n").join("\n> ")}`);
507
652
  if (r.components.length > 0) {
508
- out.push("", "| Component | Status | Create | Update | Replace | Delete |", "|---|---|---:|---:|---:|---:|");
509
- for (const c of r.components) out.push(`| ${code(c.component)} | ${c.status} | ${c.create} | ${c.update} | ${c.replace} | ${c.delete} |`);
653
+ // #4330: a Where column when a row has a URL or a location, as code: env-up ran the change's code, so the post job does not link it.
654
+ const at = (c: PrEnvReportRow): string | undefined => c.site?.url ?? c.site?.domain ?? c.location;
655
+ const where = r.components.some((c) => at(c));
656
+ out.push("", `| Component | Status | Create | Update | Replace | Delete |${where ? " Where |" : ""}`, `|---|---|---:|---:|---:|---:|${where ? "---|" : ""}`);
657
+ for (const c of r.components) out.push(`| ${code(c.component)} | ${c.status} | ${c.create} | ${c.update} | ${c.replace} | ${c.delete} |${where ? ` ${at(c) ? code(at(c)!) : ""} |` : ""}`);
510
658
  }
511
659
  if (r.digest) out.push("", `Plan digest: ${code(r.digest)}${r.approvedBy?.length ? `, approved by ${r.approvedBy.map(code).join(", ")}` : ""}`);
512
660
  out.push("", `${code(r.env)} is not a release target: releases come from green commits.`);
@@ -89,7 +89,7 @@ import { EMPTY_TREE_SHA } from "./changed-files";
89
89
  import { layerWaves } from "../gated-waves";
90
90
  import type { DriverComponent } from "./driver";
91
91
  import type { GatedWavePipelineOptions } from "./gated-wave-pipeline";
92
- import type { PipelineCloudIdentity } from "../ci-identity";
92
+ import type { CloudTradeSpec, PipelineCloudIdentity } from "../ci-identity";
93
93
 
94
94
  /** The variable `chant ci run` reads its runner key from (`RUNNER_KEY_ENV` in ../workspace/ci-green.ts). */
95
95
  const RUNNER_KEY_ENV = "CHANT_RUNNER_KEY";
@@ -211,6 +211,15 @@ export interface PrLoopPipelineOptions {
211
211
  * ({@link prEnvJobs}). Set from `--pr-env` beside `--pr-loop`. Default false.
212
212
  */
213
213
  prEnvironment?: boolean;
214
+ /**
215
+ * The cloud identity a pull request environment's jobs take (#4330), from
216
+ * `ci.identity.cloud` (`resolvePrEnvCloudIdentity` in ../ci-identity.ts):
217
+ * the apply identity of a rule that names the pull request environments.
218
+ * env-up, env-down and env-sweep trade their OIDC token for it before the
219
+ * checkout, and env-up's later steps start without the token variables.
220
+ * Absent: they request no OIDC token.
221
+ */
222
+ prEnvIdentity?: CloudTradeSpec;
214
223
  }
215
224
 
216
225
  /** One job's command and the forge values it reads, as environment variables for its step. */
@@ -0,0 +1,133 @@
1
+ /**
2
+ * The pure helpers over a loaded config's `environments` and `ownership`:
3
+ * names, patterns, endpoints and the ownership stack. ./config.ts re-exports
4
+ * them. They import nothing at runtime, so ./components/pr-environment.ts and
5
+ * ./live-endpoint.ts use them without bundling the config loader.
6
+ */
7
+
8
+ import type { ChantConfig } from "./config";
9
+
10
+ /**
11
+ * One project-declared environment (chant #1166). Historically always a bare
12
+ * name (`"floci"`); an entry can now instead carry the endpoint that
13
+ * environment's `--live` reads should target (`{ name: "floci", endpoint:
14
+ * "http://localhost:4566" }`), so a project pointed at a local emulator is
15
+ * self-sufficient — no ambient `AWS_ENDPOINT_URL` export required to avoid
16
+ * silently querying real AWS. See {@link environmentName}, {@link
17
+ * environmentNames}, {@link environmentEndpoint} below, and
18
+ * `./live-endpoint.ts`'s `applyLiveEndpoint`, the CLI-side consumer.
19
+ *
20
+ * The array's order is the member's promotion order (#4030, see
21
+ * `./workspace/promotion-order.ts`), and `canary: true` marks a step as the
22
+ * canary step of that order. The canary is declared and surfaced; chant does
23
+ * not yet act on it.
24
+ */
25
+ export type EnvironmentDeclaration = string | { name: string; endpoint?: string; canary?: boolean };
26
+
27
+ /**
28
+ * `ownership.env`: a literal environment identity, or a reference to a
29
+ * declared build parameter whose resolved value becomes the identity (#1396).
30
+ */
31
+ export type OwnershipEnv = string | { param: string };
32
+
33
+ /** The declared name of one `environments` entry, whichever form it takes. */
34
+ export function environmentName(entry: EnvironmentDeclaration): string {
35
+ return typeof entry === "string" ? entry : entry.name;
36
+ }
37
+
38
+ /**
39
+ * Every declared environment's name, in `environments` order. `undefined` in,
40
+ * `undefined` out — mirrors the field itself being optional, so a caller that
41
+ * already writes `config.environments?.something` can keep doing so:
42
+ * `environmentNames(config.environments)?.includes(name)`.
43
+ */
44
+ export function environmentNames(environments: EnvironmentDeclaration[] | undefined): string[] | undefined {
45
+ return environments?.map(environmentName);
46
+ }
47
+
48
+ /**
49
+ * Whether a declared environment name is a glob pattern rather than a literal
50
+ * (#1221) — it contains at least one `*`.
51
+ */
52
+ export function isEnvironmentPattern(declaredName: string): boolean {
53
+ return declaredName.includes("*");
54
+ }
55
+
56
+ /**
57
+ * Anchored `*`-glob match (#1221): each `*` in `pattern` matches any run of
58
+ * characters (possibly empty); everything else matches itself. Deliberately
59
+ * the whole surface — no `?`, no character classes, no regex. A pattern with
60
+ * no `*` degenerates to string equality.
61
+ */
62
+ export function matchesEnvironmentPattern(pattern: string, name: string): boolean {
63
+ const parts = pattern.split("*");
64
+ if (parts.length === 1) return pattern === name;
65
+ const first = parts[0];
66
+ const last = parts[parts.length - 1];
67
+ if (!name.startsWith(first)) return false;
68
+ let cursor = first.length;
69
+ for (let i = 1; i < parts.length - 1; i++) {
70
+ const part = parts[i];
71
+ if (part === "") continue;
72
+ const at = name.indexOf(part, cursor);
73
+ if (at === -1) return false;
74
+ cursor = at + part.length;
75
+ }
76
+ return name.length - cursor >= last.length && name.endsWith(last);
77
+ }
78
+
79
+ /**
80
+ * Environment-membership test (#1221): is `name` covered by this project's
81
+ * declared `environments`? A literal entry matches by equality; an entry
82
+ * containing `*` matches as a glob pattern (`"pr-*"` covers `pr-42`).
83
+ * Literals are checked first across the whole list, then patterns — so a
84
+ * name that is declared outright never depends on pattern order. `undefined`
85
+ * / empty `environments` returns `false`; callers that treat "no declared
86
+ * environments" as "anything goes" (e.g. {@link unknownEnvError} in
87
+ * `./env.ts`) guard that case themselves.
88
+ */
89
+ export function matchesDeclaredEnvironment(
90
+ environments: EnvironmentDeclaration[] | undefined,
91
+ name: string,
92
+ ): boolean {
93
+ const names = environmentNames(environments) ?? [];
94
+ if (names.includes(name)) return true;
95
+ return names.some((declared) => isEnvironmentPattern(declared) && matchesEnvironmentPattern(declared, name));
96
+ }
97
+
98
+ /**
99
+ * The endpoint `name` declares (chant #1166) — `undefined` for a bare-string
100
+ * entry, an entry with no `endpoint` set, or a name this project doesn't
101
+ * declare at all. `./live-endpoint.ts`'s `applyLiveEndpoint` is the consumer:
102
+ * it injects this into the ambient env var each observing lexicon's CLI
103
+ * shell-out reads (e.g. `AWS_ENDPOINT_URL`), unless that var is already set —
104
+ * ambient always wins.
105
+ *
106
+ * A pattern entry (#1221) can carry an endpoint too: `{ name: "pr-*",
107
+ * endpoint }` supplies the endpoint for every `pr-<n>` environment. A literal
108
+ * entry always wins over a pattern — declaring `pr-special` alongside
109
+ * `pr-*` resolves `pr-special` to the literal entry's endpoint (or none).
110
+ */
111
+ export function environmentEndpoint(environments: EnvironmentDeclaration[] | undefined, name: string): string | undefined {
112
+ const found =
113
+ environments?.find((e) => environmentName(e) === name) ??
114
+ environments?.find((e) => isEnvironmentPattern(environmentName(e)) && matchesEnvironmentPattern(environmentName(e), name));
115
+ return found && typeof found !== "string" ? found.endpoint : undefined;
116
+ }
117
+
118
+ /** Whether an `ownership.env` entry is a build-parameter reference rather than a literal. */
119
+ export function isOwnershipParamRef(env: OwnershipEnv | undefined): env is { param: string } {
120
+ return typeof env === "object" && env !== null && typeof env.param === "string";
121
+ }
122
+
123
+ /**
124
+ * The ownership stack identity alone — `ownership.stack` when marking is on
125
+ * (no `stack`, or `enabled: false`, means off). For callers that need the
126
+ * stack and nothing else (a field manager, a prune selector) and so have no
127
+ * build parameters to resolve `env` against.
128
+ */
129
+ export function resolveOwnershipStack(config: Pick<ChantConfig, "ownership">): string | undefined {
130
+ const o = config.ownership;
131
+ if (!o || !o.stack || o.enabled === false) return undefined;
132
+ return o.stack;
133
+ }
package/src/config.ts CHANGED
@@ -14,113 +14,22 @@ import { lexiconNames, pathLexiconRoot, registerLexiconDeclarations, type Lexico
14
14
  import { decideConfigSchema, type DecideConfig } from "./op/decide-config";
15
15
  import { PublishDeclarationSchema, type PublishDeclaration } from "./lifecycle/publish-declaration";
16
16
 
17
- /**
18
- * One project-declared environment (chant #1166). Historically always a bare
19
- * name (`"floci"`); an entry can now instead carry the endpoint that
20
- * environment's `--live` reads should target (`{ name: "floci", endpoint:
21
- * "http://localhost:4566" }`), so a project pointed at a local emulator is
22
- * self-sufficient — no ambient `AWS_ENDPOINT_URL` export required to avoid
23
- * silently querying real AWS. See {@link environmentName}, {@link
24
- * environmentNames}, {@link environmentEndpoint} below, and
25
- * `./live-endpoint.ts`'s `applyLiveEndpoint`, the CLI-side consumer.
26
- *
27
- * The array's order is the member's promotion order (#4030, see
28
- * `./workspace/promotion-order.ts`), and `canary: true` marks a step as the
29
- * canary step of that order. The canary is declared and surfaced; chant does
30
- * not yet act on it.
31
- */
32
- export type EnvironmentDeclaration = string | { name: string; endpoint?: string; canary?: boolean };
33
-
34
- /**
35
- * `ownership.env`: a literal environment identity, or a reference to a
36
- * declared build parameter whose resolved value becomes the identity (#1396).
37
- */
38
- export type OwnershipEnv = string | { param: string };
39
-
40
- /** The declared name of one `environments` entry, whichever form it takes. */
41
- export function environmentName(entry: EnvironmentDeclaration): string {
42
- return typeof entry === "string" ? entry : entry.name;
43
- }
44
-
45
- /**
46
- * Every declared environment's name, in `environments` order. `undefined` in,
47
- * `undefined` out — mirrors the field itself being optional, so a caller that
48
- * already writes `config.environments?.something` can keep doing so:
49
- * `environmentNames(config.environments)?.includes(name)`.
50
- */
51
- export function environmentNames(environments: EnvironmentDeclaration[] | undefined): string[] | undefined {
52
- return environments?.map(environmentName);
53
- }
54
-
55
- /**
56
- * Whether a declared environment name is a glob pattern rather than a literal
57
- * (#1221) — it contains at least one `*`.
58
- */
59
- export function isEnvironmentPattern(declaredName: string): boolean {
60
- return declaredName.includes("*");
61
- }
62
-
63
- /**
64
- * Anchored `*`-glob match (#1221): each `*` in `pattern` matches any run of
65
- * characters (possibly empty); everything else matches itself. Deliberately
66
- * the whole surface — no `?`, no character classes, no regex. A pattern with
67
- * no `*` degenerates to string equality.
68
- */
69
- export function matchesEnvironmentPattern(pattern: string, name: string): boolean {
70
- const parts = pattern.split("*");
71
- if (parts.length === 1) return pattern === name;
72
- const first = parts[0];
73
- const last = parts[parts.length - 1];
74
- if (!name.startsWith(first)) return false;
75
- let cursor = first.length;
76
- for (let i = 1; i < parts.length - 1; i++) {
77
- const part = parts[i];
78
- if (part === "") continue;
79
- const at = name.indexOf(part, cursor);
80
- if (at === -1) return false;
81
- cursor = at + part.length;
82
- }
83
- return name.length - cursor >= last.length && name.endsWith(last);
84
- }
85
-
86
- /**
87
- * Environment-membership test (#1221): is `name` covered by this project's
88
- * declared `environments`? A literal entry matches by equality; an entry
89
- * containing `*` matches as a glob pattern (`"pr-*"` covers `pr-42`).
90
- * Literals are checked first across the whole list, then patterns — so a
91
- * name that is declared outright never depends on pattern order. `undefined`
92
- * / empty `environments` returns `false`; callers that treat "no declared
93
- * environments" as "anything goes" (e.g. {@link unknownEnvError} in
94
- * `./env.ts`) guard that case themselves.
95
- */
96
- export function matchesDeclaredEnvironment(
97
- environments: EnvironmentDeclaration[] | undefined,
98
- name: string,
99
- ): boolean {
100
- const names = environmentNames(environments) ?? [];
101
- if (names.includes(name)) return true;
102
- return names.some((declared) => isEnvironmentPattern(declared) && matchesEnvironmentPattern(declared, name));
103
- }
104
-
105
- /**
106
- * The endpoint `name` declares (chant #1166) — `undefined` for a bare-string
107
- * entry, an entry with no `endpoint` set, or a name this project doesn't
108
- * declare at all. `./live-endpoint.ts`'s `applyLiveEndpoint` is the consumer:
109
- * it injects this into the ambient env var each observing lexicon's CLI
110
- * shell-out reads (e.g. `AWS_ENDPOINT_URL`), unless that var is already set —
111
- * ambient always wins.
112
- *
113
- * A pattern entry (#1221) can carry an endpoint too: `{ name: "pr-*",
114
- * endpoint }` supplies the endpoint for every `pr-<n>` environment. A literal
115
- * entry always wins over a pattern — declaring `pr-special` alongside
116
- * `pr-*` resolves `pr-special` to the literal entry's endpoint (or none).
117
- */
118
- export function environmentEndpoint(environments: EnvironmentDeclaration[] | undefined, name: string): string | undefined {
119
- const found =
120
- environments?.find((e) => environmentName(e) === name) ??
121
- environments?.find((e) => isEnvironmentPattern(environmentName(e)) && matchesEnvironmentPattern(environmentName(e), name));
122
- return found && typeof found !== "string" ? found.endpoint : undefined;
123
- }
17
+ // The environment and ownership helpers that read a config already loaded
18
+ // live in ./config-environments.ts, which loads nothing, so a bundle that
19
+ // needs only them stays clear of the loader below (#4356 follow-up).
20
+ export {
21
+ environmentEndpoint,
22
+ environmentName,
23
+ environmentNames,
24
+ isEnvironmentPattern,
25
+ isOwnershipParamRef,
26
+ matchesDeclaredEnvironment,
27
+ matchesEnvironmentPattern,
28
+ resolveOwnershipStack,
29
+ type EnvironmentDeclaration,
30
+ type OwnershipEnv,
31
+ } from "./config-environments";
32
+ import { isOwnershipParamRef, resolveOwnershipStack, type EnvironmentDeclaration, type OwnershipEnv } from "./config-environments";
124
33
 
125
34
  /**
126
35
  * Zod schema for ChantConfig validation.
@@ -157,6 +66,10 @@ export const ChantConfigSchema = z.object({
157
66
  build: z.object({
158
67
  fold: z.boolean().optional(),
159
68
  }).optional(),
69
+ // Strict like `run`: a misspelt `lint` would leave the gate on with nothing said.
70
+ graph: z.object({
71
+ lint: z.enum(["gate", "report"]).optional(),
72
+ }).strict().optional(),
160
73
  buildParams: z.record(
161
74
  z.string(),
162
75
  z.object({
@@ -299,6 +212,23 @@ export interface ChantConfig {
299
212
  /** Lint configuration (rules, extends, overrides, plugins) */
300
213
  lint?: LintConfig;
301
214
 
215
+ /**
216
+ * How `chant graph` treats lint errors (INTENTIUS/behold#919).
217
+ *
218
+ * `"gate"` (the default) refuses to emit the entity graph for source that
219
+ * fails `chant lint`: the graph stands for source chant will build.
220
+ * `"report"` emits it anyway and carries every lint finding on the IR's
221
+ * `meta.lint`, for a project that only reads source it did not author. The
222
+ * reader project `chant workspace graph` writes for a terraform or choudoufu
223
+ * member sets it, and so does behold's scratch project for a bare Terraform
224
+ * folder: an error-severity finding there describes the estate, and the
225
+ * person needs the picture to look at it. A chant older than the key
226
+ * ignores it and gates as before.
227
+ */
228
+ graph?: {
229
+ lint?: "gate" | "report";
230
+ };
231
+
302
232
  /**
303
233
  * Telemetry attribution (#2558, D22): lexicons that declare workloads stamp
304
234
  * resource attributes (`chant.workspace`, `chant.member`, `chant.decl`,
@@ -764,23 +694,6 @@ function warnIfFragmentShadowsProjectConfig(configPath: string): void {
764
694
  /** Top-level keys that make a config a *project* config rather than a lint fragment. */
765
695
  const PROJECT_CONFIG_KEYS = new Set(Object.keys(ChantConfigSchema.shape).filter((k) => k !== "lint"));
766
696
 
767
- /** Whether an `ownership.env` entry is a build-parameter reference rather than a literal. */
768
- export function isOwnershipParamRef(env: OwnershipEnv | undefined): env is { param: string } {
769
- return typeof env === "object" && env !== null && typeof env.param === "string";
770
- }
771
-
772
- /**
773
- * The ownership stack identity alone — `ownership.stack` when marking is on
774
- * (no `stack`, or `enabled: false`, means off). For callers that need the
775
- * stack and nothing else (a field manager, a prune selector) and so have no
776
- * build parameters to resolve `env` against.
777
- */
778
- export function resolveOwnershipStack(config: ChantConfig): string | undefined {
779
- const o = config.ownership;
780
- if (!o || !o.stack || o.enabled === false) return undefined;
781
- return o.stack;
782
- }
783
-
784
697
  /**
785
698
  * Resolve the `env` half of the ownership marker (#1396). A literal is
786
699
  * returned as-is. A `{ param }` reference is looked up in this build's resolved