@intentius/chant 0.124.45 → 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 (190) 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/cli/commands/lint.d.ts.map +1 -1
  16. package/dist/cli/commands/lint.js +1 -0
  17. package/dist/cli/handlers/declared-graph.d.ts +44 -0
  18. package/dist/cli/handlers/declared-graph.d.ts.map +1 -0
  19. package/dist/cli/handlers/declared-graph.js +87 -0
  20. package/dist/cli/handlers/graph.d.ts +25 -0
  21. package/dist/cli/handlers/graph.d.ts.map +1 -1
  22. package/dist/cli/handlers/graph.js +93 -87
  23. package/dist/cli/handlers/pr-env.d.ts.map +1 -1
  24. package/dist/cli/handlers/pr-env.js +2 -1
  25. package/dist/cli/handlers/search-count.d.ts +89 -0
  26. package/dist/cli/handlers/search-count.d.ts.map +1 -0
  27. package/dist/cli/handlers/search-count.js +132 -0
  28. package/dist/cli/handlers/search.d.ts +358 -6
  29. package/dist/cli/handlers/search.d.ts.map +1 -1
  30. package/dist/cli/handlers/search.js +1065 -155
  31. package/dist/cli/main.d.ts.map +1 -1
  32. package/dist/cli/main.js +23 -3
  33. package/dist/cli/mcp/server.d.ts.map +1 -1
  34. package/dist/cli/mcp/server.js +3 -0
  35. package/dist/cli/mcp/tools/capabilities.d.ts +7 -0
  36. package/dist/cli/mcp/tools/capabilities.d.ts.map +1 -0
  37. package/dist/cli/mcp/tools/capabilities.js +27 -0
  38. package/dist/cli/mcp/workspace-tools.d.ts +3 -1
  39. package/dist/cli/mcp/workspace-tools.d.ts.map +1 -1
  40. package/dist/cli/mcp/workspace-tools.js +47 -2
  41. package/dist/cli/parse-args.d.ts.map +1 -1
  42. package/dist/cli/parse-args.js +64 -17
  43. package/dist/cli/registry.d.ts +22 -0
  44. package/dist/cli/registry.d.ts.map +1 -1
  45. package/dist/components/pr-env-ports.d.ts +29 -0
  46. package/dist/components/pr-env-ports.d.ts.map +1 -0
  47. package/dist/components/pr-env-ports.js +77 -0
  48. package/dist/components/pr-env-run.d.ts +5 -21
  49. package/dist/components/pr-env-run.d.ts.map +1 -1
  50. package/dist/components/pr-env-run.js +6 -70
  51. package/dist/components/pr-environment.d.ts +2 -1
  52. package/dist/components/pr-environment.d.ts.map +1 -1
  53. package/dist/components/pr-environment.js +1 -1
  54. package/dist/config-environments.d.ts +92 -0
  55. package/dist/config-environments.d.ts.map +1 -0
  56. package/dist/config-environments.js +102 -0
  57. package/dist/config.d.ts +24 -84
  58. package/dist/config.d.ts.map +1 -1
  59. package/dist/config.js +9 -96
  60. package/dist/graph-detail.d.ts.map +1 -1
  61. package/dist/graph-detail.js +15 -3
  62. package/dist/graph-effective.d.ts +33 -2
  63. package/dist/graph-effective.d.ts.map +1 -1
  64. package/dist/graph-effective.js +215 -68
  65. package/dist/graph-ir.d.ts +23 -0
  66. package/dist/graph-ir.d.ts.map +1 -1
  67. package/dist/graph-ir.js +130 -0
  68. package/dist/graph-sub-template.d.ts +11 -0
  69. package/dist/graph-sub-template.d.ts.map +1 -0
  70. package/dist/graph-sub-template.js +19 -0
  71. package/dist/lexicon.d.ts +9 -0
  72. package/dist/lexicon.d.ts.map +1 -1
  73. package/dist/lifecycle/observe.d.ts +28 -0
  74. package/dist/lifecycle/observe.d.ts.map +1 -1
  75. package/dist/lifecycle/observe.js +23 -1
  76. package/dist/lint/rule.d.ts +6 -0
  77. package/dist/lint/rule.d.ts.map +1 -1
  78. package/dist/live-endpoint.d.ts +1 -1
  79. package/dist/live-endpoint.d.ts.map +1 -1
  80. package/dist/live-endpoint.js +1 -1
  81. package/dist/observation.d.ts +48 -1
  82. package/dist/observation.d.ts.map +1 -1
  83. package/dist/observation.js +21 -5
  84. package/dist/op/activities/activity-contracts.d.ts +4 -4
  85. package/dist/wave-runtime.mjs +250 -249
  86. package/dist/workspace/capabilities-contract.d.ts +66 -0
  87. package/dist/workspace/capabilities-contract.d.ts.map +1 -0
  88. package/dist/workspace/capabilities-contract.js +107 -0
  89. package/dist/workspace/capabilities.schema.json +229 -0
  90. package/dist/workspace/compose-graph.d.ts +9 -0
  91. package/dist/workspace/compose-graph.d.ts.map +1 -1
  92. package/dist/workspace/compose-graph.js +3 -1
  93. package/dist/workspace/conformance/index.d.ts +1 -1
  94. package/dist/workspace/conformance/index.d.ts.map +1 -1
  95. package/dist/workspace/conformance/index.js +39 -2
  96. package/dist/workspace/graph-cache.d.ts +8 -0
  97. package/dist/workspace/graph-cache.d.ts.map +1 -1
  98. package/dist/workspace/graph-cache.js +27 -3
  99. package/dist/workspace/graph-cli.d.ts +39 -2
  100. package/dist/workspace/graph-cli.d.ts.map +1 -1
  101. package/dist/workspace/graph-cli.js +66 -24
  102. package/dist/workspace/graph.schema.json +148 -0
  103. package/dist/workspace/kind-readers.d.ts +3 -0
  104. package/dist/workspace/kind-readers.d.ts.map +1 -1
  105. package/dist/workspace/kind-readers.js +7 -1
  106. package/dist/workspace/member-commands.d.ts +10 -1
  107. package/dist/workspace/member-commands.d.ts.map +1 -1
  108. package/dist/workspace/member-commands.js +17 -1
  109. package/dist/workspace/nested-graph.d.ts +2 -0
  110. package/dist/workspace/nested-graph.d.ts.map +1 -1
  111. package/dist/workspace/nested-graph.js +32 -1
  112. package/dist/workspace/reason-codes.d.ts +10 -0
  113. package/dist/workspace/reason-codes.d.ts.map +1 -1
  114. package/dist/workspace/reason-codes.js +12 -0
  115. package/dist/workspace/search-cli.d.ts +204 -0
  116. package/dist/workspace/search-cli.d.ts.map +1 -0
  117. package/dist/workspace/search-cli.js +466 -0
  118. package/dist/workspace/search-contract.d.ts +56 -0
  119. package/dist/workspace/search-contract.d.ts.map +1 -0
  120. package/dist/workspace/search-contract.js +70 -0
  121. package/dist/workspace/search.schema.json +822 -0
  122. package/package.json +1 -1
  123. package/src/__snapshots__/okf.test.ts.snap +10 -0
  124. package/src/capabilities/aws.paths +148 -0
  125. package/src/capabilities/base-mappings.json +122 -0
  126. package/src/capabilities/catalog.ts +230 -0
  127. package/src/capabilities/language.test.ts +191 -0
  128. package/src/capabilities/language.ts +373 -0
  129. package/src/capabilities/matcher.ts +315 -0
  130. package/src/capabilities/view-compose.test.ts +76 -0
  131. package/src/capabilities/view.ts +631 -0
  132. package/src/cli/commands/lint.ts +1 -0
  133. package/src/cli/handlers/declared-graph.ts +99 -0
  134. package/src/cli/handlers/graph.test.ts +94 -3
  135. package/src/cli/handlers/graph.ts +123 -96
  136. package/src/cli/handlers/pr-env.ts +1 -2
  137. package/src/cli/handlers/search-build-roots.test.ts +144 -0
  138. package/src/cli/handlers/search-count.test.ts +191 -0
  139. package/src/cli/handlers/search-count.ts +165 -0
  140. package/src/cli/handlers/search-json.test.ts +333 -0
  141. package/src/cli/handlers/search-live.test.ts +387 -0
  142. package/src/cli/handlers/search-paths.test.ts +235 -0
  143. package/src/cli/handlers/search.ts +1312 -166
  144. package/src/cli/main.test.ts +28 -3
  145. package/src/cli/main.ts +23 -3
  146. package/src/cli/mcp/server.test.ts +3 -3
  147. package/src/cli/mcp/server.ts +3 -0
  148. package/src/cli/mcp/tools/capabilities.ts +32 -0
  149. package/src/cli/mcp/workspace-tools.e2e.test.ts +21 -1
  150. package/src/cli/mcp/workspace-tools.ts +47 -2
  151. package/src/cli/parse-args.ts +52 -13
  152. package/src/cli/registry.ts +22 -0
  153. package/src/components/pr-env-ports.ts +89 -0
  154. package/src/components/pr-env-run-bundle.test.ts +54 -0
  155. package/src/components/pr-env-run.ts +5 -76
  156. package/src/components/pr-environment.ts +2 -1
  157. package/src/config-environments.ts +133 -0
  158. package/src/config.ts +37 -124
  159. package/src/graph-detail.ts +15 -3
  160. package/src/graph-effective.ts +226 -75
  161. package/src/graph-ir.test.ts +163 -0
  162. package/src/graph-ir.ts +147 -0
  163. package/src/graph-sub-template.ts +22 -0
  164. package/src/lexicon.ts +10 -0
  165. package/src/lifecycle/observe.test.ts +27 -1
  166. package/src/lifecycle/observe.ts +43 -1
  167. package/src/lint/rule.ts +6 -0
  168. package/src/live-endpoint.ts +1 -1
  169. package/src/observation.test.ts +25 -0
  170. package/src/observation.ts +69 -5
  171. package/src/workspace/capabilities-contract.test.ts +88 -0
  172. package/src/workspace/capabilities-contract.ts +132 -0
  173. package/src/workspace/capabilities.schema.json +229 -0
  174. package/src/workspace/compose-graph.ts +10 -1
  175. package/src/workspace/conformance/conformance.e2e.test.ts +14 -1
  176. package/src/workspace/conformance/index.ts +31 -2
  177. package/src/workspace/graph-cache.ts +27 -3
  178. package/src/workspace/graph-cli.ts +96 -24
  179. package/src/workspace/graph.schema.json +148 -0
  180. package/src/workspace/kind-readers.e2e.test.ts +28 -0
  181. package/src/workspace/kind-readers.test.ts +1 -1
  182. package/src/workspace/kind-readers.ts +7 -1
  183. package/src/workspace/member-commands.ts +22 -1
  184. package/src/workspace/nested-graph.ts +32 -1
  185. package/src/workspace/reason-codes.test.ts +7 -0
  186. package/src/workspace/reason-codes.ts +12 -0
  187. package/src/workspace/search-cli.e2e.test.ts +305 -0
  188. package/src/workspace/search-cli.ts +591 -0
  189. package/src/workspace/search-contract.ts +95 -0
  190. package/src/workspace/search.schema.json +822 -0
@@ -16,9 +16,11 @@
16
16
  * - the report and the note ({@link renderPrEnvNote}).
17
17
  *
18
18
  * `chant components pr-env` (../cli/handlers/pr-env.ts) runs these with the
19
- * components adapter and the lexicons' teardown. The ports default to
20
- * chant/lifecycle in the working directory and the gate ledger there
21
- * ({@link lifecyclePrEnvPorts}).
19
+ * components adapter and the lexicons' teardown, and the ports over
20
+ * chant/lifecycle and the gate ledger in ./pr-env-ports.ts
21
+ * (`lifecyclePrEnvPorts`). Those ports load the gate and its schema checks,
22
+ * so they live apart: a product that brings its own ports bundles this
23
+ * module without them (pr-env-run-bundle.test.ts).
22
24
  */
23
25
 
24
26
  import { spawnSync } from "node:child_process";
@@ -29,19 +31,11 @@ import { withoutForgeTokens } from "../ci-handover";
29
31
  import type { ChantConfig } from "../config";
30
32
  import type { TeardownCandidate, TeardownHole, TeardownOutcome } from "../lexicon";
31
33
  import type { ObservationLexicon } from "../lexicon";
32
- import type { PrForge } from "../pr-forge";
33
34
  import {
34
35
  PR_ENV_EXIT,
35
36
  PR_ENV_GATE,
36
- PR_ENV_LEDGER_DIR,
37
- PR_ENV_LEDGER_FILE,
38
37
  expired,
39
38
  livePrEnvironments,
40
- parsePrEnvRecords,
41
- prEnvNoteMarker,
42
- prEnvStatusContext,
43
- prEnvStatusDescription,
44
- prEnvStatusState,
45
39
  prEnvironmentName,
46
40
  renderPrEnvNote,
47
41
  teardownComplete,
@@ -425,71 +419,6 @@ export async function prEnvSweep(input: PrEnvSweepInput): Promise<{ exit: number
425
419
  return { exit, results };
426
420
  }
427
421
 
428
- // ── the ports chant itself uses ────────────────────────────────────────────
429
-
430
- /**
431
- * The note and the `chant/pr-env` status on the forge. A refusal from the
432
- * forge warns and never fails the run.
433
- */
434
- export function forgePrEnvPublisher(forge: PrForge, warn: (message: string) => void = () => {}): (report: PrEnvReport) => Promise<void> {
435
- return async (report) => {
436
- const { forgePrincipalOf } = await import("../pr-forge");
437
- const note = renderPrEnvNote(report, forgePrincipalOf(forge.kind, forge.host, "<your-login>"));
438
- if (report.head) {
439
- try {
440
- await forge.setStatus(report.head, { context: prEnvStatusContext(report.member), state: prEnvStatusState(report), description: prEnvStatusDescription(report), ...(forge.runUrl ? { url: forge.runUrl } : {}) });
441
- } catch (err) {
442
- warn(`could not set the ${prEnvStatusContext(report.member)} status: ${messageOf(err)}`);
443
- }
444
- }
445
- try {
446
- await forge.upsertNote(report.pr, prEnvNoteMarker(report.env, report.member), note);
447
- } catch (err) {
448
- warn(`could not post the note on #${report.pr}: ${messageOf(err)}`);
449
- }
450
- };
451
- }
452
-
453
- /**
454
- * The ports over chant/lifecycle in `cwd` (default the working directory):
455
- * the records at `_pr-envs/records.jsonl`, appended and pushed, and the gate
456
- * ledger read without writing a pending fact. With `forge`, the note and
457
- * status are posted and the sweep asks it whether a pull request is open.
458
- */
459
- export function lifecyclePrEnvPorts(opts: { cwd?: string; forge?: PrForge; log?: (line: string) => void; warn?: (message: string) => void } = {}): PrEnvPorts {
460
- const at = opts.cwd ? { cwd: opts.cwd } : undefined;
461
- const warn = opts.warn ?? (() => {});
462
- const forge = opts.forge;
463
- return {
464
- async readRecords() {
465
- const { fetchLifecycle, readBlobFromPath } = await import("../lifecycle/git");
466
- await fetchLifecycle(at).catch(() => false);
467
- return parsePrEnvRecords(await readBlobFromPath(PR_ENV_LEDGER_DIR, PR_ENV_LEDGER_FILE, at));
468
- },
469
- async appendRecord(record) {
470
- const { appendLedgerLine, fetchLifecycle, pushLifecycleReport } = await import("../lifecycle/git");
471
- try {
472
- await fetchLifecycle(at).catch(() => false);
473
- await appendLedgerLine(PR_ENV_LEDGER_DIR, PR_ENV_LEDGER_FILE, JSON.stringify(record), `Pull request environment ${record.env} ${record.kind} (${record.result})`, at);
474
- const pushed = await pushLifecycleReport(at);
475
- if (pushed.pushWarning) warn(pushed.pushWarning);
476
- } catch (err) {
477
- warn(`could not record ${record.env}'s ${record.kind} on chant/lifecycle: ${messageOf(err)}`);
478
- }
479
- },
480
- async decideGate(input) {
481
- const { approvalOf, decidePrGate, readOnlyLedger } = await import("./pr-run");
482
- const { gitGateLedgerPort } = await import("../op/gate");
483
- const approval = approvalOf(await decidePrGate(readOnlyLedger(gitGateLedgerPort(at)), input));
484
- return { status: approval.status, ...(approval.approvedBy ? { approvedBy: approval.approvedBy } : {}) };
485
- },
486
- ...(forge ? { publish: forgePrEnvPublisher(forge, warn) } : {}),
487
- ...(forge?.pullRequestState ? { pullRequestState: (pr: number) => forge.pullRequestState!(pr) } : {}),
488
- ...(opts.log ? { log: opts.log } : {}),
489
- warn,
490
- };
491
- }
492
-
493
422
  // ── a teardown command ─────────────────────────────────────────────────────
494
423
 
495
424
  /** `ci.prEnvironment.teardown` (#4330): a command that tears down what no lexicon does. */
@@ -39,7 +39,8 @@
39
39
  * as a lexicon does.
40
40
  */
41
41
 
42
- 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";
43
44
  import type { OwnershipMarker } from "../ownership";
44
45
  import { executeTeardown, planTeardown, type TeardownPlan, type TeardownPlanEntry, type TeardownReport } from "../lifecycle/teardown";
45
46
  import type { ObservationLexicon, TeardownCandidate, TeardownEnumeration, TeardownExecution } from "../lexicon";
@@ -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
@@ -1,4 +1,5 @@
1
1
  import type { GraphIR, IRNode, IREdge } from "./graph-ir";
2
+ import { subTemplateRefs } from "./graph-sub-template";
2
3
 
3
4
  /**
4
5
  * Detail tiers — the diagram "detail dial". Each level is a pure IR → IR
@@ -60,13 +61,13 @@ function paintAttrs(attrs: Record<string, unknown>): Record<string, unknown> {
60
61
  return out;
61
62
  }
62
63
 
63
- /** A `{ $ref: "producer.attribute" }` envelope — a reference, not a property tree. */
64
+ /** A `{ $ref: "producer.attribute" }` or `{ $sub: "..." }` (#4373) envelope — a reference, not a property tree. */
64
65
  function isRefEnvelope(v: unknown): boolean {
65
66
  return (
66
67
  typeof v === "object" &&
67
68
  v !== null &&
68
69
  !Array.isArray(v) &&
69
- typeof (v as { $ref?: unknown }).$ref === "string"
70
+ (typeof (v as { $ref?: unknown }).$ref === "string" || typeof (v as { $sub?: unknown }).$sub === "string")
70
71
  );
71
72
  }
72
73
 
@@ -271,7 +272,8 @@ function toAttributes(ir: GraphIR): GraphIR {
271
272
  return { ...ir, edges };
272
273
  }
273
274
 
274
- /** Find the attribute in a `{ $ref: "producer.attribute" }` envelope under attrs. */
275
+ /** Find the attribute in a `{ $ref: "producer.attribute" }` envelope under attrs,
276
+ * or in a `{ $sub }` envelope's `${producer.attribute}` (#4373). */
275
277
  function findRefAttr(attrs: Record<string, unknown>, producer: string): string | undefined {
276
278
  let found: string | undefined;
277
279
  const visit = (v: unknown): void => {
@@ -288,6 +290,16 @@ function findRefAttr(attrs: Record<string, unknown>, producer: string): string |
288
290
  return;
289
291
  }
290
292
  }
293
+ const sub = (v as { $sub?: unknown; $vars?: unknown }).$sub;
294
+ if (typeof sub === "string") {
295
+ const vars = (v as { $vars?: unknown }).$vars;
296
+ const bound = new Set(vars && typeof vars === "object" ? Object.keys(vars) : []);
297
+ const hit = subTemplateRefs(sub, bound).find((r) => r.name === producer && r.attr);
298
+ if (hit) {
299
+ found = hit.attr;
300
+ return;
301
+ }
302
+ }
291
303
  for (const val of Object.values(v as Record<string, unknown>)) visit(val);
292
304
  };
293
305
  visit(attrs);