@intentius/chant 0.44.14 → 0.46.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 (220) hide show
  1. package/dist/audit/discover.d.ts +26 -2
  2. package/dist/audit/discover.d.ts.map +1 -1
  3. package/dist/audit/report-model.d.ts +10 -0
  4. package/dist/audit/report-model.d.ts.map +1 -1
  5. package/dist/audit/rules-doc.d.ts.map +1 -1
  6. package/dist/cli/build-params-cli.d.ts +16 -7
  7. package/dist/cli/build-params-cli.d.ts.map +1 -1
  8. package/dist/cli/commands/audit.d.ts +20 -0
  9. package/dist/cli/commands/audit.d.ts.map +1 -1
  10. package/dist/cli/commands/build.d.ts +9 -0
  11. package/dist/cli/commands/build.d.ts.map +1 -1
  12. package/dist/cli/commands/check-lexicon-docs.d.ts +13 -0
  13. package/dist/cli/commands/check-lexicon-docs.d.ts.map +1 -1
  14. package/dist/cli/commands/check-lexicon-examples.d.ts +24 -14
  15. package/dist/cli/commands/check-lexicon-examples.d.ts.map +1 -1
  16. package/dist/cli/commands/check-lexicon.d.ts +14 -0
  17. package/dist/cli/commands/check-lexicon.d.ts.map +1 -1
  18. package/dist/cli/commands/init-lexicon/templates/codegen.d.ts.map +1 -1
  19. package/dist/cli/commands/init-lexicon/templates/docs.d.ts +5 -0
  20. package/dist/cli/commands/init-lexicon/templates/docs.d.ts.map +1 -1
  21. package/dist/cli/commands/init-lexicon.d.ts.map +1 -1
  22. package/dist/cli/commands/lexicon-surface-diff.d.ts +6 -0
  23. package/dist/cli/commands/lexicon-surface-diff.d.ts.map +1 -1
  24. package/dist/cli/commands/lint.d.ts.map +1 -1
  25. package/dist/cli/commands/onboard.d.ts +28 -0
  26. package/dist/cli/commands/onboard.d.ts.map +1 -1
  27. package/dist/cli/handlers/build.d.ts.map +1 -1
  28. package/dist/cli/handlers/graph.d.ts.map +1 -1
  29. package/dist/cli/handlers/lifecycle.d.ts +13 -0
  30. package/dist/cli/handlers/lifecycle.d.ts.map +1 -1
  31. package/dist/cli/handlers/run.d.ts.map +1 -1
  32. package/dist/cli/handlers/search.d.ts +27 -0
  33. package/dist/cli/handlers/search.d.ts.map +1 -1
  34. package/dist/cli/main.d.ts.map +1 -1
  35. package/dist/cli/registry.d.ts +7 -0
  36. package/dist/cli/registry.d.ts.map +1 -1
  37. package/dist/codegen/docs-pages.d.ts +39 -0
  38. package/dist/codegen/docs-pages.d.ts.map +1 -0
  39. package/dist/codegen/docs-sections.d.ts.map +1 -1
  40. package/dist/codegen/docs-sidebar.d.ts +13 -2
  41. package/dist/codegen/docs-sidebar.d.ts.map +1 -1
  42. package/dist/codegen/docs-types.d.ts +30 -10
  43. package/dist/codegen/docs-types.d.ts.map +1 -1
  44. package/dist/codegen/docs.d.ts +14 -2
  45. package/dist/codegen/docs.d.ts.map +1 -1
  46. package/dist/codegen/lexicon-regen.d.ts +11 -0
  47. package/dist/codegen/lexicon-regen.d.ts.map +1 -1
  48. package/dist/codegen/surface-snapshot.d.ts +27 -3
  49. package/dist/codegen/surface-snapshot.d.ts.map +1 -1
  50. package/dist/codegen/validate.d.ts +21 -3
  51. package/dist/codegen/validate.d.ts.map +1 -1
  52. package/dist/components/capability-plugin.d.ts +13 -2
  53. package/dist/components/capability-plugin.d.ts.map +1 -1
  54. package/dist/components/driver.d.ts +38 -0
  55. package/dist/components/driver.d.ts.map +1 -1
  56. package/dist/components/index.d.ts +1 -1
  57. package/dist/components/index.d.ts.map +1 -1
  58. package/dist/components/starter-plugin.d.ts.map +1 -1
  59. package/dist/config.d.ts +85 -5
  60. package/dist/config.d.ts.map +1 -1
  61. package/dist/discovery/fold-import.d.ts.map +1 -1
  62. package/dist/env.d.ts +12 -1
  63. package/dist/env.d.ts.map +1 -1
  64. package/dist/fold/fold.d.ts +100 -1
  65. package/dist/fold/fold.d.ts.map +1 -1
  66. package/dist/graph-ir.d.ts +12 -0
  67. package/dist/graph-ir.d.ts.map +1 -1
  68. package/dist/graph-ops.d.ts +17 -0
  69. package/dist/graph-ops.d.ts.map +1 -0
  70. package/dist/lexicon.d.ts +182 -2
  71. package/dist/lexicon.d.ts.map +1 -1
  72. package/dist/lifecycle/change-set.d.ts +23 -2
  73. package/dist/lifecycle/change-set.d.ts.map +1 -1
  74. package/dist/lifecycle/deep-observe.d.ts.map +1 -1
  75. package/dist/lifecycle/index.d.ts +1 -0
  76. package/dist/lifecycle/index.d.ts.map +1 -1
  77. package/dist/lifecycle/observe.d.ts +8 -0
  78. package/dist/lifecycle/observe.d.ts.map +1 -1
  79. package/dist/lifecycle/replay.d.ts.map +1 -1
  80. package/dist/lifecycle/snapshot.d.ts.map +1 -1
  81. package/dist/lifecycle/teardown.d.ts +130 -0
  82. package/dist/lifecycle/teardown.d.ts.map +1 -0
  83. package/dist/lifecycle/types.d.ts +7 -0
  84. package/dist/lifecycle/types.d.ts.map +1 -1
  85. package/dist/lint/engine.d.ts +6 -2
  86. package/dist/lint/engine.d.ts.map +1 -1
  87. package/dist/lint/policy.d.ts.map +1 -1
  88. package/dist/lint/rule.d.ts +31 -0
  89. package/dist/lint/rule.d.ts.map +1 -1
  90. package/dist/lint/rules/cor021-env-literal-name.d.ts +3 -0
  91. package/dist/lint/rules/cor021-env-literal-name.d.ts.map +1 -0
  92. package/dist/lint/rules/index.d.ts +2 -1
  93. package/dist/lint/rules/index.d.ts.map +1 -1
  94. package/dist/managed-fields.d.ts +33 -21
  95. package/dist/managed-fields.d.ts.map +1 -1
  96. package/dist/observation.d.ts +23 -1
  97. package/dist/observation.d.ts.map +1 -1
  98. package/dist/op/builders.d.ts +36 -7
  99. package/dist/op/builders.d.ts.map +1 -1
  100. package/dist/op/index.d.ts +1 -1
  101. package/dist/op/index.d.ts.map +1 -1
  102. package/dist/testing.d.ts +136 -0
  103. package/dist/testing.d.ts.map +1 -0
  104. package/dist/yaml.d.ts +0 -8
  105. package/dist/yaml.d.ts.map +1 -1
  106. package/package.json +6 -1
  107. package/src/__snapshots__/okf.test.ts.snap +20 -0
  108. package/src/audit/discover.test.ts +55 -1
  109. package/src/audit/discover.ts +70 -2
  110. package/src/audit/edge-init-safety.test.ts +149 -18
  111. package/src/audit/report-model.ts +12 -1
  112. package/src/audit/rules-doc.ts +1 -0
  113. package/src/build-params.test.ts +52 -0
  114. package/src/cli/build-params-cli.test.ts +25 -8
  115. package/src/cli/build-params-cli.ts +37 -23
  116. package/src/cli/commands/__fixtures__/audit-coverage/.github/workflows/ci.yml +5 -0
  117. package/src/cli/commands/__fixtures__/audit-coverage/Dockerfile +2 -0
  118. package/src/cli/commands/__fixtures__/audit-coverage/infra/main.tf +3 -0
  119. package/src/cli/commands/__fixtures__/audit-coverage/infra/stack.json +6 -0
  120. package/src/cli/commands/__fixtures__/audit-coverage/k8s/deploy.yaml +12 -0
  121. package/src/cli/commands/__fixtures__/init-lexicon-output/docs/pages/getting-started.mdx +16 -0
  122. package/src/cli/commands/__fixtures__/init-lexicon-output/docs/src/content.config.ts +10 -2
  123. package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/generate-cli.ts +3 -1
  124. package/src/cli/commands/__fixtures__/init-lexicon-output/src/codegen/generate.ts +2 -1
  125. package/src/cli/commands/__snapshots__/init-lexicon.test.ts.snap +2 -1
  126. package/src/cli/commands/audit.test.ts +83 -1
  127. package/src/cli/commands/audit.ts +121 -28
  128. package/src/cli/commands/build.test.ts +275 -5
  129. package/src/cli/commands/build.ts +86 -14
  130. package/src/cli/commands/check-lexicon-docs.ts +30 -0
  131. package/src/cli/commands/check-lexicon-examples.test.ts +73 -7
  132. package/src/cli/commands/check-lexicon-examples.ts +66 -17
  133. package/src/cli/commands/check-lexicon.test.ts +46 -2
  134. package/src/cli/commands/check-lexicon.ts +64 -4
  135. package/src/cli/commands/init-lexicon/templates/codegen.ts +5 -2
  136. package/src/cli/commands/init-lexicon/templates/docs.ts +35 -2
  137. package/src/cli/commands/init-lexicon.test.ts +42 -1
  138. package/src/cli/commands/init-lexicon.ts +3 -1
  139. package/src/cli/commands/lexicon-rolling-upgrade.ts +1 -1
  140. package/src/cli/commands/lexicon-surface-diff.test.ts +1 -1
  141. package/src/cli/commands/lexicon-surface-diff.ts +10 -1
  142. package/src/cli/commands/lexicon-surface-diff.update.test.ts +112 -0
  143. package/src/cli/commands/lint.ts +19 -6
  144. package/src/cli/commands/onboard.test.ts +319 -225
  145. package/src/cli/commands/onboard.ts +144 -58
  146. package/src/cli/handlers/build.test.ts +3 -3
  147. package/src/cli/handlers/build.ts +2 -0
  148. package/src/cli/handlers/components.ts +2 -2
  149. package/src/cli/handlers/graph.test.ts +44 -0
  150. package/src/cli/handlers/graph.ts +24 -4
  151. package/src/cli/handlers/lifecycle.test.ts +348 -1
  152. package/src/cli/handlers/lifecycle.ts +235 -3
  153. package/src/cli/handlers/run.test.ts +5 -5
  154. package/src/cli/handlers/run.ts +1 -0
  155. package/src/cli/handlers/search-live.test.ts +217 -0
  156. package/src/cli/handlers/search.test.ts +41 -1
  157. package/src/cli/handlers/search.ts +60 -8
  158. package/src/cli/main.ts +17 -4
  159. package/src/cli/registry.ts +7 -0
  160. package/src/cli/security-boundary.test.ts +2 -1
  161. package/src/codegen/docs-pages.test.ts +194 -0
  162. package/src/codegen/docs-pages.ts +138 -0
  163. package/src/codegen/docs-sections.ts +0 -8
  164. package/src/codegen/docs-sidebar.ts +50 -37
  165. package/src/codegen/docs-types.ts +32 -4
  166. package/src/codegen/docs.ts +78 -40
  167. package/src/codegen/lexicon-regen.ts +22 -2
  168. package/src/codegen/rolling-upgrade.test.ts +1 -1
  169. package/src/codegen/rolling-upgrade.ts +1 -1
  170. package/src/codegen/surface-snapshot.test.ts +139 -0
  171. package/src/codegen/surface-snapshot.ts +297 -122
  172. package/src/codegen/validate.test.ts +49 -3
  173. package/src/codegen/validate.ts +36 -7
  174. package/src/components/auto-release.ts +1 -1
  175. package/src/components/capability-plugin.ts +23 -2
  176. package/src/components/driver.test.ts +45 -0
  177. package/src/components/driver.ts +64 -27
  178. package/src/components/index.ts +2 -0
  179. package/src/components/starter-plugin.ts +5 -2
  180. package/src/config.test.ts +40 -0
  181. package/src/config.ts +159 -8
  182. package/src/discovery/fold-import.test.ts +427 -2
  183. package/src/discovery/fold-import.ts +163 -7
  184. package/src/discovery/sandbox/driver.test.ts +11 -0
  185. package/src/env.test.ts +35 -1
  186. package/src/env.ts +17 -3
  187. package/src/fold/fold.test.ts +41 -0
  188. package/src/fold/fold.ts +372 -2
  189. package/src/graph-ir-live.test.ts +42 -0
  190. package/src/graph-ir.ts +42 -2
  191. package/src/graph-ops.test.ts +69 -0
  192. package/src/graph-ops.ts +42 -0
  193. package/src/lexicon.ts +182 -2
  194. package/src/lifecycle/change-set.test.ts +41 -0
  195. package/src/lifecycle/change-set.ts +30 -2
  196. package/src/lifecycle/deep-observe.ts +4 -0
  197. package/src/lifecycle/index.ts +1 -0
  198. package/src/lifecycle/observe.ts +29 -2
  199. package/src/lifecycle/replay.ts +3 -0
  200. package/src/lifecycle/snapshot.ts +3 -0
  201. package/src/lifecycle/teardown.test.ts +537 -0
  202. package/src/lifecycle/teardown.ts +357 -0
  203. package/src/lifecycle/types.ts +7 -0
  204. package/src/lint/engine.ts +7 -1
  205. package/src/lint/policy.ts +14 -3
  206. package/src/lint/rule.ts +23 -0
  207. package/src/lint/rules/cor021-env-literal-name.test.ts +128 -0
  208. package/src/lint/rules/cor021-env-literal-name.ts +114 -0
  209. package/src/lint/rules/index.ts +4 -1
  210. package/src/managed-fields.test.ts +15 -42
  211. package/src/managed-fields.ts +43 -26
  212. package/src/observation.test.ts +24 -8
  213. package/src/observation.ts +48 -4
  214. package/src/op/builders.ts +40 -7
  215. package/src/op/index.ts +1 -1
  216. package/src/ownership.test.ts +63 -1
  217. package/src/testing.test.ts +261 -0
  218. package/src/testing.ts +338 -0
  219. package/src/yaml.test.ts +15 -0
  220. package/src/yaml.ts +19 -2
@@ -1,6 +1,6 @@
1
1
  import { resolve } from "node:path";
2
2
  import { build } from "../../build";
3
- import { buildGraphIr, buildLiveGraphIr, sourceOverlayGraphs, type GraphIR, type IRNode, type IREdge } from "../../graph-ir";
3
+ import { buildGraphIr, buildLiveGraphIr, collectUnobserved, sourceOverlayGraphs, type GraphIR, type IRNode, type IREdge } from "../../graph-ir";
4
4
  import { buildDeclaredPerStack } from "../../graph-declared";
5
5
  import { enrichEffectiveTopology } from "../../graph-effective";
6
6
  import { reconstructEdges, mergeCatalogs, type ReferenceCatalog } from "../../graph-refs";
@@ -9,7 +9,7 @@ import { discover } from "../../discovery/index";
9
9
  import { observeResources } from "../../lifecycle/observe";
10
10
  import { replaySnapshots, hasSnapshot } from "../../lifecycle/replay";
11
11
  import type { LiveObservation } from "../../graph-ir";
12
- import { loadChantConfig } from "../../config";
12
+ import { loadChantConfig, matchesDeclaredEnvironment } from "../../config";
13
13
  import { loadPlugins, resolveProjectLexicons } from "../plugins";
14
14
  import { formatError, formatWarning } from "../format";
15
15
  import type { CommandContext } from "../registry";
@@ -61,6 +61,13 @@ export async function runSearch(ctx: CommandContext): Promise<number> {
61
61
 
62
62
  let ir: GraphIR;
63
63
  let source: AnswerSource = { kind: "declared" };
64
+ // Lexicons whose live read threw (#1263). "Nothing observed" and "could not
65
+ // observe" are different claims; this is what carries the second one.
66
+ const liveFailures: string[] = [];
67
+ // Run-level notes from the read (#1265) — "ownership could not be filtered
68
+ // on this path" — printed with the provenance footer, after the rows, so the
69
+ // answer is not preceded by what qualifies it.
70
+ let liveNotes: string[] = [];
64
71
  // Kinds that can exist in the account without being declared (#1278). Known
65
72
  // without a scan, so it costs nothing to mention.
66
73
  let ambientKinds: string[] = [];
@@ -82,7 +89,10 @@ export async function runSearch(ctx: CommandContext): Promise<number> {
82
89
  }));
83
90
  return 1;
84
91
  }
85
- if (config.environments && !config.environments.includes(environment)) {
92
+ // Membership via matchesDeclaredEnvironment (#1221): handles `{ name,
93
+ // endpoint }` entries (#1166) — a plain `.includes` never matched those —
94
+ // and glob-pattern entries like `"pr-*"`.
95
+ if (config.environments && config.environments.length > 0 && !matchesDeclaredEnvironment(config.environments, environment)) {
86
96
  console.error(formatError({ message: `Unknown environment "${environment}"` }));
87
97
  return 1;
88
98
  }
@@ -122,8 +132,15 @@ export async function runSearch(ctx: CommandContext): Promise<number> {
122
132
  stacks,
123
133
  ambient: args.ambient === true,
124
134
  });
125
- for (const e of observed.errors) console.error(formatWarning({ message: e }));
135
+ // A thrown read is not a warning. Printed as one, it sat between the
136
+ // ownership-filter notices that also print on a working run and carried
137
+ // no signal (#1263). Name the lexicon and the cause, as an error.
138
+ for (const e of observed.errors) {
139
+ liveFailures.push(e);
140
+ console.error(formatError({ message: `live read failed — ${e}` }));
141
+ }
126
142
  observations = observed.observations;
143
+ liveNotes = observed.notes ?? [];
127
144
  source = { kind: "live" };
128
145
  }
129
146
  let live = buildLiveGraphIr(observations);
@@ -189,7 +206,10 @@ export async function runSearch(ctx: CommandContext): Promise<number> {
189
206
  stacks.length > 0
190
207
  ? await buildDeclaredPerStack(stacks, projectPath)
191
208
  : buildGraphIr((await discover(resolve(args.src ?? config.sourceDir ?? "."))).entities, projectPath);
192
- ir = sourceOverlayGraphs(declared, live);
209
+ // Carry the NOT-OBSERVED half of the tri-state (#1089) onto the rows, so a
210
+ // declared entity nobody could read is painted `_unobserved` and a row can
211
+ // say so instead of printing blank where a physical id would go (#1263).
212
+ ir = sourceOverlayGraphs(declared, live, { unobserved: collectUnobserved(observations) });
193
213
  // Containment goes on AFTER the overlay, not through it.
194
214
  //
195
215
  // `sourceOverlayGraphs` admits a live edge only when one end is foreign,
@@ -242,6 +262,7 @@ export async function runSearch(ctx: CommandContext): Promise<number> {
242
262
  console.log("(no matches)");
243
263
  availableAttrs(terms, ir);
244
264
  if (args.explain) explain(terms, matches, ir, nodeById, query);
265
+ for (const n of liveNotes) console.error(formatWarning({ message: n }));
245
266
  return 0;
246
267
  }
247
268
  for (const n of matches) {
@@ -254,12 +275,28 @@ export async function runSearch(ctx: CommandContext): Promise<number> {
254
275
  source.kind === "live" && !matches.some((n) => n.physicalId) && args.env
255
276
  ? (await hasSnapshot(String(args.env))) ? "yes" : undefined
256
277
  : undefined;
257
- provenance(matches, source, recorded);
278
+ provenance(matches, source, recorded, liveFailures);
279
+ // Qualifies the provenance line, so it sits with it: one line per distinct
280
+ // note for the whole run, not one per stack, and after the rows (#1265).
281
+ for (const n of liveNotes) console.error(formatWarning({ message: n }));
258
282
  ambientHint(matches, ambientKinds, args.ambient === true, replayAmbient);
259
283
  showMiss(matches, show);
260
284
  regionSpread(terms, matches, show);
261
285
  derivedSurface(terms, matches, ir, backed);
262
286
  if (args.explain) explain(terms, matches, ir, nodeById, query);
287
+ // The caller asked for a live answer and at least one lexicon could not be
288
+ // read. The rows above are still printed — they are what the source declares,
289
+ // labelled as such — but the command did not do what it was asked, and a
290
+ // script or agent needs to see that without parsing the footer (#1263).
291
+ if (liveFailures.length > 0) {
292
+ console.error(formatError({
293
+ message: `live read failed for ${liveFailures.length} lexicon${liveFailures.length === 1 ? "" : "s"} — answer is declared-only`,
294
+ hint: recorded
295
+ ? "answer from the recorded snapshot with --at latest, or drop --live for a declared-only query"
296
+ : "drop --live for a declared-only query, or record a snapshot with chant lifecycle snapshot",
297
+ }));
298
+ return 1;
299
+ }
263
300
  return 0;
264
301
  }
265
302
 
@@ -379,13 +416,24 @@ function ambientHint(
379
416
  * already done. That is a fact about the query, printed for every query, and it
380
417
  * encodes no expected answer.
381
418
  */
382
- function provenance(matches: IRNode[], source: AnswerSource, recorded?: string): void {
419
+ function provenance(matches: IRNode[], source: AnswerSource, recorded?: string, liveFailures: string[] = []): void {
383
420
  if (source.kind === "declared") {
384
421
  console.log("— declared only · no observation · physical ids unavailable");
385
422
  return;
386
423
  }
387
424
  const bound = matches.filter((n) => n.physicalId).length;
388
425
  const what = source.kind === "live" ? "live read" : "snapshot";
426
+ if (liveFailures.length > 0) {
427
+ // Could not observe, as distinct from observed nothing. The rows carry the
428
+ // per-entity verdict; this names the lexicons that failed in one line.
429
+ const lexicons = liveFailures.map((e) => e.split(":")[0]).join(", ");
430
+ const unobserved = matches.filter((n) => (n.attrs as Record<string, unknown> | undefined)?._unobserved).length;
431
+ const rest = recorded
432
+ ? "a snapshot of this environment is recorded — answer from it with --at latest"
433
+ : "answered from the declared graph · physical ids unavailable";
434
+ console.log(`— live read failed (${lexicons}) · ${unobserved}/${matches.length} rows unobserved · ${rest}`);
435
+ return;
436
+ }
389
437
  if (bound === 0) {
390
438
  // The estate was asked for and nothing came back bound. Naming it is the
391
439
  // difference between "these do not exist" and "nobody could see them".
@@ -742,6 +790,10 @@ function formatRow(n: IRNode, show: string[]): string {
742
790
  // skip source-mode AttrRef placeholders (objects).
743
791
  const physical = (n as { physicalId?: unknown }).physicalId ?? attrs["physicalId"] ?? attrs["InstanceId"] ?? attrs["Id"];
744
792
  if (physical != null && typeof physical !== "object") parts.push(String(physical));
793
+ // A row nobody could read says so where its physical id would go (#1263). A
794
+ // blank there reads as "declared, not provisioned", which the read never
795
+ // established; the reason is the tri-state's (#1089).
796
+ else if (typeof attrs["_unobserved"] === "string") parts.push(`(unobserved: ${attrs["_unobserved"]})`);
745
797
  for (const key of show) {
746
798
  // Match the name case-insensitively, and report what was actually found.
747
799
  // AWS attribute names are PascalCase and chant's derived ones are not, so a
@@ -763,4 +815,4 @@ function formatRow(n: IRNode, show: string[]): string {
763
815
  }
764
816
 
765
817
  /** Internals exposed for unit tests. */
766
- export const __searchInternals = { parseQuery, matchTerm, formatRow, explain, describeTerm, derivedSurface, availableAttrs, ambientHint, regionSpread, showMiss };
818
+ export const __searchInternals = { parseQuery, matchTerm, formatRow, explain, describeTerm, derivedSurface, availableAttrs, ambientHint, regionSpread, showMiss, provenance };
package/src/cli/main.ts CHANGED
@@ -23,7 +23,7 @@ import { runCarveAdvise, runCarveUnknown } from "./handlers/carve";
23
23
  import { runCarveEmit } from "./handlers/carve-emit";
24
24
  import { runCarveBridge } from "./handlers/carve-bridge";
25
25
  import { runCarveApply } from "./handlers/carve-apply";
26
- import { runLifecycleSnapshot, runLifecycleShow, runLifecycleDiff, runLifecycleRollback, runLifecyclePlan, runLifecycleAffected, runLifecycleLog, runLifecycleUnknown } from "./handlers/lifecycle";
26
+ import { runLifecycleSnapshot, runLifecycleShow, runLifecycleDiff, runLifecycleRollback, runLifecyclePlan, runLifecycleAffected, runLifecycleLog, runLifecycleTeardown, runLifecycleUnknown } from "./handlers/lifecycle";
27
27
  import { runComponentsStatus, runComponentsReleaseRecord, runComponentsUnknown } from "./handlers/components";
28
28
  import { runGraph } from "./handlers/graph";
29
29
  import { runExplain } from "./handlers/explain";
@@ -78,6 +78,8 @@ const BOOLEAN_FLAGS = new Set([
78
78
  "--fold",
79
79
  "--no-fold",
80
80
  "--sandbox",
81
+ "--yes",
82
+ "--confirm-prod",
81
83
  ]);
82
84
 
83
85
  /**
@@ -228,6 +230,10 @@ export function parseArgs(args: string[]): ParsedArgs {
228
230
  result.emit = args[++i];
229
231
  } else if (arg === "--dry-run") {
230
232
  result.dryRun = true;
233
+ } else if (arg === "--yes") {
234
+ result.yes = true;
235
+ } else if (arg === "--confirm-prod") {
236
+ result.confirmProd = true;
231
237
  } else if (arg === "--strict") {
232
238
  result.strict = true;
233
239
  } else if (arg === "--validate") {
@@ -484,6 +490,10 @@ Lifecycle (alias: lc):
484
490
  lifecycle plan <env> Typed change set (create/update/delete/adopt) vs live
485
491
  lifecycle affected Stacks a change affects (--base <ref> [--include-dependents])
486
492
  --json: emit the ChangeSet as JSON
493
+ lifecycle teardown <env> Plan what deleting the environment would remove —
494
+ marker-scoped (this project's stack + env); --yes
495
+ executes the plan (production-like names also need
496
+ --confirm-prod, or an interactive confirmation)
487
497
  lifecycle log [env] History of lifecycle snapshots
488
498
 
489
499
  Component release ledger + status:
@@ -542,7 +552,9 @@ Options:
542
552
  --fix Auto-fix fixable issues (lint command)
543
553
  --force Force overwrite existing files (import command)
544
554
  -w, --watch Watch for changes and rebuild/re-lint (build, lint)
545
- -v, --verbose Show stack traces on errors
555
+ -v, --verbose Show stack traces on errors; (build) list every
556
+ resolved build parameter and per-file fold decision
557
+ instead of the one-line summaries
546
558
  -h, --help Show this help message
547
559
  -p, --profile <name> Temporal worker profile to use (run command)
548
560
  --local Run an Op with the local in-process executor (default)
@@ -572,8 +584,8 @@ Options:
572
584
  composite factory calls (#1022/#1023), falling back
573
585
  to run per-file for anything else outside the fold
574
586
  subset (a cross-file-only reference, a re-export,
575
- \`export default\`, ...). Logs which path each file
576
- took. DEFAULT since #1134 — this flag forces it on
587
+ \`export default\`, ...). Logs a fold/run count
588
+ (per-file lines under --verbose). DEFAULT since #1134 — this flag forces it on
577
589
  over a chant.config.ts \`build.fold: false\`.
578
590
  --no-fold (build) Opt out of folding for this invocation: every
579
591
  source module is imported and run, the pre-#1134
@@ -778,6 +790,7 @@ const registry: CommandDef[] = [
778
790
  { name: "lifecycle rollback", handler: runLifecycleRollback },
779
791
  { name: "lifecycle plan", requiresPlugins: true, handler: runLifecyclePlan },
780
792
  { name: "lifecycle affected", requiresPlugins: true, handler: runLifecycleAffected },
793
+ { name: "lifecycle teardown", requiresPlugins: true, handler: runLifecycleTeardown },
781
794
  { name: "lifecycle log", handler: runLifecycleLog },
782
795
 
783
796
  // Component release ledger + status surface (#568, epic #551)
@@ -85,6 +85,13 @@ export interface ParsedArgs {
85
85
  namespace?: string;
86
86
  /** `chant lifecycle rollback --dry-run` — compute the rollback delta and print it; open no PR, push nothing, leave no branch. */
87
87
  dryRun?: boolean;
88
+ /** `chant lifecycle teardown <env> --yes` — execute the planned deletion
89
+ * (#1222). Without it the command plans and stops. */
90
+ yes?: boolean;
91
+ /** `chant lifecycle teardown <env> --yes --confirm-prod` — the non-interactive
92
+ * form of the extra confirmation a production-like environment name demands
93
+ * (#1222). Meaningless without `--yes`. */
94
+ confirmProd?: boolean;
88
95
  /** `chant import --verbatim` — keep server-defaulted fields in live import */
89
96
  verbatim?: boolean;
90
97
  /** `chant lifecycle … --src <dir>` — build root override for lifecycle commands */
@@ -99,7 +99,8 @@ describe("CLI end-to-end — resolveProjectLexicons must not execute project sou
99
99
  }
100
100
 
101
101
  test("`chant build --fold` on a project with no `lexicons` config does not execute the hostile file", () => {
102
- const { status, stdout, stderr } = runCli(["build", testDir, "--fold"]);
102
+ // --verbose: the per-file fold decision lines are behind it (#1424).
103
+ const { status, stdout, stderr } = runCli(["build", testDir, "--fold", "--verbose"]);
103
104
 
104
105
  expect(stderr).not.toMatch(/EXFIL/);
105
106
  expect(stdout).not.toMatch(/EXFIL/);
@@ -0,0 +1,194 @@
1
+ import { mkdtempSync, mkdirSync, rmSync, writeFileSync, existsSync, readFileSync } from "fs";
2
+ import { tmpdir } from "os";
3
+ import { join } from "path";
4
+ import { afterEach, beforeEach, describe, expect, test } from "vitest";
5
+
6
+ import { readAuthoredPages } from "./docs-pages";
7
+ import { docsPipeline, writeDocsSite, GENERATED_MARKER_TAG } from "./docs";
8
+ import { buildSidebar, quadrantItems } from "./docs-sidebar";
9
+ import type { DocsConfig, DocsResult, SidebarPage } from "./docs-types";
10
+
11
+ let root: string;
12
+
13
+ function config(overrides: Partial<DocsConfig> = {}): DocsConfig {
14
+ return {
15
+ name: "fixture",
16
+ displayName: "Fixture",
17
+ description: "fixture lexicon",
18
+ distDir: join(root, "dist"),
19
+ outDir: join(root, "docs"),
20
+ srcDir: join(root, "src"),
21
+ examplesDir: join(root, "examples"),
22
+ basePath: "/chant/lexicons/fixture/",
23
+ ...overrides,
24
+ };
25
+ }
26
+
27
+ function page(name: string, frontmatter: string, body = "Body.\n"): void {
28
+ mkdirSync(join(root, "docs", "pages"), { recursive: true });
29
+ writeFileSync(join(root, "docs", "pages", name), `---\n${frontmatter}\n---\n\n${body}`);
30
+ }
31
+
32
+ beforeEach(() => {
33
+ root = mkdtempSync(join(tmpdir(), "chant-docs-pages-"));
34
+ mkdirSync(join(root, "dist"), { recursive: true });
35
+ mkdirSync(join(root, "src"), { recursive: true });
36
+ mkdirSync(join(root, "examples"), { recursive: true });
37
+ writeFileSync(
38
+ join(root, "dist", "manifest.json"),
39
+ JSON.stringify({ name: "fixture", version: "0.0.0", intrinsics: [{ name: "ref", description: "a ref" }] }),
40
+ );
41
+ writeFileSync(join(root, "dist", "meta.json"), JSON.stringify({}));
42
+ writeFileSync(join(root, "examples", "snippet.ts"), "export const x = 1;\n");
43
+ });
44
+
45
+ afterEach(() => {
46
+ rmSync(root, { recursive: true, force: true });
47
+ });
48
+
49
+ describe("readAuthoredPages", () => {
50
+ test("returns nothing when docs/pages/ does not exist", () => {
51
+ expect(readAuthoredPages(config())).toEqual([]);
52
+ });
53
+
54
+ test("reads frontmatter, expands file markers, strips sidebar-only keys", () => {
55
+ page(
56
+ "getting-started.mdx",
57
+ 'title: "Getting Started"\ndescription: "Start here"\ndiataxis: tutorial\norder: 1\ngroup: "Intro"\nlabel: "Start"\nhidden: false',
58
+ "Before\n\n{{file:snippet.ts}}\n\nAfter\n",
59
+ );
60
+ const [p] = readAuthoredPages(config());
61
+ expect(p.slug).toBe("getting-started");
62
+ expect(p.quadrant).toBe("tutorial");
63
+ expect(p.order).toBe(1);
64
+ expect(p.group).toBe("Intro");
65
+ expect(p.label).toBe("Start");
66
+ expect(p.hidden).toBe(false);
67
+ expect(p.content).toContain('```typescript title="snippet.ts"\nexport const x = 1;\n```');
68
+ expect(p.content).toContain("diataxis: tutorial");
69
+ expect(p.content).not.toMatch(/^(order|group|label|hidden):/m);
70
+ expect(p.content).toContain('title: "Getting Started"');
71
+ });
72
+
73
+ test("label defaults to title", () => {
74
+ page("x.mdx", "title: Plain Title\ndiataxis: reference");
75
+ expect(readAuthoredPages(config())[0].label).toBe("Plain Title");
76
+ });
77
+
78
+ test("missing diataxis throws with the file path", () => {
79
+ page("untagged.mdx", "title: Untagged");
80
+ expect(() => readAuthoredPages(config())).toThrow(/untagged\.mdx.*diataxis/);
81
+ });
82
+
83
+ test("unknown diataxis value throws", () => {
84
+ page("bad.mdx", "title: Bad\ndiataxis: guide");
85
+ expect(() => readAuthoredPages(config())).toThrow(/"guide" is not one of/);
86
+ });
87
+ });
88
+
89
+ describe("docsPipeline with authored pages", () => {
90
+ test("authored page wins a slug collision with a generated page", () => {
91
+ page("intrinsics.mdx", "title: My Intrinsics\ndiataxis: reference", "Hand-written.\n");
92
+ const result = docsPipeline(config());
93
+ expect(result.pages.get("intrinsics.mdx")).toContain("Hand-written.");
94
+ expect(result.sidebarPages.filter((p) => p.slug === "intrinsics")).toHaveLength(1);
95
+ });
96
+
97
+ test("generated reference pages land in the reference quadrant after authored ones", () => {
98
+ page("howto.mdx", "title: Do It\ndiataxis: how-to");
99
+ const result = docsPipeline(config());
100
+ const ref = result.sidebarPages.filter((p) => p.quadrant === "reference").map((p) => p.slug);
101
+ expect(ref).toEqual(["intrinsics", "serialization"]);
102
+ expect(result.sidebarPages.find((p) => p.slug === "howto")?.quadrant).toBe("how-to");
103
+ });
104
+
105
+ test("hidden pages are written but kept out of the sidebar", () => {
106
+ page("secret.mdx", "title: Secret\ndiataxis: reference\nhidden: true");
107
+ const result = docsPipeline(config());
108
+ expect(result.pages.has("secret.mdx")).toBe(true);
109
+ expect(result.sidebarPages.some((p) => p.slug === "secret")).toBe(false);
110
+ });
111
+
112
+ test("authored pages carry a provenance marker naming the source file", () => {
113
+ page("howto.mdx", "title: Do It\ndiataxis: how-to");
114
+ const result = docsPipeline(config());
115
+ const out = result.pages.get("howto.mdx") as string;
116
+ expect(out).toContain(GENERATED_MARKER_TAG);
117
+ expect(out).toContain("docs/pages/howto.mdx");
118
+ });
119
+
120
+ test("writeDocsSite reaps the written copy once the source is removed", () => {
121
+ page("howto.mdx", "title: Do It\ndiataxis: how-to");
122
+ const cfg = config();
123
+ writeDocsSite(cfg, docsPipeline(cfg));
124
+ const written = join(root, "docs", "src", "content", "docs", "howto.mdx");
125
+ expect(existsSync(written)).toBe(true);
126
+ rmSync(join(root, "docs", "pages", "howto.mdx"));
127
+ writeDocsSite(cfg, docsPipeline(cfg));
128
+ expect(existsSync(written)).toBe(false);
129
+ const astro = readFileSync(join(root, "docs", "astro.config.mjs"), "utf-8");
130
+ expect(astro).not.toContain("howto");
131
+ const schema = readFileSync(join(root, "docs", "src", "content.config.ts"), "utf-8");
132
+ expect(schema).toContain("diataxis");
133
+ });
134
+ });
135
+
136
+ describe("buildSidebar", () => {
137
+ const sb = (pages: SidebarPage[]) =>
138
+ buildSidebar(config(), { pages: new Map(), sidebarPages: pages, stats: { resources: 0, properties: 0, services: 0, rules: 0, intrinsics: 0 } } satisfies DocsResult);
139
+
140
+ test("groups in fixed quadrant order and omits empty quadrants", () => {
141
+ const items = sb([
142
+ { slug: "why", label: "Why", quadrant: "explanation" },
143
+ { slug: "start", label: "Start", quadrant: "tutorial" },
144
+ { slug: "rules", label: "All Rules", quadrant: "reference", order: 1000 },
145
+ ]);
146
+ expect(items.map((i) => i.label)).toEqual(["← chant docs", "Overview", "Tutorials", "Reference", "Explanation"]);
147
+ });
148
+
149
+ test("orders by order then label and nests groups after loose pages", () => {
150
+ const items = quadrantItems([
151
+ { slug: "b", label: "Bravo", quadrant: "reference" },
152
+ { slug: "a", label: "Alpha", quadrant: "reference" },
153
+ { slug: "z", label: "Zulu", quadrant: "reference", order: 0 },
154
+ { slug: "eks", label: "EKS", quadrant: "reference", group: "Vendor" },
155
+ { slug: "aks", label: "AKS", quadrant: "reference", group: "Vendor" },
156
+ ]);
157
+ expect(items).toEqual([
158
+ { label: "Zulu", slug: "z" },
159
+ { label: "Alpha", slug: "a" },
160
+ { label: "Bravo", slug: "b" },
161
+ { label: "Vendor", items: [{ label: "AKS", slug: "aks" }, { label: "EKS", slug: "eks" }] },
162
+ ]);
163
+ });
164
+ });
165
+
166
+ /**
167
+ * chant #1377 — `dist/manifest.json` is written by `npm run bundle` (prepack
168
+ * only), so its version is whatever was last bundled on the machine; on one
169
+ * checkout it put the fountain docs two minor versions behind what was
170
+ * already committed. `package.json` is the source of truth, so the rendered
171
+ * version comes from there and the manifest is only a fallback.
172
+ */
173
+ describe("lexicon version in generated docs comes from package.json (#1377)", () => {
174
+ test("package.json beside dist/ wins over a stale manifest", () => {
175
+ writeFileSync(join(root, "package.json"), JSON.stringify({ name: "fixture", version: "0.37.2" }));
176
+ const index = docsPipeline(config()).pages.get("index.mdx");
177
+ expect(index).toContain("**Lexicon version:** 0.37.2");
178
+ expect(index).not.toContain("0.0.0");
179
+ });
180
+
181
+ test("packageJsonPath overrides the default location", () => {
182
+ mkdirSync(join(root, "elsewhere"), { recursive: true });
183
+ writeFileSync(join(root, "elsewhere", "package.json"), JSON.stringify({ version: "1.2.3" }));
184
+ const index = docsPipeline(config({ packageJsonPath: join(root, "elsewhere", "package.json") })).pages.get(
185
+ "index.mdx",
186
+ );
187
+ expect(index).toContain("**Lexicon version:** 1.2.3");
188
+ });
189
+
190
+ test("falls back to the manifest when no package.json is present", () => {
191
+ const index = docsPipeline(config()).pages.get("index.mdx");
192
+ expect(index).toContain("**Lexicon version:** 0.0.0");
193
+ });
194
+ });
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Authored lexicon doc pages (chant #1731 / #1733).
3
+ *
4
+ * A lexicon's prose lives as `.mdx` files under `lexicons/<name>/docs/pages/`.
5
+ * Each carries a `diataxis` frontmatter field naming its Diátaxis quadrant
6
+ * (https://diataxis.fr). The pipeline expands `{{file:...}}` markers, stamps a
7
+ * provenance marker and writes the result into `src/content/docs/`, and the
8
+ * sidebar is grouped from the field. This replaced two older modes — prose
9
+ * inside `docs.ts` template literals (`extraPages`) and hand-written content
10
+ * pages wired through `sidebarExtra` — removed in chant #1757.
11
+ *
12
+ * Frontmatter recognised here, on top of Starlight's `title` / `description`:
13
+ *
14
+ * - `diataxis`: `tutorial | how-to | reference | explanation`. Required.
15
+ * - `label`: sidebar label; defaults to `title`.
16
+ * - `group`: a nested subgroup label inside the quadrant (e.g. "Vendor Composites").
17
+ * - `order`: number; lower sorts first within its group. Unordered pages follow, by label.
18
+ * - `hidden`: `true` keeps the page out of the sidebar (reachable by URL only).
19
+ *
20
+ * `group` / `order` / `hidden` / `label` are stripped from the written copy so
21
+ * Starlight's schema never sees them; `diataxis` stays, and the generated
22
+ * `content.config.ts` declares it.
23
+ */
24
+
25
+ import { existsSync, readdirSync, readFileSync } from "fs";
26
+ import { join } from "path";
27
+
28
+ import { expandFileMarkers } from "./docs-file-markers";
29
+ import type { DocsConfig, Quadrant, SidebarPage } from "./docs-types";
30
+
31
+ export const QUADRANTS: readonly Quadrant[] = ["tutorial", "how-to", "reference", "explanation"];
32
+
33
+ /** Sidebar group label for each quadrant, in display order. */
34
+ export const QUADRANT_LABELS: Record<Quadrant, string> = {
35
+ tutorial: "Tutorials",
36
+ "how-to": "How-to guides",
37
+ reference: "Reference",
38
+ explanation: "Explanation",
39
+ };
40
+
41
+ export interface AuthoredPage extends SidebarPage {
42
+ /** Source file name under `pagesDir`, e.g. `getting-started.mdx`. */
43
+ file: string;
44
+ /** Page body with the rewritten frontmatter, markers expanded. */
45
+ content: string;
46
+ }
47
+
48
+ interface Frontmatter {
49
+ fields: Map<string, string>;
50
+ /** Raw frontmatter lines, in order, so unknown keys survive the rewrite. */
51
+ lines: string[];
52
+ body: string;
53
+ }
54
+
55
+ function parseFrontmatter(text: string, file: string): Frontmatter {
56
+ if (!text.startsWith("---\n")) {
57
+ throw new Error(`${file}: authored doc page must start with a frontmatter block`);
58
+ }
59
+ const end = text.indexOf("\n---", 4);
60
+ if (end === -1) throw new Error(`${file}: unterminated frontmatter`);
61
+ const lines = text.slice(4, end).split("\n");
62
+ const fields = new Map<string, string>();
63
+ for (const line of lines) {
64
+ const m = line.match(/^([A-Za-z_][\w-]*):\s*(.*)$/);
65
+ if (m) fields.set(m[1], m[2].trim());
66
+ }
67
+ // Skip the closing `---` and the newline after it.
68
+ const afterClose = text.indexOf("\n", end + 1);
69
+ const body = afterClose === -1 ? "" : text.slice(afterClose + 1);
70
+ return { fields, lines, body };
71
+ }
72
+
73
+ function unquote(v: string): string {
74
+ if ((v.startsWith('"') && v.endsWith('"')) || (v.startsWith("'") && v.endsWith("'"))) {
75
+ try {
76
+ return v.startsWith('"') ? (JSON.parse(v) as string) : v.slice(1, -1);
77
+ } catch {
78
+ return v.slice(1, -1);
79
+ }
80
+ }
81
+ return v;
82
+ }
83
+
84
+ /** Keys this module consumes and removes from the written page. */
85
+ const SIDEBAR_KEYS = new Set(["label", "group", "order", "hidden"]);
86
+
87
+ /**
88
+ * Read every authored page under `pagesDir`. Returns an empty list when the
89
+ * directory does not exist, which is every lexicon before it migrates.
90
+ */
91
+ export function readAuthoredPages(config: DocsConfig): AuthoredPage[] {
92
+ const pagesDir = config.pagesDir ?? join(config.outDir, "pages");
93
+ if (!existsSync(pagesDir)) return [];
94
+
95
+ const pages: AuthoredPage[] = [];
96
+ for (const file of readdirSync(pagesDir).sort()) {
97
+ if (!file.endsWith(".mdx") && !file.endsWith(".md")) continue;
98
+ const path = join(pagesDir, file);
99
+ const { fields, lines, body } = parseFrontmatter(readFileSync(path, "utf-8"), path);
100
+
101
+ const title = fields.get("title");
102
+ if (!title) throw new Error(`${path}: frontmatter needs a title`);
103
+ const quadrant = fields.get("diataxis");
104
+ if (!quadrant) {
105
+ throw new Error(
106
+ `${path}: frontmatter needs \`diataxis: ${QUADRANTS.join(" | ")}\` (https://diataxis.fr)`,
107
+ );
108
+ }
109
+ if (!QUADRANTS.includes(quadrant as Quadrant)) {
110
+ throw new Error(`${path}: diataxis "${quadrant}" is not one of ${QUADRANTS.join(", ")}`);
111
+ }
112
+
113
+ const orderRaw = fields.get("order");
114
+ const order = orderRaw === undefined ? undefined : Number(orderRaw);
115
+ if (order !== undefined && Number.isNaN(order)) {
116
+ throw new Error(`${path}: order must be a number, got "${orderRaw}"`);
117
+ }
118
+
119
+ const kept = lines.filter((line) => {
120
+ const key = line.match(/^([A-Za-z_][\w-]*):/)?.[1];
121
+ return !(key && SIDEBAR_KEYS.has(key));
122
+ });
123
+ let content = body;
124
+ if (config.examplesDir) content = expandFileMarkers(content, config.examplesDir);
125
+
126
+ pages.push({
127
+ file,
128
+ slug: file.replace(/\.mdx?$/, ""),
129
+ label: unquote(fields.get("label") ?? title),
130
+ quadrant: quadrant as Quadrant,
131
+ group: fields.has("group") ? unquote(fields.get("group") as string) : undefined,
132
+ order,
133
+ hidden: fields.get("hidden") === "true",
134
+ content: ["---", ...kept, "---", "", content].join("\n"),
135
+ });
136
+ }
137
+ return pages;
138
+ }
@@ -42,14 +42,6 @@ export function generateOverview(
42
42
 
43
43
  const suppress = new Set(config.suppressPages ?? []);
44
44
 
45
- // Extra pages listed first in reference links
46
- if (config.extraPages && config.extraPages.length > 0) {
47
- for (const page of config.extraPages) {
48
- if (page.sidebar === false) continue;
49
- lines.push(`- [${page.title}](./${page.slug})`);
50
- }
51
- }
52
-
53
45
  if (!suppress.has("intrinsics") && manifest.intrinsics && manifest.intrinsics.length > 0) {
54
46
  lines.push(
55
47
  `- [Intrinsic Functions](./intrinsics) — ${manifest.intrinsics.length} built-in functions`,