@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,241 @@
1
+ /**
2
+ * `wrangler-deploy` / `wrangler-versions-promote` — the Cloudflare Workers
3
+ * apply leaves (chant #1293, epic #1296). The epic cedes the Workers plane
4
+ * entirely to `wrangler` (first-party, schema-backed, already owns the
5
+ * config format, bindings model, local dev, and deploy) — these two verbs
6
+ * are a typed wrapper over that CLI, not a reimplementation of it.
7
+ *
8
+ * Placement: core, not a cloudflare lexicon. #1293's own analysis weighed
9
+ * three options — core (generic verb, fastest, costs a little conceptual
10
+ * tidiness since `wrangler-deploy` shells a specific vendor CLI), a
11
+ * capability-only cloudflare lexicon (the "correct" home, but `LexiconPlugin`
12
+ * tier-1 completeness — serializer, lint rules, post-synth, LSP, examples,
13
+ * docs — is heavy ceremony for three verbs and no resource types), or
14
+ * generalizing to a `cli-deploy` verb parameterised by tool (tempting, but
15
+ * premature and it re-opens the verb set the bounded-primitives thesis
16
+ * depends on staying closed). Core wins on the same precedent `sign`/
17
+ * `attest-provenance` (./sign.ts) already set: a specific vendor CLI
18
+ * (`cosign`) shelled out through the injectable `ProcessRunner`
19
+ * (./process-runner.ts), no cloud-specific SDK client needed the way AWS's
20
+ * `CloudExecutor` (./cloud-executor.ts) models CloudFormation/ECS/Lambda.
21
+ * `wrangler` fits that exact shape. Revisit if/when the zone-plane lexicon
22
+ * (#1294) lands and gives these three verbs a natural cloudflare-owned home.
23
+ *
24
+ * `wrangler-versions-promote` is also the direct evidence for #1296's
25
+ * "three new verbs cover a whole new cloud" claim: Cloudflare's native
26
+ * Worker version rollback maps onto the existing `RollbackPolicy: "native"`
27
+ * (../capability.ts) with no compensation code to hand-write — both verbs
28
+ * here declare a `rollback` (auto-derived "native" per ../capability.ts's
29
+ * `Capability.rollbackPolicy` doc) that re-promotes to whichever version was
30
+ * live before the step ran, the same best-effort captured-previous-state
31
+ * pattern lexicons/aws/src/components/apply.ts's `lambda-deploy` uses for
32
+ * its alias rollback.
33
+ *
34
+ * No real Cloudflare control-plane emulator exists yet (#1295, epic #1296),
35
+ * so — like ./sign.ts — every real path here shells out through the
36
+ * injectable `ProcessRunner` and every test substitutes `MockProcessRunner`
37
+ * (./__tests__/mock-process-runner.ts): no live `wrangler`, no network, ever,
38
+ * in a test run. CI coverage is plan-shape/invocation-shape assertions only,
39
+ * per #1293's own "Verification" section.
40
+ */
41
+
42
+ import type { Capability } from "../capability";
43
+ import { defaultProcessRunner, q, requireTool, type ProcessRunner } from "./process-runner";
44
+
45
+ const WRANGLER_TOOL = "wrangler";
46
+
47
+ /** Distinguishes one deploy target (a wrangler config + optional named environment) from another, so the best-effort previous-version tracking below never conflates two different Workers sharing a process. */
48
+ function targetKey(config: string, env?: string): string {
49
+ return env ? `${config}#${env}` : config;
50
+ }
51
+
52
+ function buildWranglerDeployArgs(input: WranglerDeployInput): string {
53
+ const args = ["wrangler", "deploy", "--config", q(input.config)];
54
+ if (input.env) args.push("--env", q(input.env));
55
+ return args.join(" ");
56
+ }
57
+
58
+ function buildVersionsListArgs(config: string, env?: string): string {
59
+ const args = ["wrangler", "versions", "list", "--config", q(config), "--json"];
60
+ if (env) args.push("--env", q(env));
61
+ return args.join(" ");
62
+ }
63
+
64
+ function buildVersionsPromoteArgs(config: string, versionId: string, percentage: number, env?: string): string {
65
+ const args = ["wrangler", "versions", "deploy", q(`${versionId}@${percentage}`), "--config", q(config), "--yes"];
66
+ if (env) args.push("--env", q(env));
67
+ return args.join(" ");
68
+ }
69
+
70
+ /** Thrown when `wrangler deploy`'s stdout carries no parseable Version ID — fail-closed rather than returning a capability output downstream steps (`wrangler-versions-promote`, wired via `"@Deploy.versionId"`) would silently receive as `undefined`. */
71
+ export class WranglerVersionIdNotFoundError extends Error {
72
+ constructor(public readonly stdout: string) {
73
+ super(`wrangler-deploy: could not find a Version ID in wrangler's output. Got:\n${stdout}`);
74
+ this.name = "WranglerVersionIdNotFoundError";
75
+ }
76
+ }
77
+
78
+ // wrangler's deploy/versions-upload output names the new version as e.g.
79
+ // "Version ID: 07bcb198-... " (gradual deployments) — accept either "Version
80
+ // ID:" or the bare "Version:" wording across wrangler versions, and require a
81
+ // UUID-shaped token so a stray "Version: 2" summary line can't be mistaken
82
+ // for it.
83
+ const VERSION_ID_RE = /Version(?: ID)?:\s*([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})/;
84
+
85
+ /** Extract the deployed Worker version id from `wrangler deploy`'s stdout. Exported so tests can assert on it directly, mirroring ./sign.ts's `buildSignArgs`. */
86
+ export function parseWranglerVersionId(stdout: string): string {
87
+ const match = VERSION_ID_RE.exec(stdout);
88
+ if (!match) throw new WranglerVersionIdNotFoundError(stdout);
89
+ return match[1]!;
90
+ }
91
+
92
+ interface WranglerVersionsListEntry {
93
+ id: string;
94
+ percentage?: number;
95
+ }
96
+
97
+ /**
98
+ * Best-effort: the currently-live (100%) version id, from `wrangler versions
99
+ * list --json` — `undefined` for a first deploy (no versions yet) or if
100
+ * listing fails/parses oddly. Never throws: capturing "what to roll back to"
101
+ * must not block the deploy/promote itself, the same fail-soft stance
102
+ * `lambda-deploy`'s alias-version capture takes.
103
+ */
104
+ async function currentLiveVersionId(
105
+ runner: ProcessRunner,
106
+ config: string,
107
+ env?: string,
108
+ ): Promise<string | undefined> {
109
+ try {
110
+ const { stdout } = await runner.run(buildVersionsListArgs(config, env));
111
+ const entries = JSON.parse(stdout) as WranglerVersionsListEntry[];
112
+ return entries.find((entry) => entry.percentage === 100)?.id;
113
+ } catch {
114
+ return undefined;
115
+ }
116
+ }
117
+
118
+ // ── wrangler-deploy ──────────────────────────────────────────────────────────
119
+
120
+ export interface WranglerDeployInput {
121
+ /** Path to the Worker's `wrangler.jsonc`/`wrangler.toml`. */
122
+ config: string;
123
+ /** Named environment within the config (`wrangler deploy --env <env>`). */
124
+ env?: string;
125
+ }
126
+
127
+ export interface WranglerDeployOutput {
128
+ /** The version id wrangler assigned this deploy — wire into `wrangler-versions-promote` (e.g. `"@Deploy.versionId"`). */
129
+ versionId: string;
130
+ }
131
+
132
+ /**
133
+ * Deploy a Worker from a `wrangler.jsonc`/`wrangler.toml` via `wrangler
134
+ * deploy`, and return the version id it published — the analogue of
135
+ * `lambda-deploy` (../../lexicons/aws/src/components/apply.ts) for the
136
+ * Workers plane.
137
+ *
138
+ * Rollback: `wrangler-versions-promote` back to whichever version was live
139
+ * before this step ran (captured up front via `wrangler versions list`, the
140
+ * same best-effort captured-previous pattern `lambda-deploy` uses for its
141
+ * alias). A no-op on a first deploy — nothing was live to restore.
142
+ */
143
+ export function createWranglerDeployCapability(
144
+ processRunner: ProcessRunner = defaultProcessRunner(),
145
+ ): Capability<WranglerDeployInput, WranglerDeployOutput> {
146
+ const previousVersionByTarget = new Map<string, string | undefined>();
147
+
148
+ return {
149
+ kind: "wrangler-deploy",
150
+ async run(_ctx, input) {
151
+ await requireTool(processRunner, WRANGLER_TOOL, `deploy the Worker in ${input.config}`);
152
+ const target = targetKey(input.config, input.env);
153
+ if (!previousVersionByTarget.has(target)) {
154
+ previousVersionByTarget.set(target, await currentLiveVersionId(processRunner, input.config, input.env));
155
+ }
156
+
157
+ const { stdout } = await processRunner.run(buildWranglerDeployArgs(input));
158
+ return { versionId: parseWranglerVersionId(stdout) };
159
+ },
160
+ async rollback(_ctx, input) {
161
+ const target = targetKey(input.config, input.env);
162
+ const previousVersionId = previousVersionByTarget.get(target);
163
+ if (!previousVersionId) return; // first deploy — nothing was live before it.
164
+ await requireTool(processRunner, WRANGLER_TOOL, `roll back the Worker in ${input.config}`);
165
+ await processRunner.run(buildVersionsPromoteArgs(input.config, previousVersionId, 100, input.env));
166
+ },
167
+ };
168
+ }
169
+
170
+ /** Default `wrangler-deploy` capability, backed by the real `ProcessRunner`. */
171
+ export const wranglerDeployCapability: Capability<WranglerDeployInput, WranglerDeployOutput> =
172
+ createWranglerDeployCapability();
173
+
174
+ // ── wrangler-versions-promote ─────────────────────────────────────────────────
175
+
176
+ export interface WranglerVersionsPromoteInput {
177
+ /** Path to the Worker's `wrangler.jsonc`/`wrangler.toml`. */
178
+ config: string;
179
+ /** Version id to promote — typically wired from a prior `wrangler-deploy` step (`"@Deploy.versionId"`) or a prior version id when this step composes as an explicit rollback. */
180
+ versionId: string;
181
+ /** Traffic percentage to route to `versionId`. Default: 100 (full promote/rollback). Below 100 is the gradual-deployment lever. */
182
+ percentage?: number;
183
+ /** Named environment within the config (`wrangler versions deploy --env <env>`). */
184
+ env?: string;
185
+ }
186
+
187
+ export interface WranglerVersionsPromoteOutput {
188
+ /** The version id that was promoted. */
189
+ versionId: string;
190
+ /** The traffic percentage actually routed to it. */
191
+ percentage: number;
192
+ }
193
+
194
+ /**
195
+ * Promote a Worker version to (some percentage of) live traffic via
196
+ * `wrangler versions deploy <version-id>@<percentage> --yes` — both the
197
+ * gradual-deployment lever (`percentage` < 100) and, at `percentage: 100`,
198
+ * the rollback mechanism: Cloudflare's native version rollback is a promote
199
+ * to a prior version id, not a redeploy, so this same verb composes as the
200
+ * explicit compensation step a component wires up (#1293's "verification"
201
+ * example: forced failure after `wrangler-deploy` -> `wrangler-versions-promote`
202
+ * back to the prior version, no hand-written compensation needed).
203
+ *
204
+ * Also declares its own `rollback` (native, no `rollbackPolicy` override
205
+ * needed — see ../capability.ts): re-promotes to whichever version was live
206
+ * before *this* promote call, for the case where the promote step itself is
207
+ * composed directly (not just as `wrangler-deploy`'s compensation).
208
+ */
209
+ export function createWranglerVersionsPromoteCapability(
210
+ processRunner: ProcessRunner = defaultProcessRunner(),
211
+ ): Capability<WranglerVersionsPromoteInput, WranglerVersionsPromoteOutput> {
212
+ const previousVersionByTarget = new Map<string, string | undefined>();
213
+
214
+ return {
215
+ kind: "wrangler-versions-promote",
216
+ async run(_ctx, input) {
217
+ await requireTool(processRunner, WRANGLER_TOOL, `promote version ${input.versionId} in ${input.config}`);
218
+ const percentage = input.percentage ?? 100;
219
+ const target = targetKey(input.config, input.env);
220
+ if (!previousVersionByTarget.has(target)) {
221
+ previousVersionByTarget.set(target, await currentLiveVersionId(processRunner, input.config, input.env));
222
+ }
223
+
224
+ await processRunner.run(buildVersionsPromoteArgs(input.config, input.versionId, percentage, input.env));
225
+ return { versionId: input.versionId, percentage };
226
+ },
227
+ async rollback(_ctx, input) {
228
+ const target = targetKey(input.config, input.env);
229
+ const previousVersionId = previousVersionByTarget.get(target);
230
+ if (!previousVersionId) return; // nothing was live before this promote.
231
+ await requireTool(processRunner, WRANGLER_TOOL, `roll back version promotion in ${input.config}`);
232
+ await processRunner.run(buildVersionsPromoteArgs(input.config, previousVersionId, 100, input.env));
233
+ },
234
+ };
235
+ }
236
+
237
+ /** Default `wrangler-versions-promote` capability, backed by the real `ProcessRunner`. */
238
+ export const wranglerVersionsPromoteCapability: Capability<
239
+ WranglerVersionsPromoteInput,
240
+ WranglerVersionsPromoteOutput
241
+ > = createWranglerVersionsPromoteCapability();
@@ -6,9 +6,12 @@ import {
6
6
  resolveAutoReleaseDisabled,
7
7
  resolveFoldEnabled,
8
8
  resolveSbomFormat,
9
+ resolveKnowledgeDir,
9
10
  environmentName,
10
11
  environmentNames,
11
12
  environmentEndpoint,
13
+ matchesEnvironmentPattern,
14
+ matchesDeclaredEnvironment,
12
15
  } from "./config";
13
16
  import { writeFileSync, mkdirSync, rmSync } from "fs";
14
17
  import { join } from "path";
@@ -336,6 +339,44 @@ describe("environmentName / environmentNames / environmentEndpoint (#1166)", ()
336
339
  expect(environmentEndpoint(environments, "unknown")).toBeUndefined(); // not declared at all
337
340
  expect(environmentEndpoint(undefined, "floci")).toBeUndefined();
338
341
  });
342
+
343
+ test("environmentEndpoint resolves through a pattern entry, literal entry winning (#1221)", () => {
344
+ const environments = [
345
+ { name: "pr-special", endpoint: "http://special:1111" },
346
+ { name: "pr-*", endpoint: "http://preview:4566" },
347
+ ];
348
+ expect(environmentEndpoint(environments, "pr-42")).toBe("http://preview:4566");
349
+ expect(environmentEndpoint(environments, "pr-special")).toBe("http://special:1111"); // literal wins over the pattern
350
+ expect(environmentEndpoint(environments, "prod")).toBeUndefined();
351
+ });
352
+ });
353
+
354
+ describe("environment glob patterns (#1221)", () => {
355
+ test("matchesEnvironmentPattern: a * matches any run of characters, anchored", () => {
356
+ expect(matchesEnvironmentPattern("pr-*", "pr-42")).toBe(true);
357
+ expect(matchesEnvironmentPattern("pr-*", "pr-")).toBe(true); // empty run is a run
358
+ expect(matchesEnvironmentPattern("pr-*", "pr")).toBe(false);
359
+ expect(matchesEnvironmentPattern("pr-*", "xpr-42")).toBe(false); // anchored at the start
360
+ expect(matchesEnvironmentPattern("*-preview", "42-preview")).toBe(true);
361
+ expect(matchesEnvironmentPattern("*-preview", "42-preview-old")).toBe(false); // anchored at the end
362
+ expect(matchesEnvironmentPattern("test-*-eu", "test-a-eu")).toBe(true);
363
+ expect(matchesEnvironmentPattern("test-*-eu", "test-a-us")).toBe(false);
364
+ });
365
+
366
+ test("matchesEnvironmentPattern without a * is plain equality", () => {
367
+ expect(matchesEnvironmentPattern("prod", "prod")).toBe(true);
368
+ expect(matchesEnvironmentPattern("prod", "prod2")).toBe(false);
369
+ });
370
+
371
+ test("matchesDeclaredEnvironment: literal first, then pattern, across entry forms", () => {
372
+ const environments = ["dev", "prod", "pr-*", { name: "test-*", endpoint: "http://localhost:4566" }];
373
+ expect(matchesDeclaredEnvironment(environments, "prod")).toBe(true);
374
+ expect(matchesDeclaredEnvironment(environments, "pr-42")).toBe(true);
375
+ expect(matchesDeclaredEnvironment(environments, "test-suite-a")).toBe(true); // object-form pattern entry
376
+ expect(matchesDeclaredEnvironment(environments, "stage")).toBe(false);
377
+ expect(matchesDeclaredEnvironment(undefined, "prod")).toBe(false);
378
+ expect(matchesDeclaredEnvironment([], "prod")).toBe(false);
379
+ });
339
380
  });
340
381
 
341
382
  describe("resolveFoldEnabled (#1134 — fold is the default build path)", () => {
@@ -393,3 +434,17 @@ describe("resolveSbomFormat (#606)", () => {
393
434
  expect(resolveSbomFormat({}, "cyclonedx")).toBe("cyclonedx");
394
435
  });
395
436
  });
437
+
438
+ describe("resolveKnowledgeDir (#1864, design #1059)", () => {
439
+ test("convention: knowledge/ beside the project root when config is silent", () => {
440
+ expect(resolveKnowledgeDir({}, "/proj")).toBe(join("/proj", "knowledge"));
441
+ });
442
+
443
+ test("config override honored", () => {
444
+ expect(resolveKnowledgeDir({ knowledge: { dir: "docs/knowledge" } }, "/proj")).toBe(join("/proj", "docs/knowledge"));
445
+ });
446
+
447
+ test("an empty knowledge object still falls back to the convention name", () => {
448
+ expect(resolveKnowledgeDir({ knowledge: {} }, "/proj")).toBe(join("/proj", "knowledge"));
449
+ });
450
+ });
package/src/config.ts CHANGED
@@ -44,6 +44,56 @@ export function environmentNames(environments: EnvironmentDeclaration[] | undefi
44
44
  return environments?.map(environmentName);
45
45
  }
46
46
 
47
+ /**
48
+ * Whether a declared environment name is a glob pattern rather than a literal
49
+ * (#1221) — it contains at least one `*`.
50
+ */
51
+ export function isEnvironmentPattern(declaredName: string): boolean {
52
+ return declaredName.includes("*");
53
+ }
54
+
55
+ /**
56
+ * Anchored `*`-glob match (#1221): each `*` in `pattern` matches any run of
57
+ * characters (possibly empty); everything else matches itself. Deliberately
58
+ * the whole surface — no `?`, no character classes, no regex. A pattern with
59
+ * no `*` degenerates to string equality.
60
+ */
61
+ export function matchesEnvironmentPattern(pattern: string, name: string): boolean {
62
+ const parts = pattern.split("*");
63
+ if (parts.length === 1) return pattern === name;
64
+ const first = parts[0];
65
+ const last = parts[parts.length - 1];
66
+ if (!name.startsWith(first)) return false;
67
+ let cursor = first.length;
68
+ for (let i = 1; i < parts.length - 1; i++) {
69
+ const part = parts[i];
70
+ if (part === "") continue;
71
+ const at = name.indexOf(part, cursor);
72
+ if (at === -1) return false;
73
+ cursor = at + part.length;
74
+ }
75
+ return name.length - cursor >= last.length && name.endsWith(last);
76
+ }
77
+
78
+ /**
79
+ * Environment-membership test (#1221): is `name` covered by this project's
80
+ * declared `environments`? A literal entry matches by equality; an entry
81
+ * containing `*` matches as a glob pattern (`"pr-*"` covers `pr-42`).
82
+ * Literals are checked first across the whole list, then patterns — so a
83
+ * name that is declared outright never depends on pattern order. `undefined`
84
+ * / empty `environments` returns `false`; callers that treat "no declared
85
+ * environments" as "anything goes" (e.g. {@link unknownEnvError} in
86
+ * `./env.ts`) guard that case themselves.
87
+ */
88
+ export function matchesDeclaredEnvironment(
89
+ environments: EnvironmentDeclaration[] | undefined,
90
+ name: string,
91
+ ): boolean {
92
+ const names = environmentNames(environments) ?? [];
93
+ if (names.includes(name)) return true;
94
+ return names.some((declared) => isEnvironmentPattern(declared) && matchesEnvironmentPattern(declared, name));
95
+ }
96
+
47
97
  /**
48
98
  * The endpoint `name` declares (chant #1166) — `undefined` for a bare-string
49
99
  * entry, an entry with no `endpoint` set, or a name this project doesn't
@@ -51,9 +101,16 @@ export function environmentNames(environments: EnvironmentDeclaration[] | undefi
51
101
  * it injects this into the ambient env var each observing lexicon's CLI
52
102
  * shell-out reads (e.g. `AWS_ENDPOINT_URL`), unless that var is already set —
53
103
  * ambient always wins.
104
+ *
105
+ * A pattern entry (#1221) can carry an endpoint too: `{ name: "pr-*",
106
+ * endpoint }` supplies the endpoint for every `pr-<n>` environment. A literal
107
+ * entry always wins over a pattern — declaring `pr-special` alongside
108
+ * `pr-*` resolves `pr-special` to the literal entry's endpoint (or none).
54
109
  */
55
110
  export function environmentEndpoint(environments: EnvironmentDeclaration[] | undefined, name: string): string | undefined {
56
- const found = environments?.find((e) => environmentName(e) === name);
111
+ const found =
112
+ environments?.find((e) => environmentName(e) === name) ??
113
+ environments?.find((e) => isEnvironmentPattern(environmentName(e)) && matchesEnvironmentPattern(environmentName(e), name));
57
114
  return found && typeof found !== "string" ? found.endpoint : undefined;
58
115
  }
59
116
 
@@ -124,6 +181,9 @@ export const ChantConfigSchema = z.object({
124
181
  scanner: z.enum(["grype", "trivy"]).optional(),
125
182
  vexSources: z.array(z.string()).optional(),
126
183
  }).optional(),
184
+ knowledge: z.object({
185
+ dir: z.string().min(1).optional(),
186
+ }).optional(),
127
187
  }).passthrough();
128
188
 
129
189
  /**
@@ -343,6 +403,21 @@ export interface ChantConfig {
343
403
  /** Default VEX document paths (OpenVEX/CycloneDX) applied to every gate. Read where the gate step is composed. */
344
404
  vexSources?: string[];
345
405
  };
406
+
407
+ /**
408
+ * OKF knowledge bundle location (#1864, design #1059, epic #1057) — the
409
+ * *input* side of `chant explain --format okf` (#1058): a project may
410
+ * author knowledge as an OKF v0.2 bundle (a directory of markdown files
411
+ * with YAML frontmatter) that `./okf-read.ts`'s `loadOkfBundle` reads and
412
+ * binds to discovered entities via each concept's `binds` frontmatter key.
413
+ * Convention-first: `knowledge/` beside `chant.config.ts` is used when this
414
+ * is unset. Set `dir` only when that name is already taken by something
415
+ * else in the project. See {@link resolveKnowledgeDir}.
416
+ */
417
+ knowledge?: {
418
+ /** Bundle directory, relative to the project root. Defaults to `"knowledge"`. */
419
+ dir?: string;
420
+ };
346
421
  }
347
422
 
348
423
  /**
@@ -726,6 +801,18 @@ export function resolveVulnPolicy(config: ChantConfig): Partial<VulnPolicy> {
726
801
  return out;
727
802
  }
728
803
 
804
+ /**
805
+ * Resolve the OKF knowledge bundle directory (#1864, design #1059):
806
+ * `knowledge.dir` relative to `projectPath` when configured, else the
807
+ * `knowledge/` convention beside `chant.config.ts`. Never checks existence —
808
+ * a project with no bundle yet resolves a path all the same, and
809
+ * `okf-read.ts`'s `loadOkfBundle` treats a missing directory as an empty
810
+ * bundle rather than an error.
811
+ */
812
+ export function resolveKnowledgeDir(config: ChantConfig, projectPath: string): string {
813
+ return join(projectPath, config.knowledge?.dir ?? "knowledge");
814
+ }
815
+
729
816
  /**
730
817
  * Validate and normalize a raw config object into ChantConfig shape.
731
818
  */
@@ -48,6 +48,25 @@ describe("normalizeDeepProperties", () => {
48
48
  expect(Object.keys(out.mid as Record<string, unknown>)).toEqual(["a", "z"]);
49
49
  });
50
50
 
51
+ test("the mask hook collapses a structural secret path to MASKED, on either side (#1830)", () => {
52
+ const hooks: DeepNormalizationHooks = {
53
+ mask: (n) => n.entityType === "K8s::Core::Secret" && n.pattern.startsWith("data."),
54
+ };
55
+ for (const side of ["declared", "live"] as const) {
56
+ const out = normalizeDeepProperties(
57
+ { data: { "app.conf": "c2VjcmV0LWJ5dGVz" }, type: "Opaque" },
58
+ { entityType: "K8s::Core::Secret", side, hooks },
59
+ );
60
+ expect(out).toEqual({ data: { "app.conf": MASKED }, type: "Opaque" });
61
+ }
62
+ // Another entity type sails through the same hook untouched.
63
+ const other = normalizeDeepProperties(
64
+ { data: { "app.conf": "plain" } },
65
+ { entityType: "K8s::Core::ConfigMap", side: "live", hooks },
66
+ );
67
+ expect(other).toEqual({ data: { "app.conf": "plain" } });
68
+ });
69
+
51
70
  test("leaves array order alone with no ordering hook", () => {
52
71
  const out = normalizeDeepProperties({ Tags: [{ Key: "z" }, { Key: "a" }] }, { entityType: "T", side: "live" });
53
72
  expect(out.Tags).toEqual([{ Key: "z" }, { Key: "a" }]);
@@ -266,6 +266,20 @@ export interface DeepNormalizationHooks {
266
266
  * reads as permanent drift.
267
267
  */
268
268
  unresolved?(node: DeepNode): boolean;
269
+ /**
270
+ * Return true when this value is secret material the diff must never hold —
271
+ * it is collapsed to {@link MASKED} on BOTH sides (the hook runs over the
272
+ * declared and the live tree alike), so presence and key names still
273
+ * classify while values never reach a diff row, a log line, or a snapshot.
274
+ *
275
+ * The pass masks by key name on its own ({@link isSensitiveKey}); this hook
276
+ * exists for the lexicons whose secret-bearing paths are structural rather
277
+ * than name-shaped — a Kubernetes Secret's `data` carries arbitrary key
278
+ * names (`app.conf`), and #1365 decision 6 draws the hard line: drift on a
279
+ * secret observes presence, declared key-set, and metadata — never a value
280
+ * or a value-derived hash.
281
+ */
282
+ mask?(node: DeepNode): boolean;
269
283
  }
270
284
 
271
285
  /** Everything the pass needs besides the tree itself. */
@@ -405,6 +419,9 @@ export function normalizeDeepProperties(
405
419
 
406
420
  const normalizeValue = (value: unknown, path: string, pattern: string, key: string): unknown => {
407
421
  if (isSensitiveKey(key)) return MASKED;
422
+ // The lexicon's structural mask — same collapse, path-shaped rather than
423
+ // key-named (a k8s Secret's `data.*` no matter what the key is called).
424
+ if (hooks?.mask?.(nodeOf(path, pattern, key, value))) return MASKED;
408
425
  // A value the lexicon says cannot be known without deploying — an
409
426
  // expression-string reference — collapses exactly like a class-instance
410
427
  // intrinsic does below.