@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
@@ -231,18 +231,19 @@ export function unsupportedExpressionMessage(node: ts.Node): string {
231
231
  export function checkObjectMember(
232
232
  prop: ts.ObjectLiteralElementLike,
233
233
  intrinsics?: readonly IntrinsicDef[],
234
+ allowCompositeStepAccess?: boolean,
234
235
  ): SubsetViolation | undefined {
235
236
  if (ts.isPropertyAssignment(prop)) {
236
237
  if (!isLiteralPropertyName(prop.name)) {
237
238
  return violation(prop.name, computedPropertyNameMessage(prop.name));
238
239
  }
239
- return findSubsetViolation(prop.initializer, intrinsics);
240
+ return findSubsetViolation(prop.initializer, intrinsics, allowCompositeStepAccess);
240
241
  }
241
242
  if (ts.isShorthandPropertyAssignment(prop)) {
242
243
  return undefined;
243
244
  }
244
245
  if (ts.isSpreadAssignment(prop)) {
245
- return findSubsetViolation(prop.expression, intrinsics);
246
+ return findSubsetViolation(prop.expression, intrinsics, allowCompositeStepAccess);
246
247
  }
247
248
  return violation(prop, UNSUPPORTED_OBJECT_MEMBER_MESSAGE);
248
249
  }
@@ -251,9 +252,10 @@ export function checkObjectMember(
251
252
  function checkArrayElement(
252
253
  el: ts.Expression,
253
254
  intrinsics?: readonly IntrinsicDef[],
255
+ allowCompositeStepAccess?: boolean,
254
256
  ): SubsetViolation | undefined {
255
- if (ts.isSpreadElement(el)) return findSubsetViolation(el.expression, intrinsics);
256
- return findSubsetViolation(el, intrinsics);
257
+ if (ts.isSpreadElement(el)) return findSubsetViolation(el.expression, intrinsics, allowCompositeStepAccess);
258
+ return findSubsetViolation(el, intrinsics, allowCompositeStepAccess);
257
259
  }
258
260
 
259
261
  /**
@@ -265,10 +267,24 @@ function checkArrayElement(
265
267
  * environment-dependent exceptions). Returns the first (deepest,
266
268
  * `fold()`-evaluation-order) unsupported node, or `undefined` when `node`'s
267
269
  * whole shape is foldable.
270
+ *
271
+ * `allowCompositeStepAccess` (chant #1544) is an EVL-only, MORE-permissive
272
+ * divergence in the same direction as points 1/2/2b/2c in the module doc
273
+ * above: a call immediately narrowed to a single member access — the
274
+ * `Checkout({...}).step` shape every lexicon's single-action `Composite()`
275
+ * wrappers document as their embedded-inline idiom (see composites.mdx —
276
+ * "normally embedded inline as `checkout.step` inside a `Job`'s `steps`
277
+ * array") — is treated as shape-valid. `fold()` itself never passes this
278
+ * (it has no such parameter to pass; only EVL001 opts in), so `fold()`'s own
279
+ * behavior is unchanged: it still falls the file back to the run path for
280
+ * this shape, exactly as composites.mdx describes as the expected, correct
281
+ * outcome — not an error. What changes is that EVL001 stops treating that
282
+ * ordinary, documented fallback as a lint error.
268
283
  */
269
284
  export function findSubsetViolation(
270
285
  node: ts.Node,
271
286
  intrinsics?: readonly IntrinsicDef[],
287
+ allowCompositeStepAccess?: boolean,
272
288
  ): SubsetViolation | undefined {
273
289
  if (
274
290
  ts.isParenthesizedExpression(node) ||
@@ -276,7 +292,7 @@ export function findSubsetViolation(
276
292
  ts.isSatisfiesExpression(node) ||
277
293
  ts.isNonNullExpression(node)
278
294
  ) {
279
- return findSubsetViolation(node.expression, intrinsics);
295
+ return findSubsetViolation(node.expression, intrinsics, allowCompositeStepAccess);
280
296
  }
281
297
 
282
298
  if (
@@ -311,7 +327,7 @@ export function findSubsetViolation(
311
327
 
312
328
  if (ts.isTemplateExpression(node)) {
313
329
  for (const span of node.templateSpans) {
314
- const v = findSubsetViolation(span.expression, intrinsics);
330
+ const v = findSubsetViolation(span.expression, intrinsics, allowCompositeStepAccess);
315
331
  if (v) return v;
316
332
  }
317
333
  return undefined;
@@ -319,7 +335,7 @@ export function findSubsetViolation(
319
335
 
320
336
  if (ts.isObjectLiteralExpression(node)) {
321
337
  for (const prop of node.properties) {
322
- const v = checkObjectMember(prop, intrinsics);
338
+ const v = checkObjectMember(prop, intrinsics, allowCompositeStepAccess);
323
339
  if (v) return v;
324
340
  }
325
341
  return undefined;
@@ -327,28 +343,35 @@ export function findSubsetViolation(
327
343
 
328
344
  if (ts.isArrayLiteralExpression(node)) {
329
345
  for (const el of node.elements) {
330
- const v = checkArrayElement(el, intrinsics);
346
+ const v = checkArrayElement(el, intrinsics, allowCompositeStepAccess);
331
347
  if (v) return v;
332
348
  }
333
349
  return undefined;
334
350
  }
335
351
 
336
352
  if (ts.isPropertyAccessExpression(node)) {
337
- return findSubsetViolation(node.expression, intrinsics);
353
+ // chant #1544 — see this function's doc comment. `<call>(...).step`
354
+ // (any callee, any argument shape) is the composite-consumer idiom, EVL
355
+ // opt-in only: `fold()` never sets `allowCompositeStepAccess`, so it is
356
+ // unaffected and still falls back to the run path for this shape.
357
+ if (allowCompositeStepAccess && node.name.text === "step" && ts.isCallExpression(node.expression)) {
358
+ return undefined;
359
+ }
360
+ return findSubsetViolation(node.expression, intrinsics, allowCompositeStepAccess);
338
361
  }
339
362
 
340
363
  if (ts.isElementAccessExpression(node)) {
341
364
  if (!isLiteralElementKey(node.argumentExpression)) {
342
365
  return violation(node.argumentExpression, dynamicElementAccessMessage(node.argumentExpression), "EVL003");
343
366
  }
344
- return findSubsetViolation(node.expression, intrinsics);
367
+ return findSubsetViolation(node.expression, intrinsics, allowCompositeStepAccess);
345
368
  }
346
369
 
347
370
  if (ts.isPrefixUnaryExpression(node)) {
348
371
  if (!SUPPORTED_UNARY_OPERATORS.has(node.operator)) {
349
372
  return violation(node, UNSUPPORTED_UNARY_MESSAGE);
350
373
  }
351
- return findSubsetViolation(node.operand, intrinsics);
374
+ return findSubsetViolation(node.operand, intrinsics, allowCompositeStepAccess);
352
375
  }
353
376
 
354
377
  if (ts.isBinaryExpression(node)) {
@@ -358,15 +381,18 @@ export function findSubsetViolation(
358
381
  }
359
382
  // Flow-insensitive — see module doc: fold() short-circuits &&/||/?? and
360
383
  // only evaluates the taken side; EVL requires both sides shape-valid.
361
- return findSubsetViolation(node.left, intrinsics) ?? findSubsetViolation(node.right, intrinsics);
384
+ return (
385
+ findSubsetViolation(node.left, intrinsics, allowCompositeStepAccess) ??
386
+ findSubsetViolation(node.right, intrinsics, allowCompositeStepAccess)
387
+ );
362
388
  }
363
389
 
364
390
  if (ts.isConditionalExpression(node)) {
365
391
  // Flow-insensitive — see module doc: fold() only folds the taken branch.
366
392
  return (
367
- findSubsetViolation(node.condition, intrinsics) ??
368
- findSubsetViolation(node.whenTrue, intrinsics) ??
369
- findSubsetViolation(node.whenFalse, intrinsics)
393
+ findSubsetViolation(node.condition, intrinsics, allowCompositeStepAccess) ??
394
+ findSubsetViolation(node.whenTrue, intrinsics, allowCompositeStepAccess) ??
395
+ findSubsetViolation(node.whenFalse, intrinsics, allowCompositeStepAccess)
370
396
  );
371
397
  }
372
398
 
@@ -377,14 +403,14 @@ export function findSubsetViolation(
377
403
  // argument is classified on its own terms and nothing is rejected merely
378
404
  // for being in the "wrong" position.
379
405
  for (const arg of node.arguments ?? []) {
380
- const v = findSubsetViolation(arg, intrinsics);
406
+ const v = findSubsetViolation(arg, intrinsics, allowCompositeStepAccess);
381
407
  if (v) return v;
382
408
  }
383
409
  return undefined;
384
410
  }
385
411
 
386
412
  if (ts.isSpreadElement(node)) {
387
- return findSubsetViolation(node.expression, intrinsics);
413
+ return findSubsetViolation(node.expression, intrinsics, allowCompositeStepAccess);
388
414
  }
389
415
 
390
416
  if (ts.isCallExpression(node)) {
@@ -400,7 +426,7 @@ export function findSubsetViolation(
400
426
  // ever be MORE permissive than `fold()`, never stricter.
401
427
  if (ts.isIdentifier(node.expression) && isFoldableHelperName(node.expression.text)) {
402
428
  for (const arg of node.arguments) {
403
- const v = findSubsetViolation(arg, intrinsics);
429
+ const v = findSubsetViolation(arg, intrinsics, allowCompositeStepAccess);
404
430
  if (v) return v;
405
431
  }
406
432
  return undefined;
@@ -420,7 +446,7 @@ export function findSubsetViolation(
420
446
  intrinsics.some((i) => i.name === (node.expression as ts.Identifier).text && intrinsicCallFolds(i))
421
447
  ) {
422
448
  for (const arg of node.arguments) {
423
- const v = findSubsetViolation(arg, intrinsics);
449
+ const v = findSubsetViolation(arg, intrinsics, allowCompositeStepAccess);
424
450
  if (v) return v;
425
451
  }
426
452
  return undefined;
package/src/index.ts CHANGED
@@ -7,6 +7,9 @@
7
7
  export * from "./declarable";
8
8
  export * from "./composite";
9
9
  export * from "./provenance";
10
+ export * from "./secret-provenance";
11
+ export * from "./secret-materialization";
12
+ export * from "./effect-receipt";
10
13
  export * from "./build-params";
11
14
  export * from "./intrinsic";
12
15
  export * from "./types";
@@ -43,6 +46,7 @@ export * from "./lint/selectors";
43
46
  export * from "./lint/named-checks";
44
47
  export * from "./lint/post-synth";
45
48
  export * from "./lint/policy";
49
+ export * from "./lint/pipeline-change-gate";
46
50
  export * from "./lint/rule-loader";
47
51
  export * from "./lint/discover";
48
52
  export * from "./import/parser";
package/src/lexicon.ts CHANGED
@@ -11,12 +11,12 @@ import type { CompletionContext, CompletionItem, HoverContext, HoverInfo, CodeAc
11
11
  import type { McpToolContribution, McpResourceContribution } from "./mcp/types";
12
12
  import type { DriverComponent } from "./components/driver";
13
13
  import type { EmulatorDeclaration } from "./op/emulator-lifecycle";
14
- import type { OwnershipChannel } from "./ownership";
14
+ import type { OwnershipChannel, OwnershipMarker } from "./ownership";
15
15
  import type { LexiconConfigSchema } from "./lexicon-config";
16
16
  import type { RuleMeta } from "./audit/catalog";
17
17
  import type { ReferenceCatalog } from "./graph-refs";
18
18
  import type { IREdge } from "./graph-ir";
19
- import type { DescribeResourcesResult } from "./observation";
19
+ import type { DescribeResourcesResult, UnobservedReason } from "./observation";
20
20
  import type { DeepNormalizationHooks, DeepObservationResult } from "./deep-observation";
21
21
  import type { OwnerChainVerdict } from "./owner-chain";
22
22
  import type { CommandGroup } from "./cli/command-group";
@@ -439,6 +439,24 @@ export interface BuildRootContext {
439
439
  projectRoot: string;
440
440
  /** The resolved project configuration, for the lexicon's own namespace. */
441
441
  config: Record<string, unknown>;
442
+ /**
443
+ * The discovered entities, read-only (#1828 / SOPS provenance). A
444
+ * contributor that reacts to what the project DECLARED — rather than to
445
+ * what its config listed — reads them here: the committed-encrypted
446
+ * `declareSecret()` hook resolves each declaration's `file` into a sidecar
447
+ * entity, and any future declaration-driven contributor needs the same.
448
+ *
449
+ * Read-only on purpose. The merge that follows is the only thing that adds
450
+ * to the entity set, and it refuses a name collision rather than
451
+ * overwriting; a contributor mutating the map directly would slip past
452
+ * that. Contributors run in order, so a contributor also sees entities
453
+ * earlier contributors added.
454
+ *
455
+ * Optional: a caller that has no entity set (a plugin hook invoked
456
+ * directly, a graph mode that never discovered) omits it, and a hook must
457
+ * treat an absent map as an empty one.
458
+ */
459
+ entities?: ReadonlyMap<string, Declarable>;
442
460
  }
443
461
 
444
462
  /**
@@ -450,6 +468,17 @@ export interface BuildRootContribution {
450
468
  warnings?: string[];
451
469
  }
452
470
 
471
+ /**
472
+ * A plugin's `buildRoots` hook, already bound to this invocation's project
473
+ * root and config (`collectBuildRootContributors`, ./cli/plugins.ts). What is
474
+ * NOT bindable that early is the entity set — discovery has not run yet — so
475
+ * the merge supplies it when it calls the closure. A contributor that ignores
476
+ * the argument is still assignable, which is what every pre-#1828 hook does.
477
+ */
478
+ export type BuildRootContributor = (
479
+ ctx: Pick<BuildRootContext, "entities">,
480
+ ) => Promise<BuildRootContribution>;
481
+
453
482
  export interface LexiconPlugin {
454
483
  // ── Required ──────────────────────────────────────────────
455
484
  /** Human-readable name (e.g. "aws", "gcp") */
@@ -540,6 +569,34 @@ export interface LexiconPlugin {
540
569
  */
541
570
  auditCatalog?(): Record<string, RuleMeta>;
542
571
 
572
+ /**
573
+ * Parse standalone template content (a file audit discovery classified for
574
+ * this lexicon) into the lexicon's entity graph, keyed the way `ctx.entities`
575
+ * is during a build. Lets entity-reading post-synth checks fire on
576
+ * `chant audit` of hand-written manifests — parse-to-graph rather than
577
+ * output-reading rule variants (#1567). Implementations must tolerate
578
+ * arbitrary external content: a malformed document yields no entities, never
579
+ * a throw. Omit for lexicons whose audit checks read `ctx.outputs`.
580
+ */
581
+ auditEntities?(content: string): Map<string, Declarable>;
582
+
583
+ /**
584
+ * Machine-readable spec-coverage accounting for `check-lexicon` (#1330).
585
+ *
586
+ * `coverage()` prints a report for humans; this returns the one fact the
587
+ * completeness gate cares about: which upstream spec kinds are neither
588
+ * modeled as declarables nor on the lexicon's exclusion list. fountain held
589
+ * this line in a lexicon-local vitest assertion (`coverage.test.ts`), which
590
+ * is a convention rather than a contract — the same class of gap #1342
591
+ * closed for LSP providers.
592
+ *
593
+ * Implementations must work offline from committed snapshots (fountain
594
+ * reads `spec/fountain-openapi.snapshot.json` plus its surface baseline):
595
+ * `check-lexicon` runs on every PR, so no network I/O. Omit when the
596
+ * lexicon has no kind-level spec accounting; the check passes vacuously.
597
+ */
598
+ coverageReport?(): Promise<{ unaccountedKinds?: string[] }>;
599
+
543
600
  /** Return intrinsic function definitions */
544
601
  intrinsics?(): IntrinsicDef[];
545
602
 
@@ -862,6 +919,86 @@ export interface LexiconPlugin {
862
919
  */
863
920
  describeStackStatus?(options: { environment: string; stack: string }): Promise<StackStatusObservation | null>;
864
921
 
922
+ /**
923
+ * Enumerate the resources this lexicon would delete for one marker identity
924
+ * (#1222). Opt-in, and read-only here: this method names the would-delete
925
+ * set, it never deletes. `chant lifecycle teardown <env>` calls it to plan;
926
+ * the execution half ({@link executeTeardown}) deletes from the same
927
+ * enumeration.
928
+ *
929
+ * Selection is marker-scoped by construction. `marker` carries this
930
+ * project's ownership stack plus the requested environment, and every
931
+ * returned candidate must have been read carrying exactly that identity on
932
+ * this lexicon's marker channel — managed-by present, stack equal, env
933
+ * equal. A resource whose marker is absent, foreign-stack, or foreign-env is
934
+ * not a candidate, ever. Core re-checks each candidate's `marker` and drops
935
+ * mismatches, so a buggy implementation cannot widen the set.
936
+ *
937
+ * The #1089 discipline applies: a kind this lexicon stamps but cannot read
938
+ * back (no reader for the kind, the read errored, no credentials) is a
939
+ * `hole`, named with a total {@link UnobservedReason} — never silently
940
+ * absent, because "absent from the plan" reads as "safe", and an unreadable
941
+ * kind is unknown, not safe.
942
+ *
943
+ * A lexicon without this capability still takes part in teardown planning:
944
+ * core falls back to {@link describeResources} and filters on
945
+ * {@link ResourceMetadata.marker}. Implement this when that read is the
946
+ * wrong shape for deletion — aws, whose thin read carries no tags and whose
947
+ * teardown is stack-level, is the motivating case.
948
+ */
949
+ teardownOwned?(options: {
950
+ environment: string;
951
+ /** The identity to select on: this project's ownership stack + the env being torn down. */
952
+ marker: OwnershipMarker;
953
+ /** Deployed stack name, for a multi-stack project (see `stacks` in {@link ChantConfig}). */
954
+ stack?: string;
955
+ /** Region that stack is deployed in (#1261's contract). */
956
+ region?: string;
957
+ /**
958
+ * Every deployed stack a multi-stack project declares (see `stacks` in
959
+ * {@link ChantConfig}), for a lexicon whose enumeration is stack-shaped
960
+ * (aws). When absent or empty, the single-stack convention applies:
961
+ * `stack`, else the stack named after the environment.
962
+ */
963
+ stacks?: Array<{ name: string; region?: string }>;
964
+ }): Promise<TeardownEnumeration>;
965
+
966
+ /**
967
+ * Delete the teardown candidates core hands over — the execution half of
968
+ * `chant lifecycle teardown <env> --yes` (#1222). Opt-in, and the sibling of
969
+ * {@link teardownOwned}: that method names the would-delete set, this one
970
+ * deletes it. A lexicon that enumerates but does not implement this reports
971
+ * its candidates as skipped rather than pretending.
972
+ *
973
+ * `candidates` is the marker-verified set core computed from the plan — an
974
+ * implementation deletes those and only those, in whatever order its target
975
+ * requires (k8s deletes namespaces last; fly deletes apps last). It never
976
+ * re-widens the set: a live resource not in `candidates` is not this call's
977
+ * business, whatever its labels say.
978
+ *
979
+ * Every candidate gets exactly one outcome per call, keyed by `name`:
980
+ * `deleted` (including already-gone — deletion is idempotent), `failed`
981
+ * (the delete errored; core runs one bounded retry pass over these), or
982
+ * `not-prunable` with a reason (the live object no longer carries the
983
+ * requested identity, the kind cannot be addressed, the target refuses).
984
+ * A candidate the implementation says nothing about is reported as failed
985
+ * by core — silence is never success.
986
+ */
987
+ executeTeardown?(options: {
988
+ environment: string;
989
+ /** The identity everything was selected on: ownership stack + env. */
990
+ marker: OwnershipMarker;
991
+ /** The marker-verified candidates to delete — from {@link teardownOwned} / the plan. */
992
+ candidates: TeardownCandidate[];
993
+ /** Deployed stack name, for a multi-stack project (see `stacks` in {@link ChantConfig}). */
994
+ stack?: string;
995
+ /** Region that stack is deployed in (#1261's contract). */
996
+ region?: string;
997
+ /** Every declared deployed stack, mirroring {@link teardownOwned} — how a
998
+ * stack-shaped execution (aws) finds each candidate's region. */
999
+ stacks?: Array<{ name: string; region?: string }>;
1000
+ }): Promise<TeardownExecution>;
1001
+
865
1002
  /**
866
1003
  * Where this lexicon can stamp and read chant's ownership marker (#1348).
867
1004
  * Data, not a method.
@@ -962,6 +1099,79 @@ export interface LexiconPlugin {
962
1099
  }): Promise<ExportedTemplate>;
963
1100
  }
964
1101
 
1102
+ /**
1103
+ * One resource {@link LexiconPlugin.teardownOwned} would delete (#1222).
1104
+ * Identity only — no delete happens on this path.
1105
+ */
1106
+ export interface TeardownCandidate {
1107
+ /** chant entity name where a declared mapping exists, else the provider-side name. */
1108
+ name: string;
1109
+ /** Resource type (e.g. "AWS::S3::Bucket", "K8s::Apps::Deployment"). */
1110
+ type: string;
1111
+ /** Provider-side identifier, when the read surfaces one. */
1112
+ physicalId?: string;
1113
+ /**
1114
+ * The stack/env identity read off the resource's own marker — read back,
1115
+ * never inferred. Core verifies it equals the requested identity and drops
1116
+ * the candidate otherwise.
1117
+ */
1118
+ marker: OwnershipMarker;
1119
+ }
1120
+
1121
+ /**
1122
+ * One kind or entity a teardown enumeration could not read (#1089). A hole is
1123
+ * a claim of ignorance, not of absence: the plan must print it loudly, and the
1124
+ * execution half must refuse to call the env clean while holes exist.
1125
+ */
1126
+ export interface TeardownHole {
1127
+ /** The unreadable kind or entity name. */
1128
+ name: string;
1129
+ /** Resource type, when known. */
1130
+ type?: string;
1131
+ /** Total verdict — the same vocabulary the observation envelope uses. */
1132
+ reason: UnobservedReason;
1133
+ /** Human-readable detail: the failing command, the unsupported kind. */
1134
+ detail?: string;
1135
+ }
1136
+
1137
+ /** What {@link LexiconPlugin.teardownOwned} returns: the would-delete set plus its holes. */
1138
+ export interface TeardownEnumeration {
1139
+ candidates: TeardownCandidate[];
1140
+ /** Omit or leave empty when every stamped kind was readable. */
1141
+ holes?: TeardownHole[];
1142
+ }
1143
+
1144
+ /**
1145
+ * One candidate's fate after {@link LexiconPlugin.executeTeardown} (#1222).
1146
+ * `name` keys it back to the candidate it answers for.
1147
+ */
1148
+ export interface TeardownOutcome {
1149
+ /** The candidate's `name`, verbatim. */
1150
+ name: string;
1151
+ /** Resource type, when the implementation carries it through. */
1152
+ type?: string;
1153
+ /** Provider-side identifier, when known. */
1154
+ physicalId?: string;
1155
+ /**
1156
+ * - `deleted` — gone, including already-gone (deletion is idempotent);
1157
+ * - `failed` — the delete errored (core retries these once);
1158
+ * - `not-prunable` — deliberately not deleted; `detail` says why;
1159
+ * - `retained` — owned and no longer declared, but deliberately kept
1160
+ * (#1365 decision 5): a `generated-once` secret never enters the prunable
1161
+ * set, because deleting it would destroy the only copy of material chant
1162
+ * never held. Reported loudly, never deleted; deletion is an explicit act
1163
+ * (`kubectl delete`, or a future gated op), never a sweep's.
1164
+ */
1165
+ outcome: "deleted" | "failed" | "not-prunable" | "retained";
1166
+ /** The error for `failed`, the reason for `not-prunable`/`retained`. */
1167
+ detail?: string;
1168
+ }
1169
+
1170
+ /** What {@link LexiconPlugin.executeTeardown} returns: one outcome per candidate. */
1171
+ export interface TeardownExecution {
1172
+ outcomes: TeardownOutcome[];
1173
+ }
1174
+
965
1175
  /**
966
1176
  * The observation view of a lexicon — every capability except live export.
967
1177
  *
@@ -1047,6 +1257,21 @@ export interface ResourceMetadata {
1047
1257
  * a delete, and never escalates `unknown` to one.
1048
1258
  */
1049
1259
  ownership?: "owned" | "foreign" | "unknown";
1260
+ /**
1261
+ * The stack/env identity read off the resource's own ownership marker
1262
+ * (#1222) — the tags/labels/metadata chant stamped at synthesis, read back
1263
+ * verbatim on the same channel. This is what marker-scoped selection keys
1264
+ * on: {@link ownership} says "chant's", `marker` says *which* stack and env.
1265
+ *
1266
+ * Set only when the live model actually carries the channel and the
1267
+ * managed-by marker is present — an absent channel means an absent field,
1268
+ * never a guess. In particular aws's thin read is sourced from
1269
+ * `describe-stack-resources`, which returns no tags at all, so aws never
1270
+ * populates this here; aws teardown is stack-level and reads the stack's own
1271
+ * tags instead (later PR). A populated `marker` does not by itself imply an
1272
+ * {@link ownership} verdict on paths that do not declare a marker channel.
1273
+ */
1274
+ marker?: OwnershipMarker;
1050
1275
  /**
1051
1276
  * Where this resource's owner-reference chain leads, for a live resource
1052
1277
  * that is not itself declared (#1077). A lexicon that maintains an
@@ -31,11 +31,30 @@ import { unobservedReasonText, type UnobservedReason } from "../observation";
31
31
  * not drift, just the runtime doing its job. `runtimeOwner` names the
32
32
  * declared entity it belongs to.
33
33
  * - `noop` — declared and live with no drift, or already reconciled.
34
+ * - `effect` — a declared effect receipt (#1832) whose live value is absent or
35
+ * differs from the resolved expectation: the effect step will fire. Never a
36
+ * `create` or `update` — the generic apply path is observe-only to receipts,
37
+ * and the `effect()` step is the sole writer (epic #1703, decision 3). Read
38
+ * `effect` for the effect's identity and `effectReason` for why it fires.
34
39
  * - `unobserved` — declared, and the lexicon could not look (#1089). Not a
35
40
  * proposal at all: it is the plan admitting a hole. Never a create, never a
36
41
  * delete. Read `unobservedReason` for which hole.
37
42
  */
38
- export type ChangeAction = "create" | "update" | "delete" | "adopt" | "runtime" | "noop" | "unobserved";
43
+ export type ChangeAction = "create" | "update" | "delete" | "adopt" | "runtime" | "noop" | "effect" | "unobserved";
44
+
45
+ /**
46
+ * Why an `effect` entry proposes a fire (#1832).
47
+ *
48
+ * - `receipt-absent` — the provider confirmed the receipt absent: the effect
49
+ * has never recorded a run (or the run crashed before the write — the
50
+ * at-least-once case this classification exists to preserve).
51
+ * - `receipt-stale` — the receipt is live but its value differs from the
52
+ * resolved expectation: the effect's inputs changed since the last run.
53
+ * - `unresolved-input` — a reference input could not resolve at plan time, so
54
+ * the expectation cannot be computed. The fire is proposed rather than
55
+ * guessed away; the effect step resolves again at run.
56
+ */
57
+ export type EffectFireReason = "receipt-absent" | "receipt-stale" | "unresolved-input";
39
58
 
40
59
  /**
41
60
  * Who answers "is this resource chant's?". `unknown` until a live ownership
@@ -100,6 +119,16 @@ export interface ChangeSetEntry {
100
119
  queried?: string;
101
120
  /** The declared entity this resource's owner chain resolves to, for `action: "runtime"` (#1077). */
102
121
  runtimeOwner?: string;
122
+ /**
123
+ * The effect a receipt witnesses (#1832), for entries derived from an effect
124
+ * receipt. On `action: "effect"` it names what will fire; on a receipt's
125
+ * `noop`/`unobserved` rows it keeps the attribution.
126
+ */
127
+ effect?: string;
128
+ /** Why the effect fires, for `action: "effect"` (#1832). */
129
+ effectReason?: EffectFireReason;
130
+ /** Human-readable backing for `effectReason` (the digests that differ, the unresolved path). */
131
+ effectDetail?: string;
103
132
  }
104
133
 
105
134
  export interface ChangeSet {
@@ -234,13 +263,14 @@ export function buildChangeSet(env: string, input: DiffLiveInput, options?: Chan
234
263
  return { env, entries };
235
264
  }
236
265
 
237
- const ACTION_ORDER: ChangeAction[] = ["create", "update", "delete", "adopt", "runtime", "noop", "unobserved"];
266
+ const ACTION_ORDER: ChangeAction[] = ["create", "update", "effect", "delete", "adopt", "runtime", "noop", "unobserved"];
238
267
 
239
268
  /** Count entries per action. */
240
269
  export function summarize(cs: ChangeSet): Record<ChangeAction, number> {
241
270
  const counts: Record<ChangeAction, number> = {
242
271
  create: 0,
243
272
  update: 0,
273
+ effect: 0,
244
274
  delete: 0,
245
275
  adopt: 0,
246
276
  runtime: 0,
@@ -257,10 +287,10 @@ export function summarize(cs: ChangeSet): Record<ChangeAction, number> {
257
287
  * GitLab renders an `artifacts:reports:terraform` artifact in the merge-request
258
288
  * UI as "N to add, M to change, K to delete". The format is generic — any tool
259
289
  * that emits this JSON gets the widget — and the chant plan maps onto it
260
- * directly. Only the mutating actions count: `adopt`, `runtime`, `noop` and
261
- * `unobserved` are excluded, since the widget has no column for "live but
262
- * undeclared", "expected runtime child" (#1077), "no change", or "could not
263
- * look" (#1089). The widget is therefore a floor, not a complete plan: read
290
+ * directly. Only the mutating actions count: `adopt`, `runtime`, `noop`,
291
+ * `effect` and `unobserved` are excluded, since the widget has no column for
292
+ * "live but undeclared", "expected runtime child" (#1077), "no change", "an
293
+ * effect will fire" (#1832), or "could not look" (#1089). The widget is therefore a floor, not a complete plan: read
264
294
  * the full change set when entities are unobserved or classified runtime.
265
295
  *
266
296
  * The widget label reads "Terraform" regardless of producer; that is GitLab's
@@ -292,9 +322,18 @@ export function renderChangeSet(cs: ChangeSet): string {
292
322
  ? "\nUNOBSERVED (declared; chant could not read live state — no action proposed):"
293
323
  : action === "runtime"
294
324
  ? "\nRUNTIME (owned by a declared resource; not drift, never a delete/adopt candidate):"
295
- : `\n${action.toUpperCase()}:`,
325
+ : action === "effect"
326
+ ? "\nEFFECT (receipt absent or stale; the effect step fires — the generic apply never writes a receipt):"
327
+ : `\n${action.toUpperCase()}:`,
296
328
  );
297
329
  for (const e of group) {
330
+ if (e.action === "effect") {
331
+ lines.push(
332
+ ` effect will fire: ${e.effect ?? e.name} — receipt ${e.name}${e.type ? ` (${e.type})` : ""}` +
333
+ `${e.effectDetail ? ` — ${e.effectDetail}` : ""}`,
334
+ );
335
+ continue;
336
+ }
298
337
  const own = e.ownership === "unknown" ? "" : ` [${e.ownership}]`;
299
338
  const why = e.unobservedReason
300
339
  ? ` — ${unobservedReasonText(e.unobservedReason)}${e.unobservedDetail ? `: ${e.unobservedDetail}` : ""}`
@@ -7,9 +7,12 @@ export * from "./deep-diff";
7
7
  export * from "./deep-observe";
8
8
  export * from "./observation-baseline";
9
9
  export * from "./change-set";
10
+ export * from "./unobserved-gate";
11
+ export * from "./receipt-plan";
10
12
  export * from "./affected";
11
13
  export * from "./release-ledger";
12
14
  export * from "./build-ledger";
13
15
  export * from "./build-ledger-store";
14
16
  export * from "./oras-referrer-lookup";
15
17
  export * from "./status";
18
+ export * from "./teardown";
@@ -97,3 +97,49 @@ describe("acceptDeviations", () => {
97
97
  expect(countAccepted(null)).toBe(0);
98
98
  });
99
99
  });
100
+
101
+ describe("acceptDeviations refuses effect receipts (#1833)", () => {
102
+ const now = "2026-08-24T00:00:00.000Z";
103
+
104
+ test("refuses a deviation whose type is the core receipt entityType", () => {
105
+ expect(() =>
106
+ acceptDeviations(emptyBaseline("prod"), "chant", [
107
+ { entity: "seededReceipt", type: "Chant::EffectReceipt", path: "value", value: "gone" },
108
+ ], { now }),
109
+ ).toThrow(/effect receipt "seededReceipt"/);
110
+ });
111
+
112
+ test("refuses a deviation on an entity the caller recognized as a receipt (materialized row)", () => {
113
+ expect(() =>
114
+ acceptDeviations(emptyBaseline("prod"), "aws", [
115
+ { entity: "migratedReceipt", type: "AWS::SSM::Parameter", path: "Value", value: "stale" },
116
+ ], { now, receipts: new Set(["migratedReceipt"]) }),
117
+ ).toThrow(/effect receipt "migratedReceipt"/);
118
+ });
119
+
120
+ test("the refusal names the receipt and the effect step as sole writer, and records nothing", () => {
121
+ const before = emptyBaseline("prod");
122
+ let error: Error | undefined;
123
+ try {
124
+ acceptDeviations(before, "aws", [
125
+ { entity: "Role", type: "AWS::IAM::Role", path: "MaxSessionDuration", value: 7200 },
126
+ { entity: "migratedReceipt", type: "AWS::SSM::Parameter", path: "Value", value: "stale" },
127
+ ], { now, receipts: new Set(["migratedReceipt"]) });
128
+ } catch (e) {
129
+ error = e as Error;
130
+ }
131
+ expect(error).toBeDefined();
132
+ expect(error!.message).toContain('effect receipt "migratedReceipt"');
133
+ expect(error!.message).toContain("only writer");
134
+ expect(error!.message).toContain("defuse the effect");
135
+ // The whole acceptance aborts — the non-receipt row was not recorded either.
136
+ expect(before.lexicons).toEqual({});
137
+ });
138
+
139
+ test("passes: non-receipt deviations accept as before, receipts set present", () => {
140
+ const b = acceptDeviations(emptyBaseline("prod"), "aws", [
141
+ { entity: "Role", type: "AWS::IAM::Role", path: "MaxSessionDuration", value: 7200 },
142
+ ], { now, receipts: new Set(["migratedReceipt"]) });
143
+ expect(baselineForLexicon(b, "aws").Role.accepted).toHaveLength(1);
144
+ });
145
+ });