@intentius/chant 0.45.0 → 0.49.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 (223) hide show
  1. package/dist/audit/core.d.ts +21 -3
  2. package/dist/audit/core.d.ts.map +1 -1
  3. package/dist/audit/discover.d.ts +3 -2
  4. package/dist/audit/discover.d.ts.map +1 -1
  5. package/dist/audit/rules-doc.d.ts.map +1 -1
  6. package/dist/build.d.ts +3 -3
  7. package/dist/build.d.ts.map +1 -1
  8. package/dist/cli/commands/build.d.ts.map +1 -1
  9. package/dist/cli/commands/check-lexicon.d.ts +14 -0
  10. package/dist/cli/commands/check-lexicon.d.ts.map +1 -1
  11. package/dist/cli/commands/lexicon-surface-diff.d.ts +6 -0
  12. package/dist/cli/commands/lexicon-surface-diff.d.ts.map +1 -1
  13. package/dist/cli/commands/lint.d.ts.map +1 -1
  14. package/dist/cli/handlers/lifecycle.d.ts +13 -0
  15. package/dist/cli/handlers/lifecycle.d.ts.map +1 -1
  16. package/dist/cli/handlers/search.d.ts.map +1 -1
  17. package/dist/cli/main.d.ts.map +1 -1
  18. package/dist/cli/mcp/resource-handlers.d.ts.map +1 -1
  19. package/dist/cli/mcp/tools/explain.d.ts +6 -0
  20. package/dist/cli/mcp/tools/explain.d.ts.map +1 -1
  21. package/dist/cli/plugins.d.ts +1 -1
  22. package/dist/cli/plugins.d.ts.map +1 -1
  23. package/dist/cli/registry.d.ts +7 -0
  24. package/dist/cli/registry.d.ts.map +1 -1
  25. package/dist/cli/reporters/stylish.d.ts +15 -1
  26. package/dist/cli/reporters/stylish.d.ts.map +1 -1
  27. package/dist/codegen/lexicon-regen.d.ts +11 -0
  28. package/dist/codegen/lexicon-regen.d.ts.map +1 -1
  29. package/dist/codegen/validate.d.ts +10 -0
  30. package/dist/codegen/validate.d.ts.map +1 -1
  31. package/dist/components/auto-release.d.ts +4 -0
  32. package/dist/components/auto-release.d.ts.map +1 -1
  33. package/dist/components/starter-plugin.d.ts +2 -0
  34. package/dist/components/starter-plugin.d.ts.map +1 -1
  35. package/dist/components/verbs/ensure-secret.d.ts +50 -0
  36. package/dist/components/verbs/ensure-secret.d.ts.map +1 -0
  37. package/dist/components/verbs/index.d.ts +8 -0
  38. package/dist/components/verbs/index.d.ts.map +1 -1
  39. package/dist/components/verbs/r2-sync.d.ts +76 -0
  40. package/dist/components/verbs/r2-sync.d.ts.map +1 -0
  41. package/dist/components/verbs/wrangler.d.ts +108 -0
  42. package/dist/components/verbs/wrangler.d.ts.map +1 -0
  43. package/dist/config.d.ts +54 -0
  44. package/dist/config.d.ts.map +1 -1
  45. package/dist/deep-observation.d.ts +14 -0
  46. package/dist/deep-observation.d.ts.map +1 -1
  47. package/dist/effect-receipt.d.ts +177 -0
  48. package/dist/effect-receipt.d.ts.map +1 -0
  49. package/dist/env.d.ts +12 -1
  50. package/dist/env.d.ts.map +1 -1
  51. package/dist/fold/subset.d.ts +15 -2
  52. package/dist/fold/subset.d.ts.map +1 -1
  53. package/dist/index.d.ts +4 -0
  54. package/dist/index.d.ts.map +1 -1
  55. package/dist/lexicon.d.ts +223 -2
  56. package/dist/lexicon.d.ts.map +1 -1
  57. package/dist/lifecycle/change-set.d.ts +33 -5
  58. package/dist/lifecycle/change-set.d.ts.map +1 -1
  59. package/dist/lifecycle/index.d.ts +3 -0
  60. package/dist/lifecycle/index.d.ts.map +1 -1
  61. package/dist/lifecycle/observation-baseline.d.ts +21 -3
  62. package/dist/lifecycle/observation-baseline.d.ts.map +1 -1
  63. package/dist/lifecycle/receipt-plan.d.ts +62 -0
  64. package/dist/lifecycle/receipt-plan.d.ts.map +1 -0
  65. package/dist/lifecycle/release-ledger.d.ts +20 -0
  66. package/dist/lifecycle/release-ledger.d.ts.map +1 -1
  67. package/dist/lifecycle/teardown.d.ts +132 -0
  68. package/dist/lifecycle/teardown.d.ts.map +1 -0
  69. package/dist/lifecycle/unobserved-gate.d.ts +67 -0
  70. package/dist/lifecycle/unobserved-gate.d.ts.map +1 -0
  71. package/dist/lint/engine.d.ts +6 -2
  72. package/dist/lint/engine.d.ts.map +1 -1
  73. package/dist/lint/knowledge-checks.d.ts +48 -0
  74. package/dist/lint/knowledge-checks.d.ts.map +1 -0
  75. package/dist/lint/output-checks.d.ts +5 -0
  76. package/dist/lint/output-checks.d.ts.map +1 -0
  77. package/dist/lint/pipeline-change-gate.d.ts +101 -0
  78. package/dist/lint/pipeline-change-gate.d.ts.map +1 -0
  79. package/dist/lint/post-synth.d.ts +12 -0
  80. package/dist/lint/post-synth.d.ts.map +1 -1
  81. package/dist/lint/receipt-checks.d.ts +9 -0
  82. package/dist/lint/receipt-checks.d.ts.map +1 -0
  83. package/dist/lint/rule.d.ts +31 -0
  84. package/dist/lint/rule.d.ts.map +1 -1
  85. package/dist/lint/rules/cor021-env-literal-name.d.ts +3 -0
  86. package/dist/lint/rules/cor021-env-literal-name.d.ts.map +1 -0
  87. package/dist/lint/rules/cor022-receipt-leaf.d.ts +13 -0
  88. package/dist/lint/rules/cor022-receipt-leaf.d.ts.map +1 -0
  89. package/dist/lint/rules/cor024-receipt-secret-pointer.d.ts +3 -0
  90. package/dist/lint/rules/cor024-receipt-secret-pointer.d.ts.map +1 -0
  91. package/dist/lint/rules/evl001-non-literal-expression.d.ts.map +1 -1
  92. package/dist/lint/rules/index.d.ts +4 -1
  93. package/dist/lint/rules/index.d.ts.map +1 -1
  94. package/dist/okf-read.d.ts +78 -0
  95. package/dist/okf-read.d.ts.map +1 -0
  96. package/dist/op/builders.d.ts +134 -8
  97. package/dist/op/builders.d.ts.map +1 -1
  98. package/dist/op/index.d.ts +4 -2
  99. package/dist/op/index.d.ts.map +1 -1
  100. package/dist/op/local-executor.d.ts +2 -1
  101. package/dist/op/local-executor.d.ts.map +1 -1
  102. package/dist/op/receipt-store.d.ts +138 -0
  103. package/dist/op/receipt-store.d.ts.map +1 -0
  104. package/dist/op/types.d.ts +31 -1
  105. package/dist/op/types.d.ts.map +1 -1
  106. package/dist/secret-materialization.d.ts +138 -0
  107. package/dist/secret-materialization.d.ts.map +1 -0
  108. package/dist/secret-provenance.d.ts +218 -0
  109. package/dist/secret-provenance.d.ts.map +1 -0
  110. package/dist/serializer.d.ts +11 -0
  111. package/dist/serializer.d.ts.map +1 -1
  112. package/dist/testing.d.ts +136 -0
  113. package/dist/testing.d.ts.map +1 -0
  114. package/dist/yaml.d.ts.map +1 -1
  115. package/package.json +9 -1
  116. package/src/audit/core.test.ts +57 -0
  117. package/src/audit/core.ts +0 -0
  118. package/src/audit/detect-bundle.test.ts +1 -1
  119. package/src/audit/discover.test.ts +24 -0
  120. package/src/audit/discover.ts +11 -2
  121. package/src/audit/rules-doc.ts +11 -1
  122. package/src/build.test.ts +41 -0
  123. package/src/build.ts +34 -6
  124. package/src/cli/commands/__fixtures__/audit-fountain/agents/fleet.yaml +27 -0
  125. package/src/cli/commands/__fixtures__/audit-fountain/k8s/deploy.yaml +16 -0
  126. package/src/cli/commands/__fixtures__/audit-fountain-clean/fleet.yaml +20 -0
  127. package/src/cli/commands/audit.test.ts +53 -0
  128. package/src/cli/commands/audit.ts +1 -1
  129. package/src/cli/commands/build.test.ts +211 -0
  130. package/src/cli/commands/build.ts +126 -8
  131. package/src/cli/commands/check-lexicon.test.ts +45 -1
  132. package/src/cli/commands/check-lexicon.ts +45 -0
  133. package/src/cli/commands/lexicon-surface-diff.ts +9 -0
  134. package/src/cli/commands/lexicon-surface-diff.update.test.ts +112 -0
  135. package/src/cli/commands/lint.ts +32 -7
  136. package/src/cli/handlers/explain.test.ts +70 -1
  137. package/src/cli/handlers/graph.ts +4 -4
  138. package/src/cli/handlers/lifecycle.test.ts +345 -1
  139. package/src/cli/handlers/lifecycle.ts +300 -11
  140. package/src/cli/handlers/search.ts +5 -2
  141. package/src/cli/main.ts +12 -1
  142. package/src/cli/mcp/resource-handlers.ts +38 -1
  143. package/src/cli/mcp/server.test.ts +58 -1
  144. package/src/cli/mcp/tools/explain.ts +51 -2
  145. package/src/cli/plugins.ts +4 -2
  146. package/src/cli/registry.ts +7 -0
  147. package/src/cli/reporters/stylish.test.ts +154 -0
  148. package/src/cli/reporters/stylish.ts +154 -33
  149. package/src/codegen/lexicon-regen.ts +19 -1
  150. package/src/codegen/validate.test.ts +33 -0
  151. package/src/codegen/validate.ts +21 -2
  152. package/src/components/auto-release.ts +6 -0
  153. package/src/components/registry.test.ts +7 -2
  154. package/src/components/starter-plugin.ts +17 -0
  155. package/src/components/verbs/ensure-secret.test.ts +130 -0
  156. package/src/components/verbs/ensure-secret.ts +79 -0
  157. package/src/components/verbs/index.ts +8 -0
  158. package/src/components/verbs/r2-sync.test.ts +107 -0
  159. package/src/components/verbs/r2-sync.ts +124 -0
  160. package/src/components/verbs/wrangler.test.ts +170 -0
  161. package/src/components/verbs/wrangler.ts +241 -0
  162. package/src/config.test.ts +55 -0
  163. package/src/config.ts +88 -1
  164. package/src/deep-observation.test.ts +19 -0
  165. package/src/deep-observation.ts +17 -0
  166. package/src/effect-receipt-exclusion.test.ts +190 -0
  167. package/src/effect-receipt.test.ts +419 -0
  168. package/src/effect-receipt.ts +412 -0
  169. package/src/env.test.ts +35 -1
  170. package/src/env.ts +17 -3
  171. package/src/fold/subset.test.ts +26 -0
  172. package/src/fold/subset.ts +45 -19
  173. package/src/index.ts +4 -0
  174. package/src/lexicon.ts +227 -2
  175. package/src/lifecycle/change-set.ts +46 -7
  176. package/src/lifecycle/index.ts +3 -0
  177. package/src/lifecycle/observation-baseline.test.ts +46 -0
  178. package/src/lifecycle/observation-baseline.ts +33 -1
  179. package/src/lifecycle/receipt-plan.test.ts +250 -0
  180. package/src/lifecycle/receipt-plan.ts +249 -0
  181. package/src/lifecycle/release-ledger.ts +20 -0
  182. package/src/lifecycle/teardown.test.ts +568 -0
  183. package/src/lifecycle/teardown.ts +359 -0
  184. package/src/lifecycle/unobserved-gate.test.ts +109 -0
  185. package/src/lifecycle/unobserved-gate.ts +102 -0
  186. package/src/lint/engine.ts +7 -1
  187. package/src/lint/knowledge-checks.test.ts +80 -0
  188. package/src/lint/knowledge-checks.ts +74 -0
  189. package/src/lint/output-checks.test.ts +85 -0
  190. package/src/lint/output-checks.ts +99 -0
  191. package/src/lint/pipeline-change-gate.test.ts +144 -0
  192. package/src/lint/pipeline-change-gate.ts +153 -0
  193. package/src/lint/post-synth.ts +15 -0
  194. package/src/lint/receipt-checks.test.ts +101 -0
  195. package/src/lint/receipt-checks.ts +93 -0
  196. package/src/lint/rule.ts +23 -0
  197. package/src/lint/rules/cor021-env-literal-name.test.ts +128 -0
  198. package/src/lint/rules/cor021-env-literal-name.ts +114 -0
  199. package/src/lint/rules/cor022-receipt-leaf.test.ts +116 -0
  200. package/src/lint/rules/cor022-receipt-leaf.ts +130 -0
  201. package/src/lint/rules/cor024-receipt-secret-pointer.test.ts +121 -0
  202. package/src/lint/rules/cor024-receipt-secret-pointer.ts +218 -0
  203. package/src/lint/rules/evl001-non-literal-expression.test.ts +27 -0
  204. package/src/lint/rules/evl001-non-literal-expression.ts +8 -1
  205. package/src/lint/rules/index.ts +10 -1
  206. package/src/okf-read.test.ts +149 -0
  207. package/src/okf-read.ts +197 -0
  208. package/src/op/builders.ts +179 -8
  209. package/src/op/effect-step.test.ts +311 -0
  210. package/src/op/index.ts +10 -3
  211. package/src/op/local-executor.ts +172 -25
  212. package/src/op/op.test.ts +25 -2
  213. package/src/op/receipt-store.ts +211 -0
  214. package/src/op/types.ts +33 -1
  215. package/src/secret-materialization.test.ts +199 -0
  216. package/src/secret-materialization.ts +235 -0
  217. package/src/secret-provenance.test.ts +388 -0
  218. package/src/secret-provenance.ts +475 -0
  219. package/src/serializer.ts +12 -0
  220. package/src/testing.test.ts +261 -0
  221. package/src/testing.ts +338 -0
  222. package/src/yaml.test.ts +88 -0
  223. package/src/yaml.ts +76 -6
@@ -0,0 +1,197 @@
1
+ import { readdir, readFile } from "node:fs/promises";
2
+ import { existsSync } from "node:fs";
3
+ import { join, relative, sep } from "node:path";
4
+ import type { Declarable } from "./declarable";
5
+ import { splitFrontmatter } from "./okf";
6
+ import { parseYAML } from "./yaml";
7
+
8
+ /**
9
+ * OKF bundle reader (#1864, design #1059, epic #1057) — the input half of
10
+ * `./okf.ts`'s emitter: loads a project-authored knowledge bundle from disk
11
+ * and binds its concepts to discovered entities.
12
+ *
13
+ * Tolerant to the letter of the OKF v0.2 spec (§11), the same posture
14
+ * `okfConformanceProblems` enforces on the emit side: unknown `type` values
15
+ * and extra frontmatter keys pass through untouched, a file with unparseable
16
+ * or missing frontmatter is skipped with a warning, and nothing here ever
17
+ * rejects the bundle — a missing knowledge directory included.
18
+ */
19
+
20
+ const RESERVED_OKF_FILES = new Set(["index.md", "log.md"]);
21
+
22
+ /**
23
+ * One authored concept document, parsed from a bundle. Deliberately thinner
24
+ * than the emit side's internal `Concept` (`./okf.ts`) — this is what a
25
+ * *reader* trusts, and per the #1059 design that is `type`/`title` plus the
26
+ * single frontmatter key chant interprets, `binds`. Everything else an
27
+ * author wrote rides along in {@link frontmatter} unexamined, so a consumer
28
+ * that needs a project-specific key (a future `category`, say) can still
29
+ * reach it without a reader change.
30
+ */
31
+ export interface OkfConcept {
32
+ /** Bundle-relative path (forward-slash separated), e.g. "decisions/public-assets.md". */
33
+ path: string;
34
+ /** The frontmatter `type`. May be empty — the reader never rejects a concept for it, unlike the emitter's conformance check. */
35
+ type: string;
36
+ /** Frontmatter `title`, when the author set one. */
37
+ title?: string;
38
+ /**
39
+ * This concept's `binds` frontmatter key, normalized to a list — one name,
40
+ * several, or `[]` for "declared no binding, and that's legitimate."
41
+ * Structural-looking keys (`name`, `lexicon`, `kind`) are deliberately
42
+ * *not* surfaced here or anywhere else on this type: the reader interprets
43
+ * exactly `binds` (#1059's precedence-by-construction), so authored and
44
+ * inferred knowledge cannot conflict at the fact level.
45
+ */
46
+ binds: string[];
47
+ /** Every frontmatter key this concept's document carries, `binds` included, verbatim. */
48
+ frontmatter: Record<string, unknown>;
49
+ /** Markdown body (everything after the closing `---`), unchanged. */
50
+ body: string;
51
+ }
52
+
53
+ /** A loaded OKF knowledge bundle: every authored concept found, reserved files already excluded. */
54
+ export interface OkfBundle {
55
+ concepts: OkfConcept[];
56
+ }
57
+
58
+ /** `binds` accepts one name or a list (#1059); anything else is treated as "no binding" rather than rejecting the concept. */
59
+ function normalizeBinds(value: unknown): string[] {
60
+ if (typeof value === "string") return value.length > 0 ? [value] : [];
61
+ if (Array.isArray(value)) return value.filter((v): v is string => typeof v === "string" && v.length > 0);
62
+ return [];
63
+ }
64
+
65
+ /** Recursively collect every `.md` file under `dir`, as bundle-relative (forward-slash) paths paired with their absolute path. */
66
+ async function findMarkdownFiles(dir: string): Promise<Array<{ relPath: string; fullPath: string }>> {
67
+ const found: Array<{ relPath: string; fullPath: string }> = [];
68
+
69
+ async function scan(current: string): Promise<void> {
70
+ let entries;
71
+ try {
72
+ entries = await readdir(current, { withFileTypes: true });
73
+ } catch {
74
+ return; // unreadable directory: silently skip, same posture as findInfraFiles
75
+ }
76
+ for (const entry of entries) {
77
+ const fullPath = join(current, entry.name);
78
+ if (entry.isDirectory()) {
79
+ await scan(fullPath);
80
+ } else if (entry.isFile() && entry.name.endsWith(".md")) {
81
+ const relPath = relative(dir, fullPath).split(sep).join("/");
82
+ found.push({ relPath, fullPath });
83
+ }
84
+ }
85
+ }
86
+
87
+ await scan(dir);
88
+ return found;
89
+ }
90
+
91
+ /**
92
+ * Load every authored concept from an OKF knowledge bundle directory.
93
+ * `index.md`/`log.md` are skipped (reserved, by basename, at any depth —
94
+ * matching `okfConformanceProblems`'s check on the emit side). A directory
95
+ * that does not exist yields an empty bundle, not an error: the #1059 design
96
+ * treats a missing `knowledge/` as "this project has none yet," the same
97
+ * posture as a project with no lexicons configured.
98
+ *
99
+ * A file that fails to parse — no frontmatter block, unparseable YAML, or
100
+ * frontmatter that isn't a mapping — is skipped with a `console.warn` and
101
+ * does not affect any other file. Nothing here throws.
102
+ */
103
+ export async function loadOkfBundle(dir: string): Promise<OkfBundle> {
104
+ if (!existsSync(dir)) return { concepts: [] };
105
+
106
+ const concepts: OkfConcept[] = [];
107
+ for (const { relPath, fullPath } of await findMarkdownFiles(dir)) {
108
+ if (RESERVED_OKF_FILES.has(relPath.split("/").pop()!)) continue;
109
+
110
+ let content: string;
111
+ try {
112
+ content = await readFile(fullPath, "utf8");
113
+ } catch {
114
+ console.warn(`[chant] warning: knowledge bundle concept "${relPath}" could not be read, skipping`);
115
+ continue;
116
+ }
117
+
118
+ const split = splitFrontmatter(content);
119
+ if (!split) {
120
+ console.warn(`[chant] warning: knowledge bundle concept "${relPath}" has no frontmatter block, skipping`);
121
+ continue;
122
+ }
123
+
124
+ let parsed: unknown;
125
+ try {
126
+ parsed = parseYAML(split.frontmatter);
127
+ } catch {
128
+ console.warn(`[chant] warning: knowledge bundle concept "${relPath}" has unparseable frontmatter, skipping`);
129
+ continue;
130
+ }
131
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
132
+ console.warn(`[chant] warning: knowledge bundle concept "${relPath}" frontmatter is not a mapping, skipping`);
133
+ continue;
134
+ }
135
+
136
+ const frontmatter = parsed as Record<string, unknown>;
137
+ concepts.push({
138
+ path: relPath,
139
+ type: typeof frontmatter.type === "string" ? frontmatter.type : "",
140
+ title: typeof frontmatter.title === "string" ? frontmatter.title : undefined,
141
+ binds: normalizeBinds(frontmatter.binds),
142
+ frontmatter,
143
+ body: split.body,
144
+ });
145
+ }
146
+
147
+ concepts.sort((a, b) => a.path.localeCompare(b.path));
148
+ return { concepts };
149
+ }
150
+
151
+ /** {@link bindConcepts}'s result: bound concepts per entity, plus every binding that resolved nothing. */
152
+ export interface BindConceptsResult {
153
+ /**
154
+ * Bound concepts keyed by entity logical name — the same names that key
155
+ * `DiscoveryResult.entities` (`./discovery/index.ts`). An entity with no
156
+ * bound concepts is absent from the map, never present with `[]`.
157
+ */
158
+ bound: Map<string, OkfConcept[]>;
159
+ /** Every `binds` entry that named no entity in `entities` — chant's own broken-link posture (#1059): a warning for the caller to surface (COR022, #1865), never a load failure. */
160
+ unresolved: UnresolvedBinding[];
161
+ }
162
+
163
+ /** One `binds` entry that resolved to nothing. */
164
+ export interface UnresolvedBinding {
165
+ /** The concept whose `binds` named this entity. */
166
+ concept: OkfConcept;
167
+ /** The unresolved logical name. */
168
+ name: string;
169
+ }
170
+
171
+ /**
172
+ * Resolve every concept's `binds` against a project's discovered entities.
173
+ * `entities` is the same shape `./okf.ts`'s emitter takes, `DiscoveryResult`'s
174
+ * own `entities` map — a bare logical name is unambiguous within one
175
+ * discovery pass (#1059's "Rejected options" on binding ambiguity).
176
+ *
177
+ * A concept with no `binds` contributes to neither `bound` nor `unresolved`;
178
+ * an unbound concept is legitimate, not an omission (a runbook, a decision
179
+ * about the project as a whole).
180
+ */
181
+ export function bindConcepts(bundle: OkfBundle, entities: Map<string, Declarable>): BindConceptsResult {
182
+ const bound = new Map<string, OkfConcept[]>();
183
+ const unresolved: UnresolvedBinding[] = [];
184
+
185
+ for (const concept of bundle.concepts) {
186
+ for (const name of concept.binds) {
187
+ if (entities.has(name)) {
188
+ if (!bound.has(name)) bound.set(name, []);
189
+ bound.get(name)!.push(concept);
190
+ } else {
191
+ unresolved.push({ concept, name });
192
+ }
193
+ }
194
+ }
195
+
196
+ return { bound, unresolved };
197
+ }
@@ -1,5 +1,7 @@
1
1
  import { OpResource } from "./resource";
2
- import type { OpConfig, PhaseDefinition, StepDefinition, ActivityStep, GateStep } from "./types";
2
+ import type { OpConfig, PhaseDefinition, StepDefinition, ActivityStep, GateStep, EffectStep } from "./types";
3
+ import { isEffectReceipt, type EffectReceiptDeclaration } from "../effect-receipt";
4
+ import { receiptCheckInput } from "./receipt-store";
3
5
 
4
6
  // ── Core builders ─────────────────────────────────────────────────────────────
5
7
 
@@ -58,6 +60,62 @@ export function gate(
58
60
  };
59
61
  }
60
62
 
63
+ /**
64
+ * Wrap nested steps in read-compare-run-write over an effect receipt (#1834,
65
+ * epic #1703). At run: read the live receipt through the receipt store,
66
+ * compare it against the resolved expectation, and on a match skip the nested
67
+ * steps ("effect already applied"). On a mismatch the nested steps run in
68
+ * authored order, and ONLY when every one of them succeeds is the receipt
69
+ * written — last, once. This step is the sole writer of a receipt (decision
70
+ * 3); a nested-step failure leaves the receipt untouched (stale), so the next
71
+ * run re-proposes the effect.
72
+ *
73
+ * `receipt` is the typed EffectReceipt declaration — import the const from
74
+ * your receipts module. There is no string form: a receipt named by string
75
+ * would sever the step from the declaration that lint, plan, and the lexicon
76
+ * row all key on.
77
+ *
78
+ * A gate authored inside `steps` pauses only when the effect will fire (the
79
+ * matched path never reaches it). The receipt-store activities (`receiptRead`,
80
+ * `receiptWrite`) are provided by the receipt row's lexicon (#1835, aws).
81
+ *
82
+ * @example
83
+ * ```ts
84
+ * import { seededReceipt } from "./receipts";
85
+ * phase("Seed", [
86
+ * effect(seededReceipt, [
87
+ * shell("npm run db:seed"),
88
+ * ]),
89
+ * ]),
90
+ * ```
91
+ */
92
+ export function effect(
93
+ receipt: EffectReceiptDeclaration,
94
+ steps: Array<ActivityStep | GateStep>,
95
+ opts?: { description?: string },
96
+ ): EffectStep {
97
+ if (!isEffectReceipt(receipt)) {
98
+ throw new Error(
99
+ "effect(): `receipt` must be the EffectReceipt declaration itself — import the const; there is no string form",
100
+ );
101
+ }
102
+ for (const step of steps) {
103
+ if ((step as { kind?: unknown }).kind === "effect") {
104
+ throw new Error(
105
+ `effect("${receipt.name}"): effect steps do not nest — one receipt witnesses one effect`,
106
+ );
107
+ }
108
+ }
109
+ const { receipt: ref, expectation } = receiptCheckInput(receipt);
110
+ return {
111
+ kind: "effect",
112
+ receipt: ref,
113
+ ...(expectation !== undefined ? { expectation } : {}),
114
+ steps,
115
+ ...(opts?.description ? { description: opts.description } : {}),
116
+ };
117
+ }
118
+
61
119
  // ── Pre-built activity shortcuts ──────────────────────────────────────────────
62
120
 
63
121
  /**
@@ -100,6 +158,22 @@ export const helmInstall = (
100
158
  return activity("helmInstall", { name, chart, ...args }, profile ?? "longInfra");
101
159
  };
102
160
 
161
+ /**
162
+ * Deploy a recorded pinned render by its `sha256:` content digest (chant
163
+ * #1242): the helm lexicon's `helmInstall` activity loads the render from
164
+ * the render store, verifies the digest, and installs those exact bytes as
165
+ * a structure-preserving wrapper chart — no deploy-time render. Defaults to
166
+ * the `longInfra` profile (override via `opts.profile`).
167
+ */
168
+ export const helmInstallPinned = (
169
+ name: string,
170
+ contentDigest: string,
171
+ opts?: { namespace?: string; profile?: ActivityStep["profile"]; [k: string]: unknown },
172
+ ): ActivityStep => {
173
+ const { args, profile } = takeProfile(opts);
174
+ return activity("helmInstall", { name, contentDigest, ...args }, profile ?? "longInfra");
175
+ };
176
+
103
177
  /** Poll for stack readiness (kubectl rollout, CloudFormation complete, etc). Defaults to the `k8sWait` profile (override via `opts.profile`). */
104
178
  export const waitForStack = (name: string, opts?: Record<string, unknown>): ActivityStep => {
105
179
  const { args, profile } = takeProfile(opts);
@@ -133,10 +207,63 @@ export const shell = (
133
207
  ): ActivityStep =>
134
208
  activity("shellCmd", { cmd, ...(opts?.env ? { env: opts.env } : {}) }, opts?.profile);
135
209
 
210
+ /**
211
+ * Ensure a `generated-once` secret exists in the target store (#1829, epic
212
+ * #1365). The op-step surface over the same engine as the `ensure-secret`
213
+ * capability verb (`ensureSecretMaterialization`, core's
214
+ * secret-materialization module): read-then-write — if the secret exists, its
215
+ * declared key-set and any declared `metadata` are verified and the step
216
+ * stops (present means done); if absent, one value per key is minted at
217
+ * apply time and written straight to the store. Never mints over an existing
218
+ * value, never rotates implicitly; a mismatch fails the step loudly, naming
219
+ * key names and metadata keys — never values. No output of the activity
220
+ * carries secret material. The backing activity is provided by the store's
221
+ * lexicon (k8s, #1830).
222
+ */
223
+ export const ensureSecret = (
224
+ name: string,
225
+ keys: string[],
226
+ opts?: { metadata?: Record<string, string>; profile?: ActivityStep["profile"]; [k: string]: unknown },
227
+ ): ActivityStep => {
228
+ const { args, profile } = takeProfile(opts);
229
+ return activity("ensureSecret", { name, keys, ...args }, profile);
230
+ };
231
+
136
232
  /** Run `chant teardown` in the given project directory. Uses `longInfra` profile. */
137
233
  export const teardown = (path: string): ActivityStep =>
138
234
  activity("chantTeardown", { path }, "longInfra");
139
235
 
236
+ /**
237
+ * Tear down one environment's marker-owned resources — the durable form of
238
+ * `chant lifecycle teardown <env> --yes` (#1222). The activity runs core's
239
+ * teardown engine in-process: enumerate by ownership marker (this project's
240
+ * `ownership.stack` + `env`), delete through each lexicon's `executeTeardown`
241
+ * capability, retry failures once, and fail the step when any candidate is
242
+ * still failed. Distinct from {@link teardown}, which runs a project's own
243
+ * `npm run teardown` script.
244
+ *
245
+ * The CLI's guards apply unchanged, and a production-like environment name
246
+ * needs `confirmProd: true` in `opts` — the authored counterpart of
247
+ * `--confirm-prod`, since an Op never prompts. An ordinary {@link gate} step
248
+ * placed before this one composes as usual (steps run in authored order), so
249
+ * a human approval can precede the deletion:
250
+ *
251
+ * ```ts
252
+ * phase("Teardown", [
253
+ * gate("approve-teardown", { description: "Release the staging teardown" }),
254
+ * envTeardown("staging"),
255
+ * ]),
256
+ * ```
257
+ *
258
+ * `opts` also accepts `path` (the chant project directory, default the
259
+ * worker's cwd). Defaults to the `longInfra` profile (override via
260
+ * `opts.profile`).
261
+ */
262
+ export const envTeardown = (env: string, opts?: Record<string, unknown>): ActivityStep => {
263
+ const { args, profile } = takeProfile(opts);
264
+ return activity("envTeardown", { env, ...args }, profile ?? "longInfra");
265
+ };
266
+
140
267
  /**
141
268
  * Create a local k3d cluster (vanilla Kubernetes in Docker). Idempotent: skips
142
269
  * creation if a cluster of the same name already exists. Defaults to the
@@ -169,6 +296,48 @@ export const k3dDown = (name: string, opts?: Record<string, unknown>): ActivityS
169
296
  return activity("k3dDown", { name, ...args }, profile ?? "fastIdempotent");
170
297
  };
171
298
 
299
+ /**
300
+ * Run the pinned k3s installer against a reachable host (`role`: `"server"`
301
+ * or `"agent"`). Idempotent on an already-installed matching version.
302
+ * Defaults to the `longInfra` profile (the installer downloads and starts
303
+ * the binary); override via `opts.profile`.
304
+ *
305
+ * The implementation lives in the k3s lexicon (chant #1601) — the project's
306
+ * `chant.config.ts` must list `"k3s"` in `lexicons` for the activity to load.
307
+ * Bounded exactly as `k3dUp`/`k3dDown` (chant #1410): drives the reachable-host
308
+ * case only, no host provisioning, no SSH orchestration.
309
+ *
310
+ * `opts` accepts `configFile` (required — path to the chant-emitted
311
+ * config.yaml, passed as `--config`), `version` (overrides the lexicon's
312
+ * pinned `INSTALL_K3S_VERSION`), and `tokenFile` (path to a file holding the
313
+ * join token, passed to the installer as `K3S_TOKEN_FILE`). There is no
314
+ * `token` option — the join secret's value never travels through this step;
315
+ * only a file path does (the #1601 token boundary, same stance as #1365).
316
+ */
317
+ export const k3sInstall = (
318
+ role: "server" | "agent",
319
+ opts: { configFile: string; version?: string; tokenFile?: string; profile?: ActivityStep["profile"]; [k: string]: unknown },
320
+ ): ActivityStep => {
321
+ const { args, profile } = takeProfile(opts);
322
+ return activity("k3sInstall", { role, ...args }, profile ?? "longInfra");
323
+ };
324
+
325
+ /**
326
+ * Uninstall k3s from a reachable host (`role`: `"server"` or `"agent"`).
327
+ * Defaults to the `fastIdempotent` profile (override via `opts.profile`).
328
+ * Implementation lives in the k3s lexicon (chant #1601) — requires `"k3s"`
329
+ * in the project's `lexicons`. A host where k3s was never installed is a
330
+ * no-op success, the same shape as `k3dDown` against an already-gone
331
+ * cluster.
332
+ */
333
+ export const k3sUninstall = (
334
+ role: "server" | "agent",
335
+ opts?: Record<string, unknown>,
336
+ ): ActivityStep => {
337
+ const { args, profile } = takeProfile(opts);
338
+ return activity("k3sUninstall", { role, ...args }, profile ?? "fastIdempotent");
339
+ };
340
+
172
341
  /**
173
342
  * Boot a local Floci AWS emulator in Docker and point subsequent steps at it —
174
343
  * sets `AWS_ENDPOINT_URL` + test creds in the process env so a following
@@ -279,8 +448,8 @@ export const azDelete = (templatePath: string, opts?: Record<string, unknown>):
279
448
  * Deploy a built CloudFormation template by calling the CloudFormation API
280
449
  * directly (create-or-update + poll) — the direct twin of {@link azApply} /
281
450
  * {@link gcpApply} for AWS, targeting a local Floci emulator or real AWS by
282
- * endpoint override. Speaks the CFN API over HTTP rather than shelling `aws`
283
- * (that path is still `nativeApply({ target: "cloudformation" })`). Provided by
451
+ * endpoint override. Speaks the CFN API over HTTP rather than shelling `aws`
452
+ * `nativeApply({ target: "cloudformation" })` routes here too (#1449). Provided by
284
453
  * the aws lexicon; loaded when the project lists `aws`. Defaults to the
285
454
  * `longInfra` profile. `opts` requires `stackName`; accepts `endpoint`, `region`,
286
455
  * `capabilities`, `timeoutMs`, `intervalMs`.
@@ -297,13 +466,15 @@ export const awsDelete = (templatePath: string, opts?: Record<string, unknown>):
297
466
  };
298
467
 
299
468
  /**
300
- * Apply chant's built GCP (CNRM) resources directly to the GCS REST API,
469
+ * Apply chant's built GCP (CNRM) resources directly to their GCP REST APIs,
301
470
  * targeting a local floci-gcp emulator or real GCP by endpoint override — the
302
- * native GCP applier (#706 starter #711), currently handling `StorageBucket`.
303
- * Defaults to the `longInfra` profile (override via `opts.profile`).
471
+ * native GCP applier (#706 starter #711). `nativeApply({ target: "gcp" })`
472
+ * routes here too (#1449). Defaults to the `longInfra` profile (override via
473
+ * `opts.profile`).
304
474
  *
305
- * `opts` accepts `endpoint` (default `STORAGE_EMULATOR_HOST` env / real GCS) and
306
- * `project` (default `GOOGLE_CLOUD_PROJECT` env / the CNRM project-id annotation).
475
+ * `opts` accepts `endpoint` (default `GCP_ENDPOINT_URL` env / each kind's
476
+ * real-GCP host) and `project` (default `GOOGLE_CLOUD_PROJECT` env / the CNRM
477
+ * project-id annotation).
307
478
  */
308
479
  export const gcpApply = (manifestPath: string, opts?: Record<string, unknown>): ActivityStep => {
309
480
  const { args, profile } = takeProfile(opts);