@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.
- package/dist/audit/core.d.ts +21 -3
- package/dist/audit/core.d.ts.map +1 -1
- package/dist/audit/discover.d.ts +3 -2
- package/dist/audit/discover.d.ts.map +1 -1
- package/dist/audit/rules-doc.d.ts.map +1 -1
- package/dist/build.d.ts +3 -3
- package/dist/build.d.ts.map +1 -1
- package/dist/cli/commands/build.d.ts.map +1 -1
- package/dist/cli/commands/check-lexicon.d.ts +14 -0
- package/dist/cli/commands/check-lexicon.d.ts.map +1 -1
- package/dist/cli/commands/lexicon-surface-diff.d.ts +6 -0
- package/dist/cli/commands/lexicon-surface-diff.d.ts.map +1 -1
- package/dist/cli/commands/lint.d.ts.map +1 -1
- package/dist/cli/handlers/lifecycle.d.ts +13 -0
- package/dist/cli/handlers/lifecycle.d.ts.map +1 -1
- package/dist/cli/handlers/search.d.ts.map +1 -1
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/mcp/resource-handlers.d.ts.map +1 -1
- package/dist/cli/mcp/tools/explain.d.ts +6 -0
- package/dist/cli/mcp/tools/explain.d.ts.map +1 -1
- package/dist/cli/plugins.d.ts +1 -1
- package/dist/cli/plugins.d.ts.map +1 -1
- package/dist/cli/registry.d.ts +7 -0
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/cli/reporters/stylish.d.ts +15 -1
- package/dist/cli/reporters/stylish.d.ts.map +1 -1
- package/dist/codegen/lexicon-regen.d.ts +11 -0
- package/dist/codegen/lexicon-regen.d.ts.map +1 -1
- package/dist/codegen/validate.d.ts +10 -0
- package/dist/codegen/validate.d.ts.map +1 -1
- package/dist/components/auto-release.d.ts +4 -0
- package/dist/components/auto-release.d.ts.map +1 -1
- package/dist/components/starter-plugin.d.ts +2 -0
- package/dist/components/starter-plugin.d.ts.map +1 -1
- package/dist/components/verbs/ensure-secret.d.ts +50 -0
- package/dist/components/verbs/ensure-secret.d.ts.map +1 -0
- package/dist/components/verbs/index.d.ts +8 -0
- package/dist/components/verbs/index.d.ts.map +1 -1
- package/dist/components/verbs/r2-sync.d.ts +76 -0
- package/dist/components/verbs/r2-sync.d.ts.map +1 -0
- package/dist/components/verbs/wrangler.d.ts +108 -0
- package/dist/components/verbs/wrangler.d.ts.map +1 -0
- package/dist/config.d.ts +54 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/deep-observation.d.ts +14 -0
- package/dist/deep-observation.d.ts.map +1 -1
- package/dist/effect-receipt.d.ts +177 -0
- package/dist/effect-receipt.d.ts.map +1 -0
- package/dist/env.d.ts +12 -1
- package/dist/env.d.ts.map +1 -1
- package/dist/fold/subset.d.ts +15 -2
- package/dist/fold/subset.d.ts.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/lexicon.d.ts +223 -2
- package/dist/lexicon.d.ts.map +1 -1
- package/dist/lifecycle/change-set.d.ts +33 -5
- package/dist/lifecycle/change-set.d.ts.map +1 -1
- package/dist/lifecycle/index.d.ts +3 -0
- package/dist/lifecycle/index.d.ts.map +1 -1
- package/dist/lifecycle/observation-baseline.d.ts +21 -3
- package/dist/lifecycle/observation-baseline.d.ts.map +1 -1
- package/dist/lifecycle/receipt-plan.d.ts +62 -0
- package/dist/lifecycle/receipt-plan.d.ts.map +1 -0
- package/dist/lifecycle/release-ledger.d.ts +20 -0
- package/dist/lifecycle/release-ledger.d.ts.map +1 -1
- package/dist/lifecycle/teardown.d.ts +132 -0
- package/dist/lifecycle/teardown.d.ts.map +1 -0
- package/dist/lifecycle/unobserved-gate.d.ts +67 -0
- package/dist/lifecycle/unobserved-gate.d.ts.map +1 -0
- package/dist/lint/engine.d.ts +6 -2
- package/dist/lint/engine.d.ts.map +1 -1
- package/dist/lint/knowledge-checks.d.ts +48 -0
- package/dist/lint/knowledge-checks.d.ts.map +1 -0
- package/dist/lint/output-checks.d.ts +5 -0
- package/dist/lint/output-checks.d.ts.map +1 -0
- package/dist/lint/pipeline-change-gate.d.ts +101 -0
- package/dist/lint/pipeline-change-gate.d.ts.map +1 -0
- package/dist/lint/post-synth.d.ts +12 -0
- package/dist/lint/post-synth.d.ts.map +1 -1
- package/dist/lint/receipt-checks.d.ts +9 -0
- package/dist/lint/receipt-checks.d.ts.map +1 -0
- package/dist/lint/rule.d.ts +31 -0
- package/dist/lint/rule.d.ts.map +1 -1
- package/dist/lint/rules/cor021-env-literal-name.d.ts +3 -0
- package/dist/lint/rules/cor021-env-literal-name.d.ts.map +1 -0
- package/dist/lint/rules/cor022-receipt-leaf.d.ts +13 -0
- package/dist/lint/rules/cor022-receipt-leaf.d.ts.map +1 -0
- package/dist/lint/rules/cor024-receipt-secret-pointer.d.ts +3 -0
- package/dist/lint/rules/cor024-receipt-secret-pointer.d.ts.map +1 -0
- package/dist/lint/rules/evl001-non-literal-expression.d.ts.map +1 -1
- package/dist/lint/rules/index.d.ts +4 -1
- package/dist/lint/rules/index.d.ts.map +1 -1
- package/dist/okf-read.d.ts +78 -0
- package/dist/okf-read.d.ts.map +1 -0
- package/dist/op/builders.d.ts +134 -8
- package/dist/op/builders.d.ts.map +1 -1
- package/dist/op/index.d.ts +4 -2
- package/dist/op/index.d.ts.map +1 -1
- package/dist/op/local-executor.d.ts +2 -1
- package/dist/op/local-executor.d.ts.map +1 -1
- package/dist/op/receipt-store.d.ts +138 -0
- package/dist/op/receipt-store.d.ts.map +1 -0
- package/dist/op/types.d.ts +31 -1
- package/dist/op/types.d.ts.map +1 -1
- package/dist/secret-materialization.d.ts +138 -0
- package/dist/secret-materialization.d.ts.map +1 -0
- package/dist/secret-provenance.d.ts +218 -0
- package/dist/secret-provenance.d.ts.map +1 -0
- package/dist/serializer.d.ts +11 -0
- package/dist/serializer.d.ts.map +1 -1
- package/dist/testing.d.ts +136 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/yaml.d.ts.map +1 -1
- package/package.json +9 -1
- package/src/audit/core.test.ts +57 -0
- package/src/audit/core.ts +0 -0
- package/src/audit/detect-bundle.test.ts +1 -1
- package/src/audit/discover.test.ts +24 -0
- package/src/audit/discover.ts +11 -2
- package/src/audit/rules-doc.ts +11 -1
- package/src/build.test.ts +41 -0
- package/src/build.ts +34 -6
- package/src/cli/commands/__fixtures__/audit-fountain/agents/fleet.yaml +27 -0
- package/src/cli/commands/__fixtures__/audit-fountain/k8s/deploy.yaml +16 -0
- package/src/cli/commands/__fixtures__/audit-fountain-clean/fleet.yaml +20 -0
- package/src/cli/commands/audit.test.ts +53 -0
- package/src/cli/commands/audit.ts +1 -1
- package/src/cli/commands/build.test.ts +211 -0
- package/src/cli/commands/build.ts +126 -8
- package/src/cli/commands/check-lexicon.test.ts +45 -1
- package/src/cli/commands/check-lexicon.ts +45 -0
- package/src/cli/commands/lexicon-surface-diff.ts +9 -0
- package/src/cli/commands/lexicon-surface-diff.update.test.ts +112 -0
- package/src/cli/commands/lint.ts +32 -7
- package/src/cli/handlers/explain.test.ts +70 -1
- package/src/cli/handlers/graph.ts +4 -4
- package/src/cli/handlers/lifecycle.test.ts +345 -1
- package/src/cli/handlers/lifecycle.ts +300 -11
- package/src/cli/handlers/search.ts +5 -2
- package/src/cli/main.ts +12 -1
- package/src/cli/mcp/resource-handlers.ts +38 -1
- package/src/cli/mcp/server.test.ts +58 -1
- package/src/cli/mcp/tools/explain.ts +51 -2
- package/src/cli/plugins.ts +4 -2
- package/src/cli/registry.ts +7 -0
- package/src/cli/reporters/stylish.test.ts +154 -0
- package/src/cli/reporters/stylish.ts +154 -33
- package/src/codegen/lexicon-regen.ts +19 -1
- package/src/codegen/validate.test.ts +33 -0
- package/src/codegen/validate.ts +21 -2
- package/src/components/auto-release.ts +6 -0
- package/src/components/registry.test.ts +7 -2
- package/src/components/starter-plugin.ts +17 -0
- package/src/components/verbs/ensure-secret.test.ts +130 -0
- package/src/components/verbs/ensure-secret.ts +79 -0
- package/src/components/verbs/index.ts +8 -0
- package/src/components/verbs/r2-sync.test.ts +107 -0
- package/src/components/verbs/r2-sync.ts +124 -0
- package/src/components/verbs/wrangler.test.ts +170 -0
- package/src/components/verbs/wrangler.ts +241 -0
- package/src/config.test.ts +55 -0
- package/src/config.ts +88 -1
- package/src/deep-observation.test.ts +19 -0
- package/src/deep-observation.ts +17 -0
- package/src/effect-receipt-exclusion.test.ts +190 -0
- package/src/effect-receipt.test.ts +419 -0
- package/src/effect-receipt.ts +412 -0
- package/src/env.test.ts +35 -1
- package/src/env.ts +17 -3
- package/src/fold/subset.test.ts +26 -0
- package/src/fold/subset.ts +45 -19
- package/src/index.ts +4 -0
- package/src/lexicon.ts +227 -2
- package/src/lifecycle/change-set.ts +46 -7
- package/src/lifecycle/index.ts +3 -0
- package/src/lifecycle/observation-baseline.test.ts +46 -0
- package/src/lifecycle/observation-baseline.ts +33 -1
- package/src/lifecycle/receipt-plan.test.ts +250 -0
- package/src/lifecycle/receipt-plan.ts +249 -0
- package/src/lifecycle/release-ledger.ts +20 -0
- package/src/lifecycle/teardown.test.ts +568 -0
- package/src/lifecycle/teardown.ts +359 -0
- package/src/lifecycle/unobserved-gate.test.ts +109 -0
- package/src/lifecycle/unobserved-gate.ts +102 -0
- package/src/lint/engine.ts +7 -1
- package/src/lint/knowledge-checks.test.ts +80 -0
- package/src/lint/knowledge-checks.ts +74 -0
- package/src/lint/output-checks.test.ts +85 -0
- package/src/lint/output-checks.ts +99 -0
- package/src/lint/pipeline-change-gate.test.ts +144 -0
- package/src/lint/pipeline-change-gate.ts +153 -0
- package/src/lint/post-synth.ts +15 -0
- package/src/lint/receipt-checks.test.ts +101 -0
- package/src/lint/receipt-checks.ts +93 -0
- package/src/lint/rule.ts +23 -0
- package/src/lint/rules/cor021-env-literal-name.test.ts +128 -0
- package/src/lint/rules/cor021-env-literal-name.ts +114 -0
- package/src/lint/rules/cor022-receipt-leaf.test.ts +116 -0
- package/src/lint/rules/cor022-receipt-leaf.ts +130 -0
- package/src/lint/rules/cor024-receipt-secret-pointer.test.ts +121 -0
- package/src/lint/rules/cor024-receipt-secret-pointer.ts +218 -0
- package/src/lint/rules/evl001-non-literal-expression.test.ts +27 -0
- package/src/lint/rules/evl001-non-literal-expression.ts +8 -1
- package/src/lint/rules/index.ts +10 -1
- package/src/okf-read.test.ts +149 -0
- package/src/okf-read.ts +197 -0
- package/src/op/builders.ts +179 -8
- package/src/op/effect-step.test.ts +311 -0
- package/src/op/index.ts +10 -3
- package/src/op/local-executor.ts +172 -25
- package/src/op/op.test.ts +25 -2
- package/src/op/receipt-store.ts +211 -0
- package/src/op/types.ts +33 -1
- package/src/secret-materialization.test.ts +199 -0
- package/src/secret-materialization.ts +235 -0
- package/src/secret-provenance.test.ts +388 -0
- package/src/secret-provenance.ts +475 -0
- package/src/serializer.ts +12 -0
- package/src/testing.test.ts +261 -0
- package/src/testing.ts +338 -0
- package/src/yaml.test.ts +88 -0
- package/src/yaml.ts +76 -6
package/src/fold/subset.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
|
261
|
-
* `unobserved` are excluded, since the widget has no column for
|
|
262
|
-
* undeclared", "expected runtime child" (#1077), "no change",
|
|
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
|
-
:
|
|
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}` : ""}`
|
package/src/lifecycle/index.ts
CHANGED
|
@@ -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
|
+
});
|