@intentius/chant 0.124.13 → 0.124.14
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/bin/chant +11 -0
- package/dist/agents/checks.js +326 -0
- package/dist/agents/discover.js +885 -0
- package/dist/agents/importer.js +17 -0
- package/dist/agents/index.js +12 -0
- package/dist/agents/types.js +19 -0
- package/dist/apply.js +151 -0
- package/dist/attrref.js +70 -0
- package/dist/audit/catalog.js +350 -0
- package/dist/audit/core.js +252 -0
- package/dist/audit/discover.js +633 -0
- package/dist/audit/fetch.js +666 -0
- package/dist/audit/lineage.js +91 -0
- package/dist/audit/nginx.js +366 -0
- package/dist/audit/prior-art.js +132 -0
- package/dist/audit/proof.js +321 -0
- package/dist/audit/report-html.js +194 -0
- package/dist/audit/report-model.js +198 -0
- package/dist/audit/report.js +102 -0
- package/dist/audit/rules-doc.js +144 -0
- package/dist/audit/secrets.js +405 -0
- package/dist/audit/terraform-state.js +140 -0
- package/dist/audit/wrangler.js +262 -0
- package/dist/behaviour-delta.js +401 -0
- package/dist/behaviour-engine.js +357 -0
- package/dist/behaviour-graph.js +83 -0
- package/dist/behaviour-http.js +188 -0
- package/dist/behaviour-kinds.js +172 -0
- package/dist/behaviour-overlay.js +50 -0
- package/dist/behaviour-predict.js +188 -0
- package/dist/behaviour-request.js +194 -0
- package/dist/behaviour.js +1325 -0
- package/dist/build-params.js +108 -0
- package/dist/build.js +668 -0
- package/dist/builder.js +44 -0
- package/dist/cdk/advise.js +30 -0
- package/dist/cdk/assembly.js +122 -0
- package/dist/cdk/graph.js +444 -0
- package/dist/cdk/tier-map.js +64 -0
- package/dist/cdk/types.js +24 -0
- package/dist/change-set.js +312 -0
- package/dist/child-project.js +21 -0
- package/dist/claimed-fields.js +135 -0
- package/dist/cli/build-options.js +70 -0
- package/dist/cli/build-params-cli.js +139 -0
- package/dist/cli/command-group.js +123 -0
- package/dist/cli/commands/audit-agents.js +326 -0
- package/dist/cli/commands/audit.js +571 -0
- package/dist/cli/commands/build.js +920 -0
- package/dist/cli/commands/carve-apply.js +153 -0
- package/dist/cli/commands/carve-bridge.js +201 -0
- package/dist/cli/commands/carve-emit.js +332 -0
- package/dist/cli/commands/carve-status.js +119 -0
- package/dist/cli/commands/carve.js +204 -0
- package/dist/cli/commands/check-lexicon-docs.js +79 -0
- package/dist/cli/commands/check-lexicon-examples.js +177 -0
- package/dist/cli/commands/check-lexicon-intrinsics.js +291 -0
- package/dist/cli/commands/check-lexicon-mcp.js +82 -0
- package/dist/cli/commands/check-lexicon-plugin.d.ts.map +1 -1
- package/dist/cli/commands/check-lexicon-plugin.js +104 -0
- package/dist/cli/commands/check-lexicon.js +766 -0
- package/dist/cli/commands/describe.js +85 -0
- package/dist/cli/commands/diff.js +175 -0
- package/dist/cli/commands/doctor.js +350 -0
- package/dist/cli/commands/import-agents.js +155 -0
- package/dist/cli/commands/import.js +670 -0
- package/dist/cli/commands/init-lexicon/templates/codegen.js +194 -0
- package/dist/cli/commands/init-lexicon/templates/docs.js +100 -0
- package/dist/cli/commands/init-lexicon/templates/examples.js +29 -0
- package/dist/cli/commands/init-lexicon/templates/lint.js +34 -0
- package/dist/cli/commands/init-lexicon/templates/lsp.js +37 -0
- package/dist/cli/commands/init-lexicon/templates/plugin.js +108 -0
- package/dist/cli/commands/init-lexicon/templates/project.js +240 -0
- package/dist/cli/commands/init-lexicon/templates/spec.js +56 -0
- package/dist/cli/commands/init-lexicon/templates/tests.js +66 -0
- package/dist/cli/commands/init-lexicon.js +172 -0
- package/dist/cli/commands/init.js +381 -0
- package/dist/cli/commands/lexicon-rolling-upgrade.js +145 -0
- package/dist/cli/commands/lexicon-surface-diff.js +145 -0
- package/dist/cli/commands/licenses.js +125 -0
- package/dist/cli/commands/lint.d.ts.map +1 -1
- package/dist/cli/commands/lint.js +948 -0
- package/dist/cli/commands/list.js +63 -0
- package/dist/cli/commands/migrate.d.ts.map +1 -1
- package/dist/cli/commands/migrate.js +423 -0
- package/dist/cli/commands/onboard.js +403 -0
- package/dist/cli/commands/pinned-upgrade.js +191 -0
- package/dist/cli/commands/update.js +194 -0
- package/dist/cli/commands/vendor.js +217 -0
- package/dist/cli/conflict-check.js +104 -0
- package/dist/cli/debug.js +8 -0
- package/dist/cli/format.js +104 -0
- package/dist/cli/handlers/build.js +267 -0
- package/dist/cli/handlers/carve-apply.js +30 -0
- package/dist/cli/handlers/carve-bridge.js +28 -0
- package/dist/cli/handlers/carve-emit.js +50 -0
- package/dist/cli/handlers/carve-status.js +26 -0
- package/dist/cli/handlers/carve.js +40 -0
- package/dist/cli/handlers/change-set.js +46 -0
- package/dist/cli/handlers/components.js +730 -0
- package/dist/cli/handlers/dev.js +113 -0
- package/dist/cli/handlers/emulator.js +233 -0
- package/dist/cli/handlers/explain.js +54 -0
- package/dist/cli/handlers/fan-out.js +422 -0
- package/dist/cli/handlers/gate-resume.js +275 -0
- package/dist/cli/handlers/generate.js +84 -0
- package/dist/cli/handlers/graph.js +969 -0
- package/dist/cli/handlers/init.js +102 -0
- package/dist/cli/handlers/lifecycle.js +1727 -0
- package/dist/cli/handlers/lint.js +62 -0
- package/dist/cli/handlers/migrate.js +48 -0
- package/dist/cli/handlers/misc.js +391 -0
- package/dist/cli/handlers/operator.js +1162 -0
- package/dist/cli/handlers/override-approve.js +102 -0
- package/dist/cli/handlers/pr.js +503 -0
- package/dist/cli/handlers/promote.js +342 -0
- package/dist/cli/handlers/run-generate.js +206 -0
- package/dist/cli/handlers/run-wave.js +66 -0
- package/dist/cli/handlers/run.js +1191 -0
- package/dist/cli/handlers/scenario.js +325 -0
- package/dist/cli/handlers/search.js +924 -0
- package/dist/cli/handlers/serve.js +32 -0
- package/dist/cli/handlers/vendor.js +154 -0
- package/dist/cli/index.js +3 -0
- package/dist/cli/is-entry-point.js +23 -0
- package/dist/cli/lsp/capabilities.js +28 -0
- package/dist/cli/lsp/diagnostics.js +34 -0
- package/dist/cli/lsp/server.js +329 -0
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/main.js +2540 -0
- package/dist/cli/mcp/lifecycle-tools.js +80 -0
- package/dist/cli/mcp/op-tools.js +306 -0
- package/dist/cli/mcp/resource-handlers.js +258 -0
- package/dist/cli/mcp/resources/context.js +85 -0
- package/dist/cli/mcp/server.js +353 -0
- package/dist/cli/mcp/tools/build.js +111 -0
- package/dist/cli/mcp/tools/composites.js +95 -0
- package/dist/cli/mcp/tools/explain.js +166 -0
- package/dist/cli/mcp/tools/import.js +49 -0
- package/dist/cli/mcp/tools/lint.js +41 -0
- package/dist/cli/mcp/tools/scaffold.js +111 -0
- package/dist/cli/mcp/tools/search.js +112 -0
- package/dist/cli/mcp/types.js +1 -0
- package/dist/cli/mcp/workspace-plugins.js +91 -0
- package/dist/cli/mcp/workspace-tools.d.ts +1 -1
- package/dist/cli/mcp/workspace-tools.js +564 -0
- package/dist/cli/mcp-config.js +70 -0
- package/dist/cli/plugins.js +313 -0
- package/dist/cli/registry.js +23 -0
- package/dist/cli/reporters/stylish.js +329 -0
- package/dist/cli/version.js +15 -0
- package/dist/cli/watch.js +79 -0
- package/dist/codegen/bound-property-types.js +82 -0
- package/dist/codegen/coverage.js +119 -0
- package/dist/codegen/docs-file-markers.js +60 -0
- package/dist/codegen/docs-pages.js +115 -0
- package/dist/codegen/docs-rule-scanning.js +164 -0
- package/dist/codegen/docs-rules.js +76 -0
- package/dist/codegen/docs-sections.js +117 -0
- package/dist/codegen/docs-sidebar.js +65 -0
- package/dist/codegen/docs-types.js +4 -0
- package/dist/codegen/docs.js +356 -0
- package/dist/codegen/fetch.js +340 -0
- package/dist/codegen/generate-post-synth-barrel.js +63 -0
- package/dist/codegen/generate-registry.js +71 -0
- package/dist/codegen/generate-runtime-index.js +56 -0
- package/dist/codegen/generate-typescript.js +119 -0
- package/dist/codegen/generate.js +115 -0
- package/dist/codegen/json-patch.js +141 -0
- package/dist/codegen/json-schema-to-ts.js +344 -0
- package/dist/codegen/json-schema.js +246 -0
- package/dist/codegen/lexicon-regen.js +412 -0
- package/dist/codegen/naming.js +217 -0
- package/dist/codegen/okf-lexicon.js +245 -0
- package/dist/codegen/package.js +150 -0
- package/dist/codegen/pinned-upgrade.d.ts.map +1 -1
- package/dist/codegen/pinned-upgrade.js +494 -0
- package/dist/codegen/registry.js +60 -0
- package/dist/codegen/rehype-base-url.mjs +68 -0
- package/dist/codegen/rolling-upgrade.js +217 -0
- package/dist/codegen/surface-snapshot.js +537 -0
- package/dist/codegen/testdata/postgres-tags.json +242 -0
- package/dist/codegen/topo-sort.js +36 -0
- package/dist/codegen/typecheck.js +115 -0
- package/dist/codegen/validate.js +200 -0
- package/dist/codegen/version-bump.js +35 -0
- package/dist/components/auto-release.js +143 -0
- package/dist/components/builders.js +47 -0
- package/dist/components/capability-plugin-loader.js +175 -0
- package/dist/components/capability-plugin-schema.js +46 -0
- package/dist/components/capability-plugin.js +99 -0
- package/dist/components/capability.js +60 -0
- package/dist/components/changed-files.js +72 -0
- package/dist/components/cli-support.js +427 -0
- package/dist/components/component.js +120 -0
- package/dist/components/component.schema.json +271 -0
- package/dist/components/config-defaults.js +130 -0
- package/dist/components/deploy-units.js +60 -0
- package/dist/components/discover.d.ts.map +1 -1
- package/dist/components/discover.js +189 -0
- package/dist/components/driver-output.js +86 -0
- package/dist/components/driver.js +728 -0
- package/dist/components/fan-out-output.js +165 -0
- package/dist/components/fan-out-record.js +94 -0
- package/dist/components/fan-out-run.js +373 -0
- package/dist/components/fan-out-support.js +60 -0
- package/dist/components/fan-out.js +273 -0
- package/dist/components/gate-plan.js +41 -0
- package/dist/components/gated-wave-pipeline.js +46 -0
- package/dist/components/index.js +24 -0
- package/dist/components/layers.js +28 -0
- package/dist/components/manifest-persistence.js +108 -0
- package/dist/components/pilots/alb-ecs.pilot.js +59 -0
- package/dist/components/pilots/dynamodb.pilot.js +51 -0
- package/dist/components/pilots/jar-emr.pilot.js +65 -0
- package/dist/components/pilots/lambda.pilot.js +52 -0
- package/dist/components/pilots/neo4j-fanout.pilot.js +68 -0
- package/dist/components/pilots/project.js +12 -0
- package/dist/components/pr-pipeline.js +134 -0
- package/dist/components/pr-run.js +363 -0
- package/dist/components/presets/ecs-fargate.js +72 -0
- package/dist/components/presets/index.js +28 -0
- package/dist/components/presets/lambda.js +34 -0
- package/dist/components/presets/single-host-compose.js +41 -0
- package/dist/components/promote.js +549 -0
- package/dist/components/registry.js +34 -0
- package/dist/components/run-progress.js +33 -0
- package/dist/components/sandbox/driver.d.ts.map +1 -1
- package/dist/components/sandbox/driver.js +93 -0
- package/dist/components/sandbox/run.js +115 -0
- package/dist/components/starter-plugin.js +104 -0
- package/dist/components/verbs/__tests__/mock-cloud-executor.js +68 -0
- package/dist/components/verbs/__tests__/mock-process-runner.js +45 -0
- package/dist/components/verbs/__tests__/mock-sbom-generator.js +56 -0
- package/dist/components/verbs/bom-writer.js +287 -0
- package/dist/components/verbs/build-archive.js +188 -0
- package/dist/components/verbs/build.js +142 -0
- package/dist/components/verbs/cloud-executor.js +110 -0
- package/dist/components/verbs/component-bom.js +125 -0
- package/dist/components/verbs/ensure-secret.js +50 -0
- package/dist/components/verbs/index.js +61 -0
- package/dist/components/verbs/license-policy.js +75 -0
- package/dist/components/verbs/lockfile-sbom-generator.js +191 -0
- package/dist/components/verbs/process-runner.js +78 -0
- package/dist/components/verbs/r2-sync.js +93 -0
- package/dist/components/verbs/reproducibility.js +62 -0
- package/dist/components/verbs/run-agent.js +577 -0
- package/dist/components/verbs/sbom-generator.js +119 -0
- package/dist/components/verbs/sbom.js +97 -0
- package/dist/components/verbs/shell.js +32 -0
- package/dist/components/verbs/sign.js +221 -0
- package/dist/components/verbs/stub.js +28 -0
- package/dist/components/verbs/tool-sbom-generator.js +154 -0
- package/dist/components/verbs/verify.js +112 -0
- package/dist/components/verbs/vex.js +131 -0
- package/dist/components/verbs/vuln-gate.js +152 -0
- package/dist/components/verbs/vuln-scan.js +288 -0
- package/dist/components/verbs/wait-verify.js +106 -0
- package/dist/components/verbs/wrangler.js +179 -0
- package/dist/components/wave-plan.js +102 -0
- package/dist/composite.js +305 -0
- package/dist/config-import.d.ts.map +1 -1
- package/dist/config-import.js +22 -0
- package/dist/config-sandbox.js +122 -0
- package/dist/config-static.js +206 -0
- package/dist/config.js +605 -0
- package/dist/content-digest.js +23 -0
- package/dist/declarable.js +114 -0
- package/dist/deep-observation.js +437 -0
- package/dist/detectLexicon.js +54 -0
- package/dist/discovery/cache.js +70 -0
- package/dist/discovery/collect.js +208 -0
- package/dist/discovery/cycles.js +95 -0
- package/dist/discovery/entity-wire-codec.js +391 -0
- package/dist/discovery/entity-wire.js +42 -0
- package/dist/discovery/files.js +121 -0
- package/dist/discovery/fold-import.d.ts.map +1 -1
- package/dist/discovery/fold-import.js +4178 -0
- package/dist/discovery/fold-rank.js +292 -0
- package/dist/discovery/graph.js +115 -0
- package/dist/discovery/import.d.ts.map +1 -1
- package/dist/discovery/import.js +65 -0
- package/dist/discovery/index.js +333 -0
- package/dist/discovery/param-deps.js +487 -0
- package/dist/discovery/resolve.js +39 -0
- package/dist/discovery/sandbox/bundle.js +214 -0
- package/dist/discovery/sandbox/child-errors.js +26 -0
- package/dist/discovery/sandbox/config-run.js +100 -0
- package/dist/discovery/sandbox/config-wire.js +183 -0
- package/dist/discovery/sandbox/driver.d.ts.map +1 -1
- package/dist/discovery/sandbox/driver.js +333 -0
- package/dist/discovery/sandbox/fork.js +229 -0
- package/dist/discovery/sandbox/policy-run.js +142 -0
- package/dist/discovery/sandbox/policy-wire.js +152 -0
- package/dist/discovery/sandbox/run.js +77 -0
- package/dist/discovery/specifier-resolve.js +320 -0
- package/dist/discovery/walk.js +284 -0
- package/dist/effect-receipt.js +340 -0
- package/dist/env.js +55 -0
- package/dist/errors.js +62 -0
- package/dist/fold/fold.js +1434 -0
- package/dist/fold/foldable-helpers.js +229 -0
- package/dist/fold/subset.js +696 -0
- package/dist/fold-provenance.js +414 -0
- package/dist/gate-decision.js +260 -0
- package/dist/gate-resume-run.js +154 -0
- package/dist/gated-waves.js +165 -0
- package/dist/governance.js +43 -0
- package/dist/graph-declared.js +45 -0
- package/dist/graph-detail.js +207 -0
- package/dist/graph-dot.js +54 -0
- package/dist/graph-effective.js +107 -0
- package/dist/graph-ir.js +623 -0
- package/dist/graph-layout.js +184 -0
- package/dist/graph-lens.js +105 -0
- package/dist/graph-mermaid.js +83 -0
- package/dist/graph-ops.js +39 -0
- package/dist/graph-refs.js +156 -0
- package/dist/held-elsewhere.js +87 -0
- package/dist/identity.js +251 -0
- package/dist/import/base-parser.js +34 -0
- package/dist/import/embedded.js +255 -0
- package/dist/import/generator.js +1 -0
- package/dist/import/ir-utils.js +85 -0
- package/dist/import/parser-options.js +45 -0
- package/dist/import/parser.js +1 -0
- package/dist/index.js +118 -0
- package/dist/intrinsic-interpolation.js +73 -0
- package/dist/intrinsic.js +31 -0
- package/dist/join-key.js +35 -0
- package/dist/kubectl-context.js +150 -0
- package/dist/lexicon-config.js +58 -0
- package/dist/lexicon-integrity.js +47 -0
- package/dist/lexicon-manifest.js +70 -0
- package/dist/lexicon-module.d.ts.map +1 -1
- package/dist/lexicon-module.js +241 -0
- package/dist/lexicon-output.js +218 -0
- package/dist/lexicon-plugin-helpers.js +104 -0
- package/dist/lexicon-schema.js +136 -0
- package/dist/lexicon.js +130 -0
- package/dist/lifecycle/affected.js +224 -0
- package/dist/lifecycle/assert-live.js +110 -0
- package/dist/lifecycle/build-ledger-store.js +160 -0
- package/dist/lifecycle/build-ledger.js +155 -0
- package/dist/lifecycle/change-set.js +356 -0
- package/dist/lifecycle/converge-ledger.js +161 -0
- package/dist/lifecycle/deep-diff.js +233 -0
- package/dist/lifecycle/deep-observe.js +147 -0
- package/dist/lifecycle/digest.js +143 -0
- package/dist/lifecycle/disruption.js +135 -0
- package/dist/lifecycle/gate-applied.js +113 -0
- package/dist/lifecycle/gate-audit.js +341 -0
- package/dist/lifecycle/gate-ledger-select.js +89 -0
- package/dist/lifecycle/gate-ledger.js +193 -0
- package/dist/lifecycle/gate-origin.js +117 -0
- package/dist/lifecycle/gate-pending.js +58 -0
- package/dist/lifecycle/git.js +1182 -0
- package/dist/lifecycle/identity.js +58 -0
- package/dist/lifecycle/index.js +26 -0
- package/dist/lifecycle/lease.js +283 -0
- package/dist/lifecycle/ledger-port.js +1 -0
- package/dist/lifecycle/legacy-digest.js +91 -0
- package/dist/lifecycle/live-diff.js +274 -0
- package/dist/lifecycle/local-note.js +15 -0
- package/dist/lifecycle/member-ledger.js +122 -0
- package/dist/lifecycle/observation-baseline.js +155 -0
- package/dist/lifecycle/observe.js +375 -0
- package/dist/lifecycle/oras-referrer-lookup.js +82 -0
- package/dist/lifecycle/override-ledger.js +54 -0
- package/dist/lifecycle/plan-digest.js +110 -0
- package/dist/lifecycle/plan-ledger.js +129 -0
- package/dist/lifecycle/receipt-plan.js +192 -0
- package/dist/lifecycle/release-ledger.js +155 -0
- package/dist/lifecycle/release-read.js +64 -0
- package/dist/lifecycle/replay.js +193 -0
- package/dist/lifecycle/rollback.js +102 -0
- package/dist/lifecycle/run-ledger.js +183 -0
- package/dist/lifecycle/scenario-eval.js +204 -0
- package/dist/lifecycle/scenario.js +214 -0
- package/dist/lifecycle/snapshot.js +239 -0
- package/dist/lifecycle/status.js +447 -0
- package/dist/lifecycle/symptoms.js +117 -0
- package/dist/lifecycle/teardown.js +255 -0
- package/dist/lifecycle/types.js +1 -0
- package/dist/lifecycle/unobserved-gate.js +33 -0
- package/dist/lifecycle/wave-plans.js +68 -0
- package/dist/lifecycle/work-item-id.js +36 -0
- package/dist/lifecycle/work-lease.js +393 -0
- package/dist/lint/component-checks.js +87 -0
- package/dist/lint/config.js +483 -0
- package/dist/lint/declarative.js +122 -0
- package/dist/lint/discover.d.ts.map +1 -1
- package/dist/lint/discover.js +138 -0
- package/dist/lint/engine.js +246 -0
- package/dist/lint/knowledge-checks.js +64 -0
- package/dist/lint/named-checks.js +22 -0
- package/dist/lint/output-checks.js +87 -0
- package/dist/lint/output-docs.js +163 -0
- package/dist/lint/parser.js +33 -0
- package/dist/lint/pipeline-change-gate.js +70 -0
- package/dist/lint/policy-import.d.ts +1 -1
- package/dist/lint/policy-import.d.ts.map +1 -1
- package/dist/lint/policy-import.js +62 -0
- package/dist/lint/policy-sandbox.js +86 -0
- package/dist/lint/policy.js +122 -0
- package/dist/lint/post-synth.js +87 -0
- package/dist/lint/presets/relaxed.json +26 -0
- package/dist/lint/presets/strict.json +24 -24
- package/dist/lint/receipt-checks.js +83 -0
- package/dist/lint/rule-loader.d.ts.map +1 -1
- package/dist/lint/rule-loader.js +89 -0
- package/dist/lint/rule-registry.js +86 -0
- package/dist/lint/rule.js +1 -0
- package/dist/lint/rules/__fixtures__/comp/comp000-discovery-error/broken-a.component.js +7 -0
- package/dist/lint/rules/__fixtures__/comp/comp000-discovery-error/broken-b.component.js +7 -0
- package/dist/lint/rules/__fixtures__/comp/comp001/fail/orphan-lib.component.js +9 -0
- package/dist/lint/rules/__fixtures__/comp/comp001/pass/emr-job.component.js +11 -0
- package/dist/lint/rules/__fixtures__/comp/comp001/pass/jar-lib.component.js +9 -0
- package/dist/lint/rules/__fixtures__/comp/comp002/fail/search-service.component.js +25 -0
- package/dist/lint/rules/__fixtures__/comp/comp002/pass/jar-lib.component.js +8 -0
- package/dist/lint/rules/__fixtures__/comp/comp002/pass/search-service.component.js +21 -0
- package/dist/lint/rules/__fixtures__/comp/comp003/fail/migrator.component.js +8 -0
- package/dist/lint/rules/__fixtures__/comp/comp003/pass/agent-turn.component.js +24 -0
- package/dist/lint/rules/__fixtures__/comp/comp003/pass/migrator.component.js +17 -0
- package/dist/lint/rules/__fixtures__/comp/comp003/pass/orders-table.component.js +8 -0
- package/dist/lint/rules/__fixtures__/comp/comp004/fail/neo4j-cluster.component.js +18 -0
- package/dist/lint/rules/__fixtures__/comp/comp004/pass/neo4j-cluster.component.js +20 -0
- package/dist/lint/rules/__fixtures__/comp/comp004/pass/orders-table.component.js +8 -0
- package/dist/lint/rules/__fixtures__/comp/comp005/fail/search-service.component.js +12 -0
- package/dist/lint/rules/__fixtures__/comp/comp005/pass/search-service.component.js +16 -0
- package/dist/lint/rules/__fixtures__/comp/comp006/fail/legacy-tool.component.js +8 -0
- package/dist/lint/rules/__fixtures__/comp/comp006/pass/legacy-tool.component.js +12 -0
- package/dist/lint/rules/__fixtures__/comp/comp007/fail/inventory-table.component.js +11 -0
- package/dist/lint/rules/__fixtures__/comp/comp007/fail/orders-table.component.js +11 -0
- package/dist/lint/rules/__fixtures__/comp/comp007/pass/orders-table.component.js +10 -0
- package/dist/lint/rules/__fixtures__/comp/comp007/pass/search-service.component.js +16 -0
- package/dist/lint/rules/comp/comp001-publishes-never-applies.js +69 -0
- package/dist/lint/rules/comp/comp002-dangling-wiring-ref.js +84 -0
- package/dist/lint/rules/comp/comp003-mutating-no-rollback.js +103 -0
- package/dist/lint/rules/comp/comp004-gate-needs-durable-runtime.js +70 -0
- package/dist/lint/rules/comp/comp005-capability-kind-is-noun.js +109 -0
- package/dist/lint/rules/comp/comp006-shell-needs-reason.js +49 -0
- package/dist/lint/rules/comp/comp007-composition-sprawl.js +64 -0
- package/dist/lint/rules/comp/index.js +33 -0
- package/dist/lint/rules/comp/support.js +129 -0
- package/dist/lint/rules/composite-scope.js +51 -0
- package/dist/lint/rules/cor017-composite-name-match.js +93 -0
- package/dist/lint/rules/cor018-composite-prefer-lexicon-type.js +136 -0
- package/dist/lint/rules/cor021-env-literal-name.js +100 -0
- package/dist/lint/rules/cor022-receipt-leaf.js +118 -0
- package/dist/lint/rules/cor024-receipt-secret-pointer.js +199 -0
- package/dist/lint/rules/declarable-naming-convention.js +62 -0
- package/dist/lint/rules/evl001-non-literal-expression.js +66 -0
- package/dist/lint/rules/evl002-control-flow-resource.js +69 -0
- package/dist/lint/rules/evl003-dynamic-property-access.js +44 -0
- package/dist/lint/rules/evl004-spread-non-const.js +94 -0
- package/dist/lint/rules/evl005-resource-block-body.js +41 -0
- package/dist/lint/rules/evl007-invalid-siblings.js +105 -0
- package/dist/lint/rules/evl009-composite-no-constant.js +158 -0
- package/dist/lint/rules/evl010-composite-no-transform.js +59 -0
- package/dist/lint/rules/evl011-symbolic-in-template.js +106 -0
- package/dist/lint/rules/export-required.js +115 -0
- package/dist/lint/rules/file-declarable-limit.js +88 -0
- package/dist/lint/rules/flat-declarations.js +62 -0
- package/dist/lint/rules/index.js +90 -0
- package/dist/lint/rules/no-cyclic-declarable-ref.js +138 -0
- package/dist/lint/rules/no-redundant-type-import.js +76 -0
- package/dist/lint/rules/no-redundant-value-cast.js +41 -0
- package/dist/lint/rules/no-string-ref.js +59 -0
- package/dist/lint/rules/no-unused-declarable-import.js +75 -0
- package/dist/lint/rules/no-unused-declarable.js +142 -0
- package/dist/lint/rules/op/index.js +28 -0
- package/dist/lint/rules/op/ops012-activity-contract.js +54 -0
- package/dist/lint/rules/op/ops013-step-output-ref.js +88 -0
- package/dist/lint/rules/op/ops014-converge-rule-refusals.js +114 -0
- package/dist/lint/rules/op/ops015-gate-approval.js +45 -0
- package/dist/lint/rules/op/support.js +11 -0
- package/dist/lint/rules/op/sys010-key-over-plain-http.js +100 -0
- package/dist/lint/rules/property-kind.js +56 -0
- package/dist/lint/rules/single-concern-file.js +71 -0
- package/dist/lint/rules/sys001-no-literal-key.js +60 -0
- package/dist/lint/selectors.js +168 -0
- package/dist/lint/suppressions.js +129 -0
- package/dist/lint/typescript.js +17 -0
- package/dist/live-endpoint.js +113 -0
- package/dist/live-read-session.js +76 -0
- package/dist/lsp/lexicon-providers.js +163 -0
- package/dist/lsp/types.js +5 -0
- package/dist/managed-fields.js +293 -0
- package/dist/mcp/types.js +4 -0
- package/dist/notify-verify.js +0 -1
- package/dist/notify.js +0 -1
- package/dist/notify.schema.json +72 -0
- package/dist/observation.js +266 -0
- package/dist/okf-read.js +134 -0
- package/dist/okf.js +275 -0
- package/dist/op/__fixtures__/decide-stub-backend.js +125 -0
- package/dist/op/activities/activity-contracts.js +313 -0
- package/dist/op/activities/apply.js +582 -0
- package/dist/op/activities/build.js +18 -0
- package/dist/op/activities/change-coverage.js +40 -0
- package/dist/op/activities/change-set.js +64 -0
- package/dist/op/activities/converge.js +492 -0
- package/dist/op/activities/decide.js +211 -0
- package/dist/op/activities/env-teardown.js +110 -0
- package/dist/op/activities/factory.js +591 -0
- package/dist/op/activities/forge-pull-request.js +82 -0
- package/dist/op/activities/gh-host.js +78 -0
- package/dist/op/activities/guard-validate.js +165 -0
- package/dist/op/activities/http-check.js +40 -0
- package/dist/op/activities/index.js +59 -0
- package/dist/op/activities/lexicon-upgrade.js +629 -0
- package/dist/op/activities/lifecycle.js +109 -0
- package/dist/op/activities/pipeline-audit.js +147 -0
- package/dist/op/activities/policy.js +26 -0
- package/dist/op/activities/predict-behaviour.js +341 -0
- package/dist/op/activities/propose-upgrade.js +165 -0
- package/dist/op/activities/reconcile.js +997 -0
- package/dist/op/activities/shell.js +118 -0
- package/dist/op/activities/source-release.js +97 -0
- package/dist/op/activities/source-rollback.js +165 -0
- package/dist/op/activities/teardown.js +17 -0
- package/dist/op/activities/util.js +6 -0
- package/dist/op/activities/wait.js +51 -0
- package/dist/op/activities/work-evidence.js +35 -0
- package/dist/op/activities/workflow-audit.js +133 -0
- package/dist/op/activity-contract-registry.js +106 -0
- package/dist/op/activity-contract.js +232 -0
- package/dist/op/activity-failure.js +31 -0
- package/dist/op/activity-profiles.js +129 -0
- package/dist/op/activity-registry.js +105 -0
- package/dist/op/activity-runtime.js +28 -0
- package/dist/op/builders.js +801 -0
- package/dist/op/change-signal.js +118 -0
- package/dist/op/composites/apply-op.js +151 -0
- package/dist/op/composites/behaviour-op.js +65 -0
- package/dist/op/composites/converge-op.js +141 -0
- package/dist/op/composites/index.js +19 -0
- package/dist/op/composites/lexicon-upgrade-op.js +107 -0
- package/dist/op/composites/pipeline-audit-op.js +50 -0
- package/dist/op/composites/reconcile-op.js +84 -0
- package/dist/op/composites/watch-op.js +69 -0
- package/dist/op/composites/workflow-audit-op.js +57 -0
- package/dist/op/converge-rule.js +205 -0
- package/dist/op/cron.js +146 -0
- package/dist/op/decide-backend.js +181 -0
- package/dist/op/decide-config.js +68 -0
- package/dist/op/decide-read-inputs.js +141 -0
- package/dist/op/discover.d.ts.map +1 -1
- package/dist/op/discover.js +213 -0
- package/dist/op/duration.js +21 -0
- package/dist/op/emulator-freshness.js +80 -0
- package/dist/op/emulator-lifecycle.js +269 -0
- package/dist/op/factory.js +83 -0
- package/dist/op/gate-approval.js +172 -0
- package/dist/op/gate-name.js +86 -0
- package/dist/op/gate-plan-context.js +167 -0
- package/dist/op/gate-point-run.js +67 -0
- package/dist/op/gate-point.js +111 -0
- package/dist/op/gate-resume-plan.js +155 -0
- package/dist/op/gate-resume.js +31 -0
- package/dist/op/gate-review.js +290 -0
- package/dist/op/gate-run-locator.js +85 -0
- package/dist/op/gate-summary.js +100 -0
- package/dist/op/gate-wait.js +56 -0
- package/dist/op/gate.js +449 -0
- package/dist/op/generate-pipeline.js +124 -0
- package/dist/op/index.js +51 -0
- package/dist/op/lifecycle-receipt-store.js +45 -0
- package/dist/op/local-executor.js +1164 -0
- package/dist/op/local-output.js +140 -0
- package/dist/op/op-ir.js +317 -0
- package/dist/op/op-verb-class.js +131 -0
- package/dist/op/op-waves-notify.js +136 -0
- package/dist/op/op-waves-run.js +1657 -0
- package/dist/op/op-waves.js +409 -0
- package/dist/op/operator.js +743 -0
- package/dist/op/receipt-store.js +94 -0
- package/dist/op/resource.js +23 -0
- package/dist/op/run-context.js +28 -0
- package/dist/op/run-live.js +256 -0
- package/dist/op/runtime.js +21 -0
- package/dist/op/runtimes/local.js +195 -0
- package/dist/op/source-archive.js +131 -0
- package/dist/op/step-output-ref.js +397 -0
- package/dist/op/steward-beside.js +169 -0
- package/dist/op/steward-points.js +138 -0
- package/dist/op/steward-turn.js +59 -0
- package/dist/op/steward.js +317 -0
- package/dist/op/types.js +25 -0
- package/dist/op/work-lease-decl.js +81 -0
- package/dist/op/work-lease-run.js +393 -0
- package/dist/override-gate.js +284 -0
- package/dist/owner-chain.js +74 -0
- package/dist/ownership.js +92 -0
- package/dist/params.js +61 -0
- package/dist/plan-summary.js +711 -0
- package/dist/point-decision.js +211 -0
- package/dist/pr-forge.js +810 -0
- package/dist/pr-loop.js +203 -0
- package/dist/project-codegen.js +191 -0
- package/dist/project-root.js +74 -0
- package/dist/project-validation.js +50 -0
- package/dist/provenance.js +157 -0
- package/dist/pseudo-parameter.js +40 -0
- package/dist/reconcile.js +450 -0
- package/dist/resource-attributes.js +33 -0
- package/dist/runtime-adapter.js +77 -0
- package/dist/runtime.js +66 -0
- package/dist/secret-materialization.js +153 -0
- package/dist/secret-provenance.js +250 -0
- package/dist/serializer-walker.js +112 -0
- package/dist/serializer.js +1 -0
- package/dist/sort.js +45 -0
- package/dist/stack-output.js +115 -0
- package/dist/telemetry-attribution.js +149 -0
- package/dist/terraform/__fixtures__/build-graph.js +63 -0
- package/dist/terraform/adopt-state.js +40 -0
- package/dist/terraform/aws-resources.js +384 -0
- package/dist/terraform/bridge.js +204 -0
- package/dist/terraform/carve-provider.js +155 -0
- package/dist/terraform/carve.js +138 -0
- package/dist/terraform/data-source-shape.js +57 -0
- package/dist/terraform/emit-source.js +36 -0
- package/dist/terraform/excise.js +123 -0
- package/dist/terraform/graduate.js +128 -0
- package/dist/terraform/graph.js +435 -0
- package/dist/terraform/hcl2json.js +60 -0
- package/dist/terraform/manifest.js +149 -0
- package/dist/terraform/parse.js +81 -0
- package/dist/terraform/providers/aws.js +146 -0
- package/dist/terraform/providers/gcp.js +294 -0
- package/dist/terraform/providers/index.js +17 -0
- package/dist/terraform/providers/kubernetes.js +253 -0
- package/dist/terraform/score.js +151 -0
- package/dist/terraform/state.js +67 -0
- package/dist/terraform/tier-map.js +74 -0
- package/dist/terraform/types.js +13 -0
- package/dist/terraform/unified-diff.js +132 -0
- package/dist/testing.js +295 -0
- package/dist/toml-emit.js +153 -0
- package/dist/toml-parse.js +355 -0
- package/dist/toml-utils.js +60 -0
- package/dist/toml.js +428 -0
- package/dist/ts-loader.d.ts +46 -0
- package/dist/ts-loader.d.ts.map +1 -0
- package/dist/ts-loader.js +103 -0
- package/dist/types.js +1 -0
- package/dist/utils.js +61 -0
- package/dist/validation.js +20 -0
- package/dist/vendor/js-yaml-load.js +1662 -0
- package/dist/workspace/__fixtures__/contract-repo.js +103 -0
- package/dist/workspace/__fixtures__/decide-workspace.js +112 -0
- package/dist/workspace/__fixtures__/sessions.js +91 -0
- package/dist/workspace/__fixtures__/two-member-workspace.js +34 -0
- package/dist/workspace/agent-cli.js +99 -0
- package/dist/workspace/agent.schema.json +390 -0
- package/dist/workspace/answers-ledger.js +89 -0
- package/dist/workspace/box-cli.js +142 -0
- package/dist/workspace/box-factory-write.js +196 -0
- package/dist/workspace/box-factory-write.schema.json +342 -0
- package/dist/workspace/box-factory.js +88 -0
- package/dist/workspace/box-intent.js +126 -0
- package/dist/workspace/box-isolation.js +169 -0
- package/dist/workspace/box-listing-write.schema.json +204 -0
- package/dist/workspace/box-listing.js +298 -0
- package/dist/workspace/box-publish.js +352 -0
- package/dist/workspace/box-publish.schema.json +498 -0
- package/dist/workspace/box-services.js +39 -0
- package/dist/workspace/broker-protocol.js +359 -0
- package/dist/workspace/broker-protocol.schema.json +228 -0
- package/dist/workspace/change-set.schema.json +460 -0
- package/dist/workspace/changes-cli.js +56 -0
- package/dist/workspace/changes.js +446 -0
- package/dist/workspace/changes.schema.json +642 -0
- package/dist/workspace/chant-migrations/chud-lexicon-exit-fly-site.js +198 -0
- package/dist/workspace/chant-migrations/chud-lexicon-exit-live-names.js +155 -0
- package/dist/workspace/chant-migrations/chud-lexicon-exit-rollback.js +214 -0
- package/dist/workspace/chant-migrations/chud-lexicon-exit-ship-inputs.js +180 -0
- package/dist/workspace/chant-migrations/chud-lexicon-exit-ship-policy.js +99 -0
- package/dist/workspace/chant-migrations/chud-lexicon-exit.js +847 -0
- package/dist/workspace/chant-migrations.js +113 -0
- package/dist/workspace/check.schema.json +433 -0
- package/dist/workspace/checks/box-isolation.js +62 -0
- package/dist/workspace/checks/boxes.js +491 -0
- package/dist/workspace/checks/diagrams.js +104 -0
- package/dist/workspace/checks/generated.js +419 -0
- package/dist/workspace/checks/ledgers.js +212 -0
- package/dist/workspace/checks/links.js +191 -0
- package/dist/workspace/checks/live.js +100 -0
- package/dist/workspace/checks/pipelines.js +188 -0
- package/dist/workspace/checks/records.js +169 -0
- package/dist/workspace/checks.js +426 -0
- package/dist/workspace/ci-green-cli.js +125 -0
- package/dist/workspace/ci-green-forge.js +83 -0
- package/dist/workspace/ci-green-workflow.js +199 -0
- package/dist/workspace/ci-green.js +325 -0
- package/dist/workspace/ci-last-green.schema.json +158 -0
- package/dist/workspace/compose-graph.js +235 -0
- package/dist/workspace/compose-reports.js +146 -0
- package/dist/workspace/composites.js +264 -0
- package/dist/workspace/composites.schema.json +617 -0
- package/dist/workspace/conformance/__fixture__/app/package.json +7 -0
- package/dist/workspace/conformance/__fixture__/app/src/server.mjs +29 -0
- package/dist/workspace/conformance/__fixture__/decisions/decision.kind.mjs +54 -0
- package/dist/workspace/conformance/__fixture__/decisions/decision.schema.json +512 -0
- package/dist/workspace/conformance/__fixture__/decisions/fix-001-how-the-app-is-deployed.md +40 -0
- package/dist/workspace/conformance/__fixture__/delivery/chant.config.ts +7 -0
- package/dist/workspace/conformance/__fixture__/delivery/lexicon/index.ts +26 -0
- package/dist/workspace/conformance/__fixture__/delivery/package.json +7 -0
- package/dist/workspace/conformance/__fixture__/delivery/src/app.component.ts +14 -0
- package/dist/workspace/conformance/__fixture__/delivery/src/app.ts +4 -0
- package/dist/workspace/conformance/__fixtures__/reference-broker.js +205 -0
- package/dist/workspace/conformance/__writer_fixture__/answers/answer.kind.mjs +32 -0
- package/dist/workspace/conformance/__writer_fixture__/answers/answer.schema.json +135 -0
- package/dist/workspace/conformance/__writer_fixture__/contracts/contract.kind.mjs +37 -0
- package/dist/workspace/conformance/__writer_fixture__/contracts/contract.schema.json +213 -0
- package/dist/workspace/conformance/__writer_fixture__/decisions/points.json +228 -0
- package/dist/workspace/conformance/__writer_fixture__/drivers/driver.kind.mjs +15 -0
- package/dist/workspace/conformance/__writer_fixture__/drivers/driver.schema.json +38 -0
- package/dist/workspace/conformance/__writer_fixture__/sessions/session.kind.mjs +54 -0
- package/dist/workspace/conformance/__writer_fixture__/sessions/session.schema.json +289 -0
- package/dist/workspace/conformance/__writer_fixture__/work/W-001-the-writer-suite-works-this-item.md +25 -0
- package/dist/workspace/conformance/__writer_fixture__/work/work.kind.mjs +60 -0
- package/dist/workspace/conformance/__writer_fixture__/work/work.schema.json +603 -0
- package/dist/workspace/conformance/broker.js +736 -0
- package/dist/workspace/conformance/index.js +673 -0
- package/dist/workspace/conformance/vitest.js +108 -0
- package/dist/workspace/conformance/writer.js +1080 -0
- package/dist/workspace/decide.js +879 -0
- package/dist/workspace/decision-points.schema.json +135 -135
- package/dist/workspace/declaration-write-cli.js +74 -0
- package/dist/workspace/declaration-write.js +238 -0
- package/dist/workspace/declaration.js +938 -0
- package/dist/workspace/declaration.schema.json +1413 -1413
- package/dist/workspace/declared-kinds.js +106 -0
- package/dist/workspace/environments.js +164 -0
- package/dist/workspace/evidence.schema.json +279 -0
- package/dist/workspace/export-cli.js +185 -0
- package/dist/workspace/export.js +499 -0
- package/dist/workspace/factory-rules.js +175 -0
- package/dist/workspace/generated-files.js +92 -0
- package/dist/workspace/glob.js +45 -0
- package/dist/workspace/graph-cache.js +352 -0
- package/dist/workspace/graph-cli.js +348 -0
- package/dist/workspace/graph.schema.json +1338 -0
- package/dist/workspace/identity-gate-rule.js +75 -0
- package/dist/workspace/identity.js +126 -0
- package/dist/workspace/import.js +283 -0
- package/dist/workspace/init.js +620 -0
- package/dist/workspace/intent-cli.js +263 -0
- package/dist/workspace/intent-joins.js +201 -0
- package/dist/workspace/intent-record.js +352 -0
- package/dist/workspace/intent-record.schema.json +864 -0
- package/dist/workspace/intent.js +1582 -0
- package/dist/workspace/intent.schema.json +2420 -0
- package/dist/workspace/json-edit.js +221 -0
- package/dist/workspace/jsonc.js +235 -0
- package/dist/workspace/kind-readers.js +103 -0
- package/dist/workspace/kinds.js +587 -0
- package/dist/workspace/lexicon-symbols.js +112 -0
- package/dist/workspace/lineage-adopt-cli.js +102 -0
- package/dist/workspace/lineage-adopt.js +360 -0
- package/dist/workspace/lineage-check.js +282 -0
- package/dist/workspace/lineage-cli.js +121 -0
- package/dist/workspace/lineage-hash-index.js +284 -0
- package/dist/workspace/lineage-init.js +507 -0
- package/dist/workspace/lineage-lock.js +387 -0
- package/dist/workspace/lineage-merge.js +45 -0
- package/dist/workspace/lineage-migrations.d.ts.map +1 -1
- package/dist/workspace/lineage-migrations.js +329 -0
- package/dist/workspace/lineage-provenance.js +36 -0
- package/dist/workspace/lineage-update.js +138 -0
- package/dist/workspace/lineage-upgrade-cli.js +131 -0
- package/dist/workspace/lineage-upgrade.js +889 -0
- package/dist/workspace/lineage-vendor.js +171 -0
- package/dist/workspace/lineage-version.js +128 -0
- package/dist/workspace/lineage-versions.js +275 -0
- package/dist/workspace/links.js +330 -0
- package/dist/workspace/ls-generated.js +95 -0
- package/dist/workspace/ls.js +277 -0
- package/dist/workspace/ls.schema.json +333 -0
- package/dist/workspace/member-commands.js +508 -0
- package/dist/workspace/member-handles.js +216 -0
- package/dist/workspace/member-pipeline.js +258 -0
- package/dist/workspace/member-run.js +117 -0
- package/dist/workspace/member-write.schema.json +193 -0
- package/dist/workspace/nested-graph.js +156 -0
- package/dist/workspace/nesting.js +38 -0
- package/dist/workspace/patch.js +301 -0
- package/dist/workspace/patch.schema.json +321 -0
- package/dist/workspace/pin-cli.js +55 -0
- package/dist/workspace/pin-integrity.js +100 -0
- package/dist/workspace/plan-summary.schema.json +155 -0
- package/dist/workspace/point-answer.schema.json +129 -129
- package/dist/workspace/points-cli.js +291 -0
- package/dist/workspace/points-write.schema.json +660 -0
- package/dist/workspace/points.js +521 -0
- package/dist/workspace/points.schema.json +916 -0
- package/dist/workspace/pr-report.schema.json +108 -0
- package/dist/workspace/principal-classes.js +245 -0
- package/dist/workspace/reason-codes.js +326 -0
- package/dist/workspace/record-assets.js +151 -0
- package/dist/workspace/record-decided.js +105 -0
- package/dist/workspace/record-sessions.js +96 -0
- package/dist/workspace/record-source.js +114 -0
- package/dist/workspace/records-amend.schema.json +284 -0
- package/dist/workspace/records-checkout.js +76 -0
- package/dist/workspace/records-cli.js +707 -0
- package/dist/workspace/records-close.js +116 -0
- package/dist/workspace/records-close.schema.json +278 -0
- package/dist/workspace/records-new.schema.json +202 -0
- package/dist/workspace/records-review.schema.json +367 -0
- package/dist/workspace/records-since.js +317 -0
- package/dist/workspace/records-since.schema.json +218 -0
- package/dist/workspace/records-write.js +1229 -0
- package/dist/workspace/records.js +1345 -0
- package/dist/workspace/records.schema.json +604 -0
- package/dist/workspace/reference-kinds/app/package.json +10 -0
- package/dist/workspace/reference-kinds/app/workspace-kinds.json +45 -0
- package/dist/workspace/returns.js +303 -0
- package/dist/workspace/root-refusal.js +75 -0
- package/dist/workspace/run-statement.schema.json +622 -0
- package/dist/workspace/runs-cli.js +584 -0
- package/dist/workspace/runs-write.schema.json +994 -0
- package/dist/workspace/runs.js +708 -0
- package/dist/workspace/runs.schema.json +1257 -0
- package/dist/workspace/runtimes.js +76 -0
- package/dist/workspace/session-kinds.js +83 -0
- package/dist/workspace/signers.schema.json +206 -0
- package/dist/workspace/source-block.js +125 -0
- package/dist/workspace/squash.js +177 -0
- package/dist/workspace/status-gates.js +162 -0
- package/dist/workspace/status-stewards.js +330 -0
- package/dist/workspace/status.js +514 -0
- package/dist/workspace/status.schema.json +998 -0
- package/dist/workspace/steward-index.js +191 -0
- package/dist/workspace/symbols.js +188 -0
- package/dist/workspace/template-manifest.js +227 -0
- package/dist/workspace/template-pins.js +115 -0
- package/dist/workspace/trailer-joins.js +65 -0
- package/dist/workspace/trailers.js +111 -0
- package/dist/workspace/tree.js +136 -0
- package/dist/workspace/trust/attestor.js +63 -0
- package/dist/workspace/trust/dsse.js +108 -0
- package/dist/workspace/trust/evidence-cli.js +175 -0
- package/dist/workspace/trust/evidence.js +161 -0
- package/dist/workspace/trust/gate-seal.js +226 -0
- package/dist/workspace/trust/policy.js +190 -0
- package/dist/workspace/trust/provenance.js +180 -0
- package/dist/workspace/trust/rotation.js +298 -0
- package/dist/workspace/trust/run-commits.js +101 -0
- package/dist/workspace/trust/run-statement.js +182 -0
- package/dist/workspace/trust/seal.js +272 -0
- package/dist/workspace/trust/signers-cli.js +152 -0
- package/dist/workspace/trust/ssh-commit.js +178 -0
- package/dist/workspace/trust/test-repo.js +118 -0
- package/dist/workspace/trust/trust-files.js +193 -0
- package/dist/workspace/trust/verify-cli.js +63 -0
- package/dist/workspace/trust/verify.js +192 -0
- package/dist/workspace/which-chant.js +141 -0
- package/dist/workspace/wip-cli.js +95 -0
- package/dist/workspace/wip-write.schema.json +504 -0
- package/dist/workspace/wip.js +631 -0
- package/dist/workspace/wip.schema.json +334 -0
- package/dist/workspace/work-cli.js +448 -0
- package/dist/workspace/work-evidence.js +122 -0
- package/dist/workspace/work-evidence.schema.json +222 -0
- package/dist/workspace/work-history.schema.json +421 -0
- package/dist/workspace/work-lease.schema.json +246 -0
- package/dist/workspace/work-size.js +77 -0
- package/dist/workspace/work.js +283 -0
- package/dist/workspace/workspace-kinds.schema.json +259 -259
- package/dist/workspace/workspace-principals.schema.json +53 -53
- package/dist/workspace/write-lock-cli.js +105 -0
- package/dist/workspace/write-lock.js +424 -0
- package/dist/workspace/write-lock.schema.json +172 -0
- package/dist/workspace/write-scope.js +484 -0
- package/dist/yaml.js +459 -0
- package/package.json +2 -2
- package/src/cli/commands/check-lexicon-plugin.ts +2 -0
- package/src/cli/commands/lint.ts +3 -0
- package/src/cli/commands/migrate.ts +2 -0
- package/src/cli/compiled-cli.e2e.test.ts +165 -0
- package/src/cli/main.ts +121 -27
- package/src/cli/mcp/workspace-tools.ts +1 -1
- package/src/codegen/pinned-upgrade.ts +2 -0
- package/src/components/discover.ts +2 -0
- package/src/components/sandbox/driver.ts +3 -1
- package/src/config-import.ts +3 -0
- package/src/discovery/fold-import.ts +23 -14
- package/src/discovery/import.ts +2 -0
- package/src/discovery/sandbox/driver.ts +3 -1
- package/src/lexicon-module.ts +3 -0
- package/src/lint/config.ts +1 -1
- package/src/lint/discover.ts +28 -8
- package/src/lint/policy-import.ts +4 -1
- package/src/lint/rule-loader.ts +2 -0
- package/src/op/discover.ts +3 -0
- package/src/ts-loader.test.ts +23 -0
- package/src/ts-loader.ts +122 -0
- package/src/workspace/chant-migrations/chud-lexicon-exit.ts +1 -1
- package/src/workspace/declaration.ts +1 -1
- package/src/workspace/kinds.ts +1 -1
- package/src/workspace/lineage-migrations.ts +2 -0
- package/src/workspace/points.ts +1 -1
- package/src/workspace/principal-classes.ts +1 -1
|
@@ -0,0 +1,1727 @@
|
|
|
1
|
+
import { lexiconNames } from "../../lexicon-module.js";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { commandBuildParams } from "../build-params-cli.js";
|
|
4
|
+
import { build } from "../../build.js";
|
|
5
|
+
import { takeSnapshot } from "../../lifecycle/snapshot.js";
|
|
6
|
+
import { readSnapshot, readSnapshotAt, readEnvironmentSnapshots, listSnapshots, fetchLifecycle, pushLifecycle, snapshotStorageKey, StaleLifecycleBranchError } from "../../lifecycle/git.js";
|
|
7
|
+
import { deepDiffForLexicon } from "../../lifecycle/deep-observe.js";
|
|
8
|
+
import { countUnclaimed, countPending, countPropertyDrift, countHeld, suspiciousHeld } from "../../lifecycle/deep-diff.js";
|
|
9
|
+
import { describePathOrigin, getPathProvenance, getProvenance } from "../../provenance.js";
|
|
10
|
+
import { resolveDeepDrift, resolveDriftedField } from "../../fold-provenance.js";
|
|
11
|
+
import { acceptDeviations, baselineForLexicon, emptyBaseline, readObservationBaseline, writeObservationBaseline, OBSERVATION_BASELINE_FILE, } from "../../lifecycle/observation-baseline.js";
|
|
12
|
+
import { computeBuildDigest, declaredDefinitions, diffDigests } from "../../lifecycle/digest.js";
|
|
13
|
+
import { diffLive, diffLiveArtifacts, diffSnapshots, missingRow } from "../../lifecycle/live-diff.js";
|
|
14
|
+
import { buildChangeSet, renderChangeSet, renderChangeSetMarkdown, gitlabMrReport, unobservedPlanNotice } from "../../lifecycle/change-set.js";
|
|
15
|
+
import { mergeReceiptEntries, observedValueResolver, planReceipts, readReceiptValue } from "../../lifecycle/receipt-plan.js";
|
|
16
|
+
import { annotateDisruption, disruptionNotices } from "../../lifecycle/disruption.js";
|
|
17
|
+
import { collectEffectReceipts, isEffectReceipt } from "../../effect-receipt.js";
|
|
18
|
+
import { formatUnobserved, mergeObservations, normalizeObservation, unobservedAll, } from "../../observation.js";
|
|
19
|
+
import { describeIdentities, identityStatusText } from "../../identity.js";
|
|
20
|
+
import { discoverComponents } from "../../components/discover.js";
|
|
21
|
+
import { cfnDeployStacks } from "./components.js";
|
|
22
|
+
import { affectedStacks } from "../../lifecycle/affected.js";
|
|
23
|
+
import { rollbackToRevision } from "../../lifecycle/rollback.js";
|
|
24
|
+
import { loadChantConfig, environmentNames, matchesDeclaredEnvironment, resolveOwnershipStack } from "../../config.js";
|
|
25
|
+
import { unknownEnvError, isProdLikeEnvironment } from "../../env.js";
|
|
26
|
+
import { planTeardown, executeTeardown } from "../../lifecycle/teardown.js";
|
|
27
|
+
import { collectBuildRootContributors } from "../plugins.js";
|
|
28
|
+
import { resolveBuildModes, resolveProjectBuildOptions } from "../build-options.js";
|
|
29
|
+
import { applyLiveEndpoint } from "../../live-endpoint.js";
|
|
30
|
+
import { isObservableDeclarable } from "../../declarable.js";
|
|
31
|
+
import { formatError, formatWarning, formatSuccess, formatBold } from "../format.js";
|
|
32
|
+
/**
|
|
33
|
+
* Resolve the build root for a lifecycle command. The project root (where
|
|
34
|
+
* chant.config.ts lives) is always ".", but the *build* can be scoped to a
|
|
35
|
+
* subdirectory so a mixed-layout project — chant `src/` next to app code with
|
|
36
|
+
* import side effects — only synthesizes its infra. Precedence: `--src` flag,
|
|
37
|
+
* then `config.sourceDir`, then "." (the root). Snapshot/diff/plan all use this
|
|
38
|
+
* so their build digests stay consistent.
|
|
39
|
+
*/
|
|
40
|
+
export function resolveBuildRoot(args, config) {
|
|
41
|
+
return resolve(args.src ?? config.sourceDir ?? ".");
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* The stacks a lifecycle command (`snapshot`/`diff`) iterates. A multi-stack
|
|
45
|
+
* project declares them via `stacks` in chant.config (#932) — each built from
|
|
46
|
+
* its own `src` and observed against its own live stack `name`. A single-stack
|
|
47
|
+
* project (no `stacks`) resolves to one target built from `sourceDir`/root and
|
|
48
|
+
* observed as the stack named after the environment (unchanged behavior). An
|
|
49
|
+
* explicit `--src` always wins and forces a single scoped target.
|
|
50
|
+
*/
|
|
51
|
+
function resolveStackTargets(args, config) {
|
|
52
|
+
if (args.src)
|
|
53
|
+
return [{ root: resolve(args.src) }];
|
|
54
|
+
if (config.stacks && config.stacks.length > 0) {
|
|
55
|
+
return config.stacks.map((s) => ({ stack: s.name, root: resolve(s.src), region: s.region }));
|
|
56
|
+
}
|
|
57
|
+
return [{ root: resolveBuildRoot(args, config) }];
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* chant lifecycle snapshot <environment> [lexicon]
|
|
61
|
+
*/
|
|
62
|
+
export async function runLifecycleSnapshot(ctx) {
|
|
63
|
+
const { args, plugins } = ctx;
|
|
64
|
+
const environment = args.extraPositional;
|
|
65
|
+
const lexiconFilter = args.extraPositional2;
|
|
66
|
+
if (!environment) {
|
|
67
|
+
console.error(formatError({ message: "Environment is required: chant lifecycle snapshot <environment> [lexicon]" }));
|
|
68
|
+
return 1;
|
|
69
|
+
}
|
|
70
|
+
// Validate environment against config
|
|
71
|
+
const projectPath = resolve(".");
|
|
72
|
+
const { config } = await loadChantConfig(projectPath);
|
|
73
|
+
// This invocation's parameters, so the declared side of the comparison is the estate
|
|
74
|
+
// the caller asked for rather than the parameter defaults (#1483).
|
|
75
|
+
const declaredParams = await commandBuildParams(config.buildParams, args);
|
|
76
|
+
if (!declaredParams)
|
|
77
|
+
return 1;
|
|
78
|
+
const declaredEnvNames = environmentNames(config.environments);
|
|
79
|
+
if (declaredEnvNames && !matchesDeclaredEnvironment(config.environments, environment)) {
|
|
80
|
+
console.error(formatError({
|
|
81
|
+
message: `Unknown environment "${environment}"`,
|
|
82
|
+
hint: `Defined environments: ${declaredEnvNames.join(", ")}`,
|
|
83
|
+
}));
|
|
84
|
+
return 1;
|
|
85
|
+
}
|
|
86
|
+
// Filter plugins if lexicon specified
|
|
87
|
+
const targetPlugins = lexiconFilter
|
|
88
|
+
? plugins.filter((p) => p.name === lexiconFilter)
|
|
89
|
+
: plugins;
|
|
90
|
+
const targetSerializers = targetPlugins.map((p) => p.serializer);
|
|
91
|
+
const observingPlugins = targetPlugins.filter((p) => p.describeResources || p.listArtifacts);
|
|
92
|
+
if (observingPlugins.length === 0) {
|
|
93
|
+
console.error(formatError({
|
|
94
|
+
message: "No plugins implement describeResources or listArtifacts",
|
|
95
|
+
hint: lexiconFilter ? `Lexicon "${lexiconFilter}" does not support state snapshots` : undefined,
|
|
96
|
+
}));
|
|
97
|
+
return 1;
|
|
98
|
+
}
|
|
99
|
+
// One target per stack (single-stack projects: exactly one). Each stack builds
|
|
100
|
+
// from its own scoped root — so its logical ids match what it deploys — and
|
|
101
|
+
// snapshots against its own live stack name (#932).
|
|
102
|
+
const targets = resolveStackTargets(args, config);
|
|
103
|
+
let anySnapshotSaved = false;
|
|
104
|
+
let anyHardError = false;
|
|
105
|
+
// #1166 — same self-sufficiency as `chant graph --live`: a snapshot is
|
|
106
|
+
// always a live read, so an environment's declared endpoint applies here
|
|
107
|
+
// too, unless the ambient shell already set it.
|
|
108
|
+
const endpointResult = applyLiveEndpoint(config.environments, environment, observingPlugins);
|
|
109
|
+
if (endpointResult.notice)
|
|
110
|
+
console.error(formatWarning({ message: endpointResult.notice }));
|
|
111
|
+
// Build every stack first, so the ambient scan (#1278) can be bounded by the
|
|
112
|
+
// kinds the PROJECT manages rather than the ones this stack happens to
|
|
113
|
+
// declare. "Which of my security groups are unused" is a question about the
|
|
114
|
+
// estate; a region whose stack declares no security group still has a default
|
|
115
|
+
// one, and scoping the bound per stack silently drops it.
|
|
116
|
+
const built = [];
|
|
117
|
+
const buildRoots = collectBuildRootContributors(targetPlugins, config, projectPath);
|
|
118
|
+
for (const target of targets) {
|
|
119
|
+
const label = target.stack ? `stack "${target.stack}"` : "project";
|
|
120
|
+
const buildResult = await build(target.root, targetSerializers, undefined, { buildParams: declaredParams, buildRoots });
|
|
121
|
+
if (buildResult.errors.length > 0) {
|
|
122
|
+
console.error(formatError({ message: `Build failed for ${label} — fix errors before taking a snapshot` }));
|
|
123
|
+
anyHardError = true;
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
built.push({ target, buildResult });
|
|
127
|
+
}
|
|
128
|
+
const projectKinds = [
|
|
129
|
+
...new Set(built.flatMap(({ buildResult }) => [...buildResult.entities.values()].map((e) => e.entityType))),
|
|
130
|
+
];
|
|
131
|
+
try {
|
|
132
|
+
for (const { target, buildResult } of built) {
|
|
133
|
+
const label = target.stack ? `stack "${target.stack}"` : "project";
|
|
134
|
+
let result;
|
|
135
|
+
try {
|
|
136
|
+
result = await takeSnapshot(environment, observingPlugins, buildResult, {
|
|
137
|
+
stack: target.stack,
|
|
138
|
+
region: target.region,
|
|
139
|
+
deep: args.deep,
|
|
140
|
+
ambient: args.ambient,
|
|
141
|
+
ambientKinds: projectKinds,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
catch (err) {
|
|
145
|
+
if (err instanceof StaleLifecycleBranchError) {
|
|
146
|
+
console.error(formatError({
|
|
147
|
+
message: `Another snapshot completed for chant/lifecycle after this run started (env: ${environment}).`,
|
|
148
|
+
hint: `Pull and retry: \`git fetch origin ${"chant/lifecycle"}:${"chant/lifecycle"}\` && \`chant lifecycle snapshot ${environment}\`.`,
|
|
149
|
+
}));
|
|
150
|
+
return 1;
|
|
151
|
+
}
|
|
152
|
+
throw err;
|
|
153
|
+
}
|
|
154
|
+
for (const w of result.warnings) {
|
|
155
|
+
console.error(formatWarning({ message: w }));
|
|
156
|
+
}
|
|
157
|
+
for (const e of result.errors) {
|
|
158
|
+
console.error(formatError({ message: e }));
|
|
159
|
+
}
|
|
160
|
+
if (result.snapshots.length > 0) {
|
|
161
|
+
anySnapshotSaved = true;
|
|
162
|
+
const prefix = target.stack ? `${target.stack}: ` : "";
|
|
163
|
+
const counts = result.snapshots
|
|
164
|
+
.map((s) => `${s.lexicon}(${Object.keys(s.resources).length})`)
|
|
165
|
+
.join(" ");
|
|
166
|
+
console.error(formatSuccess(`${prefix}Snapshot saved to chant/lifecycle (${counts})`));
|
|
167
|
+
}
|
|
168
|
+
if (result.errors.length > 0)
|
|
169
|
+
anyHardError = true;
|
|
170
|
+
}
|
|
171
|
+
return anyHardError && !anySnapshotSaved ? 1 : 0;
|
|
172
|
+
}
|
|
173
|
+
finally {
|
|
174
|
+
endpointResult.restore();
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* chant lifecycle show <environment> [lexicon]
|
|
179
|
+
*/
|
|
180
|
+
export async function runLifecycleShow(ctx) {
|
|
181
|
+
const environment = ctx.args.extraPositional;
|
|
182
|
+
const lexiconFilter = ctx.args.extraPositional2;
|
|
183
|
+
if (!environment) {
|
|
184
|
+
console.error(formatError({ message: "Environment is required: chant lifecycle show <environment> [lexicon]" }));
|
|
185
|
+
return 1;
|
|
186
|
+
}
|
|
187
|
+
// Fetch from remote first
|
|
188
|
+
await fetchLifecycle();
|
|
189
|
+
if (lexiconFilter) {
|
|
190
|
+
const content = await readSnapshot(environment, lexiconFilter);
|
|
191
|
+
if (!content) {
|
|
192
|
+
console.error(formatError({ message: `No snapshot found for ${environment}/${lexiconFilter}` }));
|
|
193
|
+
return 1;
|
|
194
|
+
}
|
|
195
|
+
const snapshot = JSON.parse(content);
|
|
196
|
+
printSnapshotTable(snapshot);
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
const snapshots = await readEnvironmentSnapshots(environment);
|
|
200
|
+
if (snapshots.size === 0) {
|
|
201
|
+
console.error(formatError({ message: `No snapshots found for environment "${environment}"` }));
|
|
202
|
+
return 1;
|
|
203
|
+
}
|
|
204
|
+
for (const [lexicon, content] of snapshots) {
|
|
205
|
+
const snapshot = JSON.parse(content);
|
|
206
|
+
// Depth is stated, not inferred (#1267). An identity snapshot cannot
|
|
207
|
+
// answer a property question, and a reader that assumes otherwise reads
|
|
208
|
+
// "no properties recorded" as "no such properties".
|
|
209
|
+
const depth = snapshot.depth ?? "identity";
|
|
210
|
+
const depthNote = depth === "deep"
|
|
211
|
+
? ` — deep (${Object.keys(snapshot.properties ?? {}).length} property trees)`
|
|
212
|
+
: " — identity only";
|
|
213
|
+
console.log(`\n${formatBold(`${environment}/${lexicon}`)} — ${Object.keys(snapshot.resources).length} resources${depthNote} — ${snapshot.timestamp}`);
|
|
214
|
+
printSnapshotTable(snapshot);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return 0;
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* chant lifecycle rollback [<environment>] --to <ref>
|
|
221
|
+
*
|
|
222
|
+
* Open a PR restoring `sourceDir` to a prior git revision (#873). Source-level,
|
|
223
|
+
* reviewable, no cloud mutation — a human merges, then a gated apply rolls the
|
|
224
|
+
* env back. The `<env>` positional is PR context.
|
|
225
|
+
*/
|
|
226
|
+
export async function runLifecycleRollback(ctx) {
|
|
227
|
+
const { args } = ctx;
|
|
228
|
+
const environment = args.extraPositional; // optional — PR context/title
|
|
229
|
+
const ref = args.migrateTo; // the --to value
|
|
230
|
+
if (!ref) {
|
|
231
|
+
console.error(formatError({ message: "Target revision required: chant lifecycle rollback [<env>] --to <ref>" }));
|
|
232
|
+
return 1;
|
|
233
|
+
}
|
|
234
|
+
const { config } = await loadChantConfig(resolve("."));
|
|
235
|
+
const sourceDir = config.sourceDir ?? ".";
|
|
236
|
+
try {
|
|
237
|
+
const result = await rollbackToRevision({ ref, env: environment, sourceDir, cwd: resolve("."), dryRun: args.dryRun });
|
|
238
|
+
if (result.noop) {
|
|
239
|
+
console.error(formatSuccess(`${sourceDir} already matches ${ref} — nothing to roll back`));
|
|
240
|
+
return 0;
|
|
241
|
+
}
|
|
242
|
+
if (args.dryRun) {
|
|
243
|
+
// The delta on stdout, so it pipes and diffs like any other patch; the
|
|
244
|
+
// summary on stderr, matching the PR path's split.
|
|
245
|
+
process.stdout.write(result.diff ?? "");
|
|
246
|
+
console.error(formatSuccess(`Rollback delta for ${ref} computed — no PR opened, nothing pushed`));
|
|
247
|
+
return 0;
|
|
248
|
+
}
|
|
249
|
+
console.log(result.prUrl); // the PR URL — the consumer (behold) reads this from stdout
|
|
250
|
+
console.error(formatSuccess(`Opened rollback PR on ${result.branch}`));
|
|
251
|
+
return 0;
|
|
252
|
+
}
|
|
253
|
+
catch (err) {
|
|
254
|
+
console.error(formatError({ message: `rollback failed — ${err instanceof Error ? err.message : String(err)}` }));
|
|
255
|
+
return 1;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
/**
|
|
259
|
+
* chant lifecycle diff <environment> [lexicon]
|
|
260
|
+
*/
|
|
261
|
+
export async function runLifecycleDiff(ctx) {
|
|
262
|
+
const { args, plugins, serializers } = ctx;
|
|
263
|
+
const environment = args.extraPositional;
|
|
264
|
+
const lexiconFilter = args.extraPositional2;
|
|
265
|
+
if (!environment) {
|
|
266
|
+
console.error(formatError({ message: "Environment is required: chant lifecycle diff <environment> [lexicon]" }));
|
|
267
|
+
return 1;
|
|
268
|
+
}
|
|
269
|
+
// `--between <refA> <refB>` (#822): diff two saved snapshots against each other.
|
|
270
|
+
// Purely historical — no build, no cloud query; reads the orphan branch.
|
|
271
|
+
if (args.betweenA && args.betweenB) {
|
|
272
|
+
return runLifecycleDiffBetween({
|
|
273
|
+
environment,
|
|
274
|
+
lexiconFilter,
|
|
275
|
+
refA: args.betweenA,
|
|
276
|
+
refB: args.betweenB,
|
|
277
|
+
json: !!args.json,
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
// Filter serializers to target lexicon before building
|
|
281
|
+
const targetSerializers = lexiconFilter
|
|
282
|
+
? plugins.filter((p) => p.name === lexiconFilter).map((p) => p.serializer)
|
|
283
|
+
: serializers;
|
|
284
|
+
// Fetch previous snapshots once (all stacks share the orphan branch).
|
|
285
|
+
const { config } = await loadChantConfig(resolve("."));
|
|
286
|
+
// This invocation's parameters, so the declared side of the comparison is the estate
|
|
287
|
+
// the caller asked for rather than the parameter defaults (#1483).
|
|
288
|
+
const declaredParams = await commandBuildParams(config.buildParams, args);
|
|
289
|
+
if (!declaredParams)
|
|
290
|
+
return 1;
|
|
291
|
+
await fetchLifecycle();
|
|
292
|
+
// One target per stack (single-stack projects: exactly one), each built from
|
|
293
|
+
// its own scoped root and diffed against its own live stack name (#932).
|
|
294
|
+
const targets = resolveStackTargets(args, config);
|
|
295
|
+
const json = !!args.json;
|
|
296
|
+
const perStackJson = {};
|
|
297
|
+
let combinedLexiconsJson;
|
|
298
|
+
let totalDrift = 0;
|
|
299
|
+
let totalUnobserved = 0;
|
|
300
|
+
let totalChecked = 0;
|
|
301
|
+
let anyBuildError = false;
|
|
302
|
+
// Accepted-deviation baseline (#1014). Read once for the whole run — it is
|
|
303
|
+
// env-keyed, not stack-keyed, and every deep pass subtracts from the same
|
|
304
|
+
// committed set. Absent is the normal state (nothing accepted yet).
|
|
305
|
+
const baseline = args.live ? await readObservationBaseline(environment) : null;
|
|
306
|
+
const accepted = {};
|
|
307
|
+
// Effect receipts across every target's build (#1833) — `--update-baseline`
|
|
308
|
+
// refuses to accept drift on them (the effect step is their only writer),
|
|
309
|
+
// and recognition is marker-based so materialized lexicon rows are caught.
|
|
310
|
+
const receiptEntities = new Set();
|
|
311
|
+
// #1166 — an environment can declare its own endpoint (a local emulator like
|
|
312
|
+
// Floci), so `--live` is self-sufficient even when the ambient shell never
|
|
313
|
+
// exported e.g. AWS_ENDPOINT_URL. Ambient always wins when it's already set.
|
|
314
|
+
// Scoped to just the live reads below — restored in `finally`.
|
|
315
|
+
const liveLexicons = args.live ? plugins.filter((p) => p.describeResources || p.listArtifacts) : [];
|
|
316
|
+
const endpointResult = applyLiveEndpoint(config.environments, environment, liveLexicons);
|
|
317
|
+
if (endpointResult.notice)
|
|
318
|
+
console.error(formatWarning({ message: endpointResult.notice }));
|
|
319
|
+
// The build `chant build` would produce for this project (#2002's one
|
|
320
|
+
// place), so the diff folds by default as `chant build` does. A run build
|
|
321
|
+
// cannot say which composite argument produced a field, so without this
|
|
322
|
+
// every composite field in the live diff read as unknown (#3597).
|
|
323
|
+
const projectRoot = resolve(".");
|
|
324
|
+
const diffBuildOptions = resolveProjectBuildOptions({
|
|
325
|
+
config,
|
|
326
|
+
configDir: projectRoot,
|
|
327
|
+
plugins,
|
|
328
|
+
modes: resolveBuildModes(config, { fold: args.fold, sandbox: args.sandbox }),
|
|
329
|
+
buildParams: declaredParams,
|
|
330
|
+
});
|
|
331
|
+
try {
|
|
332
|
+
for (const target of targets) {
|
|
333
|
+
const buildResult = await build(target.root, targetSerializers, undefined, diffBuildOptions);
|
|
334
|
+
if (buildResult.errors.length > 0) {
|
|
335
|
+
const label = target.stack ? `stack "${target.stack}"` : "project";
|
|
336
|
+
console.error(formatError({ message: `Build failed for ${label} — fix errors before diffing` }));
|
|
337
|
+
anyBuildError = true;
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
// The lexicons the diff walks. Keyed on the BUILT manifest — which is
|
|
341
|
+
// right for the resource axis (a lexicon with no declared entities has
|
|
342
|
+
// nothing to diff) but drops the artifact axis whole: artifacts are
|
|
343
|
+
// context-keyed with no declared side at all (that is their defining
|
|
344
|
+
// property), so an estate that uses helm purely as component deploy
|
|
345
|
+
// steps builds zero helm entities and its releases were never listed —
|
|
346
|
+
// `observedArtifacts` silently absent for exactly the estates behold#146
|
|
347
|
+
// exists for (found live on kubemicrovm-ops, four releases invisible).
|
|
348
|
+
// On the live path, artifact-capable configured lexicons join the walk;
|
|
349
|
+
// their resource half stays gated on `describeResources` + declared
|
|
350
|
+
// entities as before.
|
|
351
|
+
const builtLexicons = Array.from(buildResult.manifest.lexicons);
|
|
352
|
+
const artifactLexicons = args.live ? plugins.filter((p) => p.listArtifacts).map((p) => p.name) : [];
|
|
353
|
+
const lexicons = lexiconFilter
|
|
354
|
+
? [lexiconFilter]
|
|
355
|
+
: [...new Set([...builtLexicons, ...artifactLexicons])];
|
|
356
|
+
if (args.live) {
|
|
357
|
+
// Multi-stack component projects: observe each component's own cfn stack
|
|
358
|
+
// and union (same fix runGraphLive / plan use), so a deployed resource
|
|
359
|
+
// isn't reported "missing". Only when this target has no explicit stack.
|
|
360
|
+
let componentStacks = [];
|
|
361
|
+
if (!target.stack) {
|
|
362
|
+
try {
|
|
363
|
+
const disc = await discoverComponents(target.root, { sandbox: args.sandbox });
|
|
364
|
+
const set = new Set();
|
|
365
|
+
for (const { component } of disc.components.values())
|
|
366
|
+
for (const s of cfnDeployStacks(component.deploy))
|
|
367
|
+
set.add(s);
|
|
368
|
+
componentStacks = [...set];
|
|
369
|
+
}
|
|
370
|
+
catch {
|
|
371
|
+
// no components / discovery failed → single-stack observe path
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
const r = await runLifecycleDiffLive({
|
|
375
|
+
environment,
|
|
376
|
+
lexicons,
|
|
377
|
+
plugins,
|
|
378
|
+
buildResult,
|
|
379
|
+
json,
|
|
380
|
+
stack: target.stack,
|
|
381
|
+
region: target.region,
|
|
382
|
+
componentStacks,
|
|
383
|
+
baseline,
|
|
384
|
+
updateBaseline: args.updateBaseline,
|
|
385
|
+
...(args.namespace ? { namespace: args.namespace } : {}),
|
|
386
|
+
projectRoot,
|
|
387
|
+
});
|
|
388
|
+
totalDrift += r.totalDrift;
|
|
389
|
+
totalUnobserved += r.totalUnobserved;
|
|
390
|
+
totalChecked += r.totalLexiconsChecked;
|
|
391
|
+
for (const [lexicon, deviations] of Object.entries(r.toAccept)) {
|
|
392
|
+
(accepted[lexicon] ?? (accepted[lexicon] = [])).push(...deviations);
|
|
393
|
+
}
|
|
394
|
+
if (args.updateBaseline) {
|
|
395
|
+
for (const name of collectEffectReceipts(buildResult.entities).keys())
|
|
396
|
+
receiptEntities.add(name);
|
|
397
|
+
}
|
|
398
|
+
if (json) {
|
|
399
|
+
if (target.stack)
|
|
400
|
+
perStackJson[target.stack] = r.byLexicon;
|
|
401
|
+
else
|
|
402
|
+
combinedLexiconsJson = r.byLexicon;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
else {
|
|
406
|
+
await runLifecycleDiffDigest({ environment, lexicons, buildResult, stack: target.stack });
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
// `--update-baseline` (#1014): record what the deep pass just reported as
|
|
410
|
+
// accepted, so it stops re-alerting. Runs before the summary lines so the
|
|
411
|
+
// "no drift" verdict below still describes the run that produced it.
|
|
412
|
+
if (args.live && args.updateBaseline) {
|
|
413
|
+
await recordAcceptedBaseline(environment, baseline, accepted, json, receiptEntities);
|
|
414
|
+
}
|
|
415
|
+
if (args.live) {
|
|
416
|
+
if (json) {
|
|
417
|
+
// Single-stack keeps the original `{ environment, lexicons }` shape
|
|
418
|
+
// (behold's inspect diff, #852); multi-stack nests under `stacks`.
|
|
419
|
+
console.log(JSON.stringify(combinedLexiconsJson !== undefined
|
|
420
|
+
? { environment, lexicons: combinedLexiconsJson }
|
|
421
|
+
: { environment, stacks: perStackJson }));
|
|
422
|
+
}
|
|
423
|
+
else if (totalChecked === 0) {
|
|
424
|
+
console.error(formatWarning({
|
|
425
|
+
message: "No lexicons implement describeResources or listArtifacts — nothing to diff in --live mode",
|
|
426
|
+
}));
|
|
427
|
+
return 1;
|
|
428
|
+
}
|
|
429
|
+
else if (totalDrift === 0) {
|
|
430
|
+
// Qualify the all-clear when part of the estate was never read (#1089):
|
|
431
|
+
// "no drift" over an incomplete observation is not the same claim.
|
|
432
|
+
console.error(totalUnobserved > 0
|
|
433
|
+
? formatWarning({
|
|
434
|
+
message: `No drift detected across ${totalChecked} lexicon(s), but ${totalUnobserved} declared entity(ies) could not be observed — that part of the estate is unknown, not clean`,
|
|
435
|
+
})
|
|
436
|
+
: formatSuccess(`No drift detected across ${totalChecked} lexicon(s)`));
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
return anyBuildError ? 1 : 0;
|
|
440
|
+
}
|
|
441
|
+
finally {
|
|
442
|
+
endpointResult.restore();
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
/**
|
|
446
|
+
* Write the accepted-deviation baseline (#1014) for everything the deep pass
|
|
447
|
+
* reported this run, and push it on the same orphan branch the snapshots use.
|
|
448
|
+
*
|
|
449
|
+
* Acceptance is a deliberate, committed act — that is the whole difference
|
|
450
|
+
* between this and a suppression flag — so the write is loud: it names the
|
|
451
|
+
* count and the storage path, and a failed push says so rather than leaving
|
|
452
|
+
* the operator believing the team's baseline moved.
|
|
453
|
+
*/
|
|
454
|
+
async function recordAcceptedBaseline(environment, existing, accepted, json, receipts) {
|
|
455
|
+
const total = Object.values(accepted).reduce((n, d) => n + d.length, 0);
|
|
456
|
+
if (total === 0) {
|
|
457
|
+
if (!json) {
|
|
458
|
+
console.error(formatWarning({
|
|
459
|
+
message: "--update-baseline: nothing to accept — no property-level deviations were reported",
|
|
460
|
+
}));
|
|
461
|
+
}
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
try {
|
|
465
|
+
// `acceptDeviations` throws on an effect-receipt deviation (#1833) —
|
|
466
|
+
// inside the try so the refusal reaches the operator as a formatted
|
|
467
|
+
// error, with no baseline written at all.
|
|
468
|
+
let next = existing ?? emptyBaseline(environment);
|
|
469
|
+
for (const [lexicon, deviations] of Object.entries(accepted)) {
|
|
470
|
+
next = acceptDeviations(next, lexicon, deviations, { receipts });
|
|
471
|
+
}
|
|
472
|
+
await writeObservationBaseline(next);
|
|
473
|
+
const pushed = await pushLifecycle();
|
|
474
|
+
if (!json) {
|
|
475
|
+
console.error(formatSuccess(`--update-baseline: accepted ${total} deviation(s) into ${environment}/${OBSERVATION_BASELINE_FILE} on chant/lifecycle` +
|
|
476
|
+
(pushed ? " (pushed)" : " (local only — no remote configured or push refused)")));
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
catch (err) {
|
|
480
|
+
console.error(formatError({
|
|
481
|
+
message: `--update-baseline: baseline not updated — ${err instanceof Error ? err.message : String(err)}`,
|
|
482
|
+
}));
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
/**
|
|
486
|
+
* `chant lifecycle diff <env> --between <refA> <refB>` (#822). Diffs two saved
|
|
487
|
+
* snapshots against each other — read-only, no build, no cloud query. Reads
|
|
488
|
+
* `<ref>:<env>/<lexicon>.json` from the orphan branch at each ref and reports the
|
|
489
|
+
* two-way delta per lexicon.
|
|
490
|
+
*/
|
|
491
|
+
async function runLifecycleDiffBetween(args) {
|
|
492
|
+
await fetchLifecycle();
|
|
493
|
+
const { config } = await loadChantConfig(resolve("."));
|
|
494
|
+
const lexicons = args.lexiconFilter ? [args.lexiconFilter] : lexiconNames(config.lexicons ?? []);
|
|
495
|
+
if (lexicons.length === 0) {
|
|
496
|
+
console.error(formatError({ message: "No lexicons to diff — pass a lexicon or declare `lexicons` in chant.config." }));
|
|
497
|
+
return 1;
|
|
498
|
+
}
|
|
499
|
+
const parseResources = (content) => {
|
|
500
|
+
if (content === null)
|
|
501
|
+
return null;
|
|
502
|
+
try {
|
|
503
|
+
return JSON.parse(content).resources ?? {};
|
|
504
|
+
}
|
|
505
|
+
catch {
|
|
506
|
+
return null;
|
|
507
|
+
}
|
|
508
|
+
};
|
|
509
|
+
const byLexicon = {};
|
|
510
|
+
for (const lexicon of lexicons) {
|
|
511
|
+
const prev = parseResources(await readSnapshotAt(args.environment, lexicon, args.refA));
|
|
512
|
+
const next = parseResources(await readSnapshotAt(args.environment, lexicon, args.refB));
|
|
513
|
+
if (prev === null && next === null) {
|
|
514
|
+
if (!args.json)
|
|
515
|
+
console.error(formatWarning({ message: `${lexicon}: not captured at either ref — skipping` }));
|
|
516
|
+
continue;
|
|
517
|
+
}
|
|
518
|
+
byLexicon[lexicon] = diffSnapshots(prev ?? {}, next ?? {});
|
|
519
|
+
}
|
|
520
|
+
if (args.json) {
|
|
521
|
+
console.log(JSON.stringify({ environment: args.environment, refA: args.refA, refB: args.refB, lexicons: byLexicon }, null, 2));
|
|
522
|
+
return 0;
|
|
523
|
+
}
|
|
524
|
+
if (Object.keys(byLexicon).length === 0) {
|
|
525
|
+
console.log(`No snapshots for ${args.environment} at ${args.refA} / ${args.refB}.`);
|
|
526
|
+
return 0;
|
|
527
|
+
}
|
|
528
|
+
console.log(formatBold(`Snapshot diff — ${args.environment}: ${args.refA} → ${args.refB}`));
|
|
529
|
+
for (const [lexicon, d] of Object.entries(byLexicon)) {
|
|
530
|
+
console.log(`\n${formatBold(lexicon)} — ${d.added.length} added, ${d.removed.length} removed, ${d.changed.length} changed, ${d.unchanged.length} unchanged`);
|
|
531
|
+
for (const name of d.added)
|
|
532
|
+
console.log(` + ${name}`);
|
|
533
|
+
for (const name of d.removed)
|
|
534
|
+
console.log(` - ${name}`);
|
|
535
|
+
for (const drift of d.changed)
|
|
536
|
+
console.log(` ~ ${drift.name} (${drift.changes.map((c) => c.path).join(", ")})`);
|
|
537
|
+
}
|
|
538
|
+
return 0;
|
|
539
|
+
}
|
|
540
|
+
async function runLifecycleDiffDigest(args) {
|
|
541
|
+
const currentDigest = computeBuildDigest(args.buildResult);
|
|
542
|
+
if (args.stack)
|
|
543
|
+
console.log(`\n${formatBold(`■ stack ${args.stack}`)}`);
|
|
544
|
+
for (const lexicon of args.lexicons) {
|
|
545
|
+
const content = await readSnapshot(args.environment, snapshotStorageKey(lexicon, args.stack));
|
|
546
|
+
let previousDigest = undefined;
|
|
547
|
+
if (content) {
|
|
548
|
+
const snapshot = JSON.parse(content);
|
|
549
|
+
previousDigest = snapshot.digest;
|
|
550
|
+
}
|
|
551
|
+
const diff = diffDigests(currentDigest, previousDigest);
|
|
552
|
+
console.log(`\n${formatBold(lexicon)}`);
|
|
553
|
+
console.log("RESOURCE".padEnd(20) + "STATUS".padEnd(12) + "TYPE");
|
|
554
|
+
console.log("-".repeat(60));
|
|
555
|
+
for (const name of diff.added) {
|
|
556
|
+
console.log(name.padEnd(20) + "added".padEnd(12) + (currentDigest.resources[name]?.type ?? ""));
|
|
557
|
+
}
|
|
558
|
+
for (const name of diff.changed) {
|
|
559
|
+
console.log(name.padEnd(20) + "changed".padEnd(12) + (currentDigest.resources[name]?.type ?? ""));
|
|
560
|
+
}
|
|
561
|
+
for (const name of diff.removed) {
|
|
562
|
+
console.log(name.padEnd(20) + "removed".padEnd(12) + (previousDigest?.resources[name]?.type ?? ""));
|
|
563
|
+
}
|
|
564
|
+
for (const name of diff.unchanged) {
|
|
565
|
+
console.log(name.padEnd(20) + "unchanged".padEnd(12) + (currentDigest.resources[name]?.type ?? ""));
|
|
566
|
+
}
|
|
567
|
+
// chant #1442 — a lexicon bump can change emitted output with no source
|
|
568
|
+
// change, so every resource above can read "unchanged" while the build is
|
|
569
|
+
// not the same build. Printed under the table rather than as a row: no
|
|
570
|
+
// resource's declaration moved, the thing that interpreted them did.
|
|
571
|
+
for (const change of diff.lexiconVersionChanges) {
|
|
572
|
+
const from = change.previous ?? "(absent)";
|
|
573
|
+
const to = change.current ?? "(absent)";
|
|
574
|
+
console.log(`\nlexicon ${change.lexicon}: ${from} → ${to}`);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
return 0;
|
|
578
|
+
}
|
|
579
|
+
/**
|
|
580
|
+
* Read one lexicon's live resources, resolving the observation tri-state
|
|
581
|
+
* (#1089) for every declared entity: present, confirmed-absent, or not
|
|
582
|
+
* observed with a reason.
|
|
583
|
+
*
|
|
584
|
+
* Two behaviours the old inline call sites did not have. A throw no longer
|
|
585
|
+
* skips the lexicon — every entity it was asked about comes back
|
|
586
|
+
* NOT-OBSERVED (`read-failed`), so a failed read shows up in the plan instead
|
|
587
|
+
* of vanishing from it. And a multi-stack read merges with
|
|
588
|
+
* present > not-observed > absent, so one unreadable stack cannot un-observe a
|
|
589
|
+
* resource another stack returned.
|
|
590
|
+
*/
|
|
591
|
+
async function observeLexicon(plugin, opts) {
|
|
592
|
+
const entityNames = Array.from(opts.declared);
|
|
593
|
+
const base = {
|
|
594
|
+
environment: opts.environment,
|
|
595
|
+
buildOutput: opts.buildOutput,
|
|
596
|
+
entityNames,
|
|
597
|
+
entities: opts.entities,
|
|
598
|
+
...(opts.region ? { region: opts.region } : {}),
|
|
599
|
+
...(opts.owned !== undefined ? { owned: opts.owned } : {}),
|
|
600
|
+
...(opts.namespace ? { namespace: opts.namespace } : {}),
|
|
601
|
+
};
|
|
602
|
+
try {
|
|
603
|
+
if (opts.componentStacks && opts.componentStacks.length > 0) {
|
|
604
|
+
const parts = [];
|
|
605
|
+
for (const stack of opts.componentStacks) {
|
|
606
|
+
parts.push(normalizeObservation(await plugin.describeResources({ ...base, stack })));
|
|
607
|
+
}
|
|
608
|
+
return mergeObservations(parts);
|
|
609
|
+
}
|
|
610
|
+
return normalizeObservation(await plugin.describeResources({ ...base, ...(opts.stack ? { stack: opts.stack } : {}) }));
|
|
611
|
+
}
|
|
612
|
+
catch (err) {
|
|
613
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
614
|
+
console.error(formatError({
|
|
615
|
+
message: `${plugin.name}: describeResources failed — ${message} (reporting ${entityNames.length} declared entity(ies) as not observed, not as absent)`,
|
|
616
|
+
}));
|
|
617
|
+
return {
|
|
618
|
+
resources: {},
|
|
619
|
+
unobserved: unobservedAll(entityNames, "read-failed", message, opts.entities),
|
|
620
|
+
queried: {},
|
|
621
|
+
sources: {},
|
|
622
|
+
notes: [],
|
|
623
|
+
};
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
/** Diff current build vs live cloud for one stack. Renders the human report
|
|
627
|
+
* inline; returns per-lexicon results so the caller emits the aggregate `--json`
|
|
628
|
+
* once (single-stack keeps the original shape; multi-stack nests under `stacks`). */
|
|
629
|
+
async function runLifecycleDiffLive(args) {
|
|
630
|
+
let totalDrift = 0;
|
|
631
|
+
let totalUnobserved = 0;
|
|
632
|
+
let totalLexiconsChecked = 0;
|
|
633
|
+
const byLexicon = {};
|
|
634
|
+
const toAccept = {};
|
|
635
|
+
if (!args.json && args.stack)
|
|
636
|
+
console.log(`\n${formatBold(`■ stack ${args.stack}`)}`);
|
|
637
|
+
for (const lexiconName of args.lexicons) {
|
|
638
|
+
const plugin = args.plugins.find((p) => p.name === lexiconName);
|
|
639
|
+
if (!plugin)
|
|
640
|
+
continue;
|
|
641
|
+
if (!plugin.describeResources && !plugin.listArtifacts) {
|
|
642
|
+
console.error(formatWarning({
|
|
643
|
+
message: `${lexiconName}: lexicon does not implement describeResources or listArtifacts — skipping (use without --live for digest diff)`,
|
|
644
|
+
}));
|
|
645
|
+
continue;
|
|
646
|
+
}
|
|
647
|
+
// Build per-lexicon entity index. Resource declarables only — outputs,
|
|
648
|
+
// parameters, serializer directives and property-kind declarables (read
|
|
649
|
+
// inside their parent, #3001) have no live counterpart of their own, and a
|
|
650
|
+
// declared name the reader can never resolve reads as missing (see
|
|
651
|
+
// lifecycle/observe.ts).
|
|
652
|
+
const declared = new Set();
|
|
653
|
+
const entities = new Map();
|
|
654
|
+
for (const [name, entity] of args.buildResult.entities) {
|
|
655
|
+
if (entity.lexicon === lexiconName && isObservableDeclarable(entity)) {
|
|
656
|
+
declared.add(name);
|
|
657
|
+
// Path origins are carried across explicitly (#1443): this map is plain
|
|
658
|
+
// objects, and the symbol-keyed provenance channel does not survive it.
|
|
659
|
+
const pathOrigins = getPathProvenance(entity);
|
|
660
|
+
entities.set(name, {
|
|
661
|
+
entityType: entity.entityType,
|
|
662
|
+
props: (entity.props != null ? entity.props : {}),
|
|
663
|
+
...(pathOrigins ? { pathOrigins } : {}),
|
|
664
|
+
});
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
const rawOutput = args.buildResult.outputs.get(lexiconName);
|
|
668
|
+
const buildOutput = rawOutput === undefined
|
|
669
|
+
? ""
|
|
670
|
+
: typeof rawOutput === "string"
|
|
671
|
+
? rawOutput
|
|
672
|
+
: rawOutput.primary;
|
|
673
|
+
// Read previous snapshot once; both flows pull what they need.
|
|
674
|
+
let prevSnapshot;
|
|
675
|
+
const content = await readSnapshot(args.environment, snapshotStorageKey(lexiconName, args.stack));
|
|
676
|
+
if (content)
|
|
677
|
+
prevSnapshot = JSON.parse(content);
|
|
678
|
+
let lexiconChecked = false;
|
|
679
|
+
// ── Resources path (entity-keyed) ──────────────────────────────────────
|
|
680
|
+
if (plugin.describeResources) {
|
|
681
|
+
const observed = await observeLexicon(plugin, {
|
|
682
|
+
environment: args.environment,
|
|
683
|
+
buildOutput,
|
|
684
|
+
declared,
|
|
685
|
+
entities,
|
|
686
|
+
stack: args.stack,
|
|
687
|
+
region: args.region,
|
|
688
|
+
componentStacks: args.componentStacks,
|
|
689
|
+
...(args.namespace ? { namespace: args.namespace } : {}),
|
|
690
|
+
});
|
|
691
|
+
const observedNow = observed.resources;
|
|
692
|
+
const observedThen = prevSnapshot?.resources;
|
|
693
|
+
const diff = diffLive({
|
|
694
|
+
declared,
|
|
695
|
+
observedNow,
|
|
696
|
+
observedThen,
|
|
697
|
+
unobserved: observed.unobserved,
|
|
698
|
+
queried: observed.queried,
|
|
699
|
+
// What each create would create (#3652), so the diff, and the plan
|
|
700
|
+
// digest ApplyOp's gate binds to it, moves when a missing entity's
|
|
701
|
+
// declaration does.
|
|
702
|
+
definitions: declaredDefinitions(entities),
|
|
703
|
+
});
|
|
704
|
+
// Unobserved entities are deliberately NOT drift: a hole in the read is
|
|
705
|
+
// not a change in the cloud. They are reported separately (#1089) so a
|
|
706
|
+
// "no drift detected" line can never be built on top of a failed read.
|
|
707
|
+
totalDrift += diff.driftedSinceSnapshot.length + diff.missing.length + diff.orphan.length + diff.disappeared.length;
|
|
708
|
+
totalUnobserved += diff.unobserved.length;
|
|
709
|
+
if (args.json) {
|
|
710
|
+
const entry = (byLexicon[lexiconName] ?? (byLexicon[lexiconName] = {}));
|
|
711
|
+
entry.resources = diff;
|
|
712
|
+
entry.observed = observedNow; // live state (#862) — status/attributes per resource
|
|
713
|
+
if (Object.keys(observed.unobserved).length > 0)
|
|
714
|
+
entry.unobserved = observed.unobserved;
|
|
715
|
+
}
|
|
716
|
+
else
|
|
717
|
+
renderLiveDiff(lexiconName, args.environment, diff);
|
|
718
|
+
lexiconChecked = true;
|
|
719
|
+
// ── Deep path (property-level, #1014) ───────────────────────────────
|
|
720
|
+
// Gated purely on the capability: a lexicon without a deep reader is
|
|
721
|
+
// completely unaffected, including its output.
|
|
722
|
+
if (plugin.observeResourcesDeep) {
|
|
723
|
+
const deep = await deepDiffForLexicon(plugin, {
|
|
724
|
+
environment: args.environment,
|
|
725
|
+
buildOutput,
|
|
726
|
+
entities,
|
|
727
|
+
stack: args.stack,
|
|
728
|
+
region: args.region,
|
|
729
|
+
componentStacks: args.componentStacks,
|
|
730
|
+
baseline: baselineForLexicon(args.baseline, lexiconName),
|
|
731
|
+
});
|
|
732
|
+
totalDrift += countPropertyDrift(deep) + countPending(deep);
|
|
733
|
+
// Only count a deep hole for an entity the thin read *did* resolve —
|
|
734
|
+
// otherwise one unreadable entity is counted twice.
|
|
735
|
+
totalUnobserved += deep.unobserved.filter((u) => !observed.unobserved[u.name]).length;
|
|
736
|
+
if (args.updateBaseline)
|
|
737
|
+
toAccept[lexiconName] = deviationsToAccept(deep);
|
|
738
|
+
// chant #2161 — the return leg, classified by fold provenance. Read off
|
|
739
|
+
// the build's own entities, which is where the record lives.
|
|
740
|
+
const provenanceOf = (name) => {
|
|
741
|
+
const entity = args.buildResult.entities.get(name);
|
|
742
|
+
return entity ? getProvenance(entity) : undefined;
|
|
743
|
+
};
|
|
744
|
+
if (args.json) {
|
|
745
|
+
const entry = (byLexicon[lexiconName] ?? (byLexicon[lexiconName] = {}));
|
|
746
|
+
entry.deep = deep;
|
|
747
|
+
entry.reconcile = resolveDeepDrift(deep.drifted, provenanceOf, { root: args.projectRoot });
|
|
748
|
+
}
|
|
749
|
+
else
|
|
750
|
+
renderDeepDiff(lexiconName, deep, provenanceOf, args.projectRoot);
|
|
751
|
+
// Printed on stderr regardless of `--json` (#2162), same reason the
|
|
752
|
+
// unobserved/disruption notices are: a shape with no column for
|
|
753
|
+
// "suspicious" must not read as clean.
|
|
754
|
+
for (const s of suspiciousHeld(deep)) {
|
|
755
|
+
console.error(formatWarning({
|
|
756
|
+
message: `${lexiconName}: ${s.name}.${s.path} is declared heldElsewhere(by: "${s.by}") but no live value ever showed up there — the hand-over may never have happened.`,
|
|
757
|
+
}));
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
// ── Artifacts path (context-keyed) ─────────────────────────────────────
|
|
762
|
+
if (plugin.listArtifacts) {
|
|
763
|
+
let observedNow;
|
|
764
|
+
try {
|
|
765
|
+
observedNow = await plugin.listArtifacts({ environment: args.environment, entities, stack: args.stack });
|
|
766
|
+
}
|
|
767
|
+
catch (err) {
|
|
768
|
+
console.error(formatError({
|
|
769
|
+
message: `${lexiconName}: listArtifacts failed — ${err instanceof Error ? err.message : String(err)}`,
|
|
770
|
+
}));
|
|
771
|
+
continue;
|
|
772
|
+
}
|
|
773
|
+
const observedThen = prevSnapshot?.artifacts;
|
|
774
|
+
const adiff = diffLiveArtifacts({ observedNow, observedThen });
|
|
775
|
+
totalDrift += adiff.added.length + adiff.removed.length + adiff.changed.length;
|
|
776
|
+
if (args.json) {
|
|
777
|
+
const lex = (byLexicon[lexiconName] ?? (byLexicon[lexiconName] = {}));
|
|
778
|
+
lex.artifacts = adiff;
|
|
779
|
+
// What was actually seen, not just how it moved (behold#146): the
|
|
780
|
+
// metadata was in hand and dropped, leaving JSON consumers unable to
|
|
781
|
+
// read an artifact's own status (a Helm release's "deployed").
|
|
782
|
+
lex.observedArtifacts = observedNow;
|
|
783
|
+
}
|
|
784
|
+
else
|
|
785
|
+
renderLiveArtifactDiff(lexiconName, args.environment, adiff);
|
|
786
|
+
lexiconChecked = true;
|
|
787
|
+
}
|
|
788
|
+
if (lexiconChecked)
|
|
789
|
+
totalLexiconsChecked++;
|
|
790
|
+
}
|
|
791
|
+
return { byLexicon, totalDrift, totalUnobserved, totalLexiconsChecked, toAccept };
|
|
792
|
+
}
|
|
793
|
+
/**
|
|
794
|
+
* Everything a deep diff reported this run, as deviations to record accepted.
|
|
795
|
+
* `--update-baseline` accepts what was *reported*, never what was already
|
|
796
|
+
* suppressed — re-accepting an unchanged suppression would rewrite its
|
|
797
|
+
* `recordedAt` on every run and turn the baseline into a churn file.
|
|
798
|
+
*/
|
|
799
|
+
function deviationsToAccept(deep) {
|
|
800
|
+
const out = [];
|
|
801
|
+
for (const entity of deep.drifted) {
|
|
802
|
+
for (const change of entity.changes) {
|
|
803
|
+
// Only a value that is actually live can be accepted: `absent` means the
|
|
804
|
+
// cloud does not carry the declared property, which is a finding to fix
|
|
805
|
+
// in source or in the cloud, not a value to bless.
|
|
806
|
+
if (!("live" in change))
|
|
807
|
+
continue;
|
|
808
|
+
out.push({ entity: entity.name, type: entity.type, path: change.path, value: change.live });
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
return out;
|
|
812
|
+
}
|
|
813
|
+
/** Property-level drift report (#1014). Silent when a lexicon's deep read found nothing to say. */
|
|
814
|
+
function renderDeepDiff(lexiconName, deep, provenanceOf = () => undefined, projectRoot) {
|
|
815
|
+
const drift = countPropertyDrift(deep);
|
|
816
|
+
const unclaimedCount = countUnclaimed(deep);
|
|
817
|
+
const heldCount = countHeld(deep);
|
|
818
|
+
const pendingCount = countPending(deep);
|
|
819
|
+
if (drift === 0 &&
|
|
820
|
+
pendingCount === 0 &&
|
|
821
|
+
heldCount === 0 &&
|
|
822
|
+
unclaimedCount === 0 &&
|
|
823
|
+
deep.accepted.length === 0 &&
|
|
824
|
+
deep.unobserved.length === 0 &&
|
|
825
|
+
deep.undeclaredEntities.length === 0) {
|
|
826
|
+
return;
|
|
827
|
+
}
|
|
828
|
+
const acceptedCount = deep.accepted.reduce((n, e) => n + e.changes.length, 0);
|
|
829
|
+
console.log(`\n${formatBold(`${lexiconName} (properties)`)}`);
|
|
830
|
+
console.log(`${drift} property drift across ${deep.drifted.length} resource(s), ` +
|
|
831
|
+
`${acceptedCount} accepted, ${deep.unchanged.length} unchanged` +
|
|
832
|
+
(pendingCount > 0 ? `, ${pendingCount} pending` : "") +
|
|
833
|
+
(heldCount > 0 ? `, ${heldCount} held` : "") +
|
|
834
|
+
(unclaimedCount > 0 ? `, ${unclaimedCount} unclaimed` : "") +
|
|
835
|
+
(deep.unobserved.length > 0 ? `, ${deep.unobserved.length} unobserved` : ""));
|
|
836
|
+
console.log("-".repeat(80));
|
|
837
|
+
if (deep.unobserved.length > 0) {
|
|
838
|
+
console.log(formatBold("\nPROPERTIES UNOBSERVED (declared; the deep read could not look):"));
|
|
839
|
+
for (const u of deep.unobserved)
|
|
840
|
+
console.log(` ? ${formatUnobserved(u.name, u)}`);
|
|
841
|
+
}
|
|
842
|
+
if (deep.drifted.length > 0) {
|
|
843
|
+
console.log(formatBold("\nPROPERTY DRIFT (declared vs live; baseline shown where one exists):"));
|
|
844
|
+
for (const entity of deep.drifted) {
|
|
845
|
+
console.log(` - ${entity.name} (${entity.type})`);
|
|
846
|
+
const provenance = provenanceOf(entity.name);
|
|
847
|
+
for (const change of entity.changes) {
|
|
848
|
+
const declared = "declared" in change ? formatValue(change.declared) : "<undeclared>";
|
|
849
|
+
const live = "live" in change ? formatValue(change.live) : "<absent>";
|
|
850
|
+
const baseline = "baseline" in change ? ` [accepted: ${formatValue(change.baseline)}]` : "";
|
|
851
|
+
// Both sides of the attribution, where each is known (#1443/#1189).
|
|
852
|
+
const from = change.origin ? ` [from: ${describePathOrigin(change.origin)}]` : "";
|
|
853
|
+
const owner = change.owner ? ` [owner: ${change.owner}]` : "";
|
|
854
|
+
const seen = change.seenOn ? ` [seen on: ${change.seenOn}]` : "";
|
|
855
|
+
console.log(` ${change.path}: ${declared} → ${live}${baseline}${from}${owner}${seen}`);
|
|
856
|
+
// chant #2161 — what may be done about it, when the origin says
|
|
857
|
+
// something today's behaviour would get wrong. A field the author
|
|
858
|
+
// declared directly is left unremarked: editing it IS today's
|
|
859
|
+
// behaviour, and saying so on every row would bury the two that matter.
|
|
860
|
+
// #3597 — the line before the proposal names the composite call and
|
|
861
|
+
// the argument behind the field, with the file and line it was written on.
|
|
862
|
+
const { resolution, summary } = resolveDriftedField({
|
|
863
|
+
entity: entity.name,
|
|
864
|
+
entityType: entity.type,
|
|
865
|
+
path: change.path,
|
|
866
|
+
...("declared" in change ? { declared: change.declared } : {}),
|
|
867
|
+
...("live" in change ? { live: change.live } : {}),
|
|
868
|
+
...(change.origin ? { origin: change.origin } : {}),
|
|
869
|
+
...(provenance ? { provenance } : {}),
|
|
870
|
+
...(projectRoot ? { root: projectRoot } : {}),
|
|
871
|
+
});
|
|
872
|
+
if (resolution.kind !== "edit-declaration") {
|
|
873
|
+
console.log(` ${summary}`);
|
|
874
|
+
console.log(` ${resolution.description}`);
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
if (pendingCount > 0) {
|
|
880
|
+
// #3706: changes an apply would make that no declared property shows
|
|
881
|
+
// (privileges granted by hand). Printed, so the plan digest moves with them.
|
|
882
|
+
console.log(formatBold("\nPENDING (changes an apply would make that no declared property shows):"));
|
|
883
|
+
for (const p of deep.pending ?? []) {
|
|
884
|
+
console.log(` ~ ${p.subject}: ${p.change}${p.entities && p.entities.length > 0 ? ` [from: ${p.entities.join(", ")}]` : ""}`);
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
if (deep.unclaimed.length > 0) {
|
|
888
|
+
// Not drift, and printed after it for that reason (#2160): chant never set
|
|
889
|
+
// these fields, so nothing here is a change chant proposes to make.
|
|
890
|
+
console.log(formatBold("\nUNCLAIMED (live values on properties chant never declared; not drift):"));
|
|
891
|
+
for (const entity of deep.unclaimed) {
|
|
892
|
+
console.log(` - ${entity.name} (${entity.type})`);
|
|
893
|
+
for (const field of entity.fields) {
|
|
894
|
+
const holder = field.heldBy
|
|
895
|
+
? `held by ${field.heldBy}`
|
|
896
|
+
: "not in this declaration's claimed fields";
|
|
897
|
+
console.log(` ${field.path}: ${formatValue(field.live)} [${holder}]`);
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
if (heldCount > 0) {
|
|
902
|
+
console.log(formatBold("\nHELD (declared heldElsewhere(); not drift, never proposed for update):"));
|
|
903
|
+
for (const line of renderHeldLines(deep.held))
|
|
904
|
+
console.log(line);
|
|
905
|
+
}
|
|
906
|
+
if (deep.undeclaredEntities.length > 0) {
|
|
907
|
+
console.log(formatBold("\nUNDECLARED (read deeply, never declared in source):"));
|
|
908
|
+
for (const name of deep.undeclaredEntities)
|
|
909
|
+
console.log(` - ${name}`);
|
|
910
|
+
}
|
|
911
|
+
if (acceptedCount > 0) {
|
|
912
|
+
console.log(formatBold("\nACCEPTED (in the baseline; not drift):"));
|
|
913
|
+
for (const entity of deep.accepted) {
|
|
914
|
+
console.log(` - ${entity.name}: ${entity.changes.map((c) => c.path).join(", ")}`);
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
/**
|
|
919
|
+
* Render one `heldElsewhere()` property (#2162) — shared between the deep
|
|
920
|
+
* diff's HELD section and the plan's, so the two never disagree about what a
|
|
921
|
+
* held field looks like on the terminal.
|
|
922
|
+
*/
|
|
923
|
+
function formatHeldProperty(h) {
|
|
924
|
+
const live = "live" in h ? formatValue(h.live) : "<absent>";
|
|
925
|
+
const owner = h.owner ? ` [owner: ${h.owner}]` : "";
|
|
926
|
+
const suspicious = h.suspicious
|
|
927
|
+
? " [SUSPICIOUS: no live value ever showed up here — check the holder is actually running, or that this is the field it writes]"
|
|
928
|
+
: "";
|
|
929
|
+
return ` ${h.path}: held by ${h.by} — ${live}${owner} (${h.reason})${suspicious}`;
|
|
930
|
+
}
|
|
931
|
+
/** Lines for every held property across a lexicon's entities, indented and grouped by entity name. */
|
|
932
|
+
function renderHeldLines(held) {
|
|
933
|
+
const lines = [];
|
|
934
|
+
for (const entity of held) {
|
|
935
|
+
lines.push(` - ${entity.name} (${entity.type})`);
|
|
936
|
+
for (const h of entity.held)
|
|
937
|
+
lines.push(formatHeldProperty(h));
|
|
938
|
+
}
|
|
939
|
+
return lines;
|
|
940
|
+
}
|
|
941
|
+
function renderLiveDiff(lexiconName, environment, diff) {
|
|
942
|
+
const counts = `${diff.missing.length} missing, ${diff.orphan.length} orphan, ` +
|
|
943
|
+
`${diff.disappeared.length} disappeared, ${diff.newlyObserved.length} newly observed, ` +
|
|
944
|
+
`${diff.driftedSinceSnapshot.length} drifted, ${diff.unchanged.length} unchanged` +
|
|
945
|
+
(diff.runtimeChildren.length > 0 ? `, ${diff.runtimeChildren.length} runtime` : "") +
|
|
946
|
+
(diff.unobserved.length > 0 ? `, ${diff.unobserved.length} unobserved` : "");
|
|
947
|
+
console.log(`\n${formatBold(lexiconName)} — environment: ${environment}`);
|
|
948
|
+
console.log(counts);
|
|
949
|
+
console.log("-".repeat(80));
|
|
950
|
+
if (diff.unobserved.length > 0) {
|
|
951
|
+
console.log(formatBold("\nUNOBSERVED (declared; chant could not read live state — status unknown):"));
|
|
952
|
+
for (const u of diff.unobserved) {
|
|
953
|
+
console.log(` ? ${formatUnobserved(u.name, u)}`);
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
if (diff.missing.length > 0) {
|
|
957
|
+
console.log(formatBold("\nMISSING (declared, provider reports not in cloud):"));
|
|
958
|
+
for (const name of diff.missing) {
|
|
959
|
+
console.log(missingRow(diff, name));
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
if (diff.orphan.length > 0) {
|
|
963
|
+
console.log(formatBold("\nORPHAN (in cloud, not declared):"));
|
|
964
|
+
for (const name of diff.orphan)
|
|
965
|
+
console.log(` - ${name}`);
|
|
966
|
+
}
|
|
967
|
+
if (diff.runtimeChildren.length > 0) {
|
|
968
|
+
console.log(formatBold("\nRUNTIME (owned by a declared resource; not drift, not an orphan — #1077):"));
|
|
969
|
+
for (const r of diff.runtimeChildren)
|
|
970
|
+
console.log(` - ${r.name} (${r.type}) — owned by ${r.owner}`);
|
|
971
|
+
}
|
|
972
|
+
if (diff.disappeared.length > 0) {
|
|
973
|
+
console.log(formatBold("\nDISAPPEARED (in last snapshot, gone now):"));
|
|
974
|
+
for (const name of diff.disappeared)
|
|
975
|
+
console.log(` - ${name}`);
|
|
976
|
+
}
|
|
977
|
+
if (diff.newlyObserved.length > 0) {
|
|
978
|
+
console.log(formatBold("\nNEWLY OBSERVED (declared, observed, no prior snapshot):"));
|
|
979
|
+
for (const name of diff.newlyObserved)
|
|
980
|
+
console.log(` - ${name}`);
|
|
981
|
+
}
|
|
982
|
+
if (diff.driftedSinceSnapshot.length > 0) {
|
|
983
|
+
console.log(formatBold("\nDRIFTED (changed since last snapshot):"));
|
|
984
|
+
for (const drift of diff.driftedSinceSnapshot) {
|
|
985
|
+
console.log(` - ${drift.name} (${drift.type})`);
|
|
986
|
+
for (const change of drift.changes) {
|
|
987
|
+
const oldStr = formatValue(change.oldValue);
|
|
988
|
+
const newStr = formatValue(change.newValue);
|
|
989
|
+
console.log(` ${change.path}: ${oldStr} → ${newStr}`);
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
function formatValue(v) {
|
|
995
|
+
if (v === undefined)
|
|
996
|
+
return "<unset>";
|
|
997
|
+
if (typeof v === "string")
|
|
998
|
+
return v.length > 60 ? v.slice(0, 57) + "..." : v;
|
|
999
|
+
const json = JSON.stringify(v);
|
|
1000
|
+
return json.length > 60 ? json.slice(0, 57) + "..." : json;
|
|
1001
|
+
}
|
|
1002
|
+
function renderLiveArtifactDiff(lexiconName, environment, diff) {
|
|
1003
|
+
// Skip emission entirely when there's nothing to show — keeps the output
|
|
1004
|
+
// clean for lexicons that only implement describeResources.
|
|
1005
|
+
if (diff.added.length === 0 && diff.removed.length === 0 && diff.changed.length === 0 && diff.unchanged.length === 0) {
|
|
1006
|
+
return;
|
|
1007
|
+
}
|
|
1008
|
+
const counts = `${diff.added.length} added, ${diff.removed.length} removed, ` +
|
|
1009
|
+
`${diff.changed.length} changed, ${diff.unchanged.length} unchanged`;
|
|
1010
|
+
console.log(`\n${formatBold(lexiconName)} (artifacts) — environment: ${environment}`);
|
|
1011
|
+
console.log(counts);
|
|
1012
|
+
console.log("-".repeat(80));
|
|
1013
|
+
if (diff.added.length > 0) {
|
|
1014
|
+
console.log(formatBold("\nARTIFACTS ADDED (in cloud now, not in last snapshot):"));
|
|
1015
|
+
for (const name of diff.added)
|
|
1016
|
+
console.log(` - ${name}`);
|
|
1017
|
+
}
|
|
1018
|
+
if (diff.removed.length > 0) {
|
|
1019
|
+
console.log(formatBold("\nARTIFACTS REMOVED (in last snapshot, gone now):"));
|
|
1020
|
+
for (const name of diff.removed)
|
|
1021
|
+
console.log(` - ${name}`);
|
|
1022
|
+
}
|
|
1023
|
+
if (diff.changed.length > 0) {
|
|
1024
|
+
console.log(formatBold("\nARTIFACTS CHANGED (metadata differs since last snapshot):"));
|
|
1025
|
+
for (const drift of diff.changed) {
|
|
1026
|
+
console.log(` - ${drift.name} (${drift.type})`);
|
|
1027
|
+
for (const change of drift.changes) {
|
|
1028
|
+
const oldStr = formatValue(change.oldValue);
|
|
1029
|
+
const newStr = formatValue(change.newValue);
|
|
1030
|
+
console.log(` ${change.path}: ${oldStr} → ${newStr}`);
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
/**
|
|
1036
|
+
* chant lifecycle plan <environment> [lexicon]
|
|
1037
|
+
*
|
|
1038
|
+
* Promote the live diff to a typed, read-only change set: per-entity
|
|
1039
|
+
* create / update / delete / adopt / noop / unobserved. Strictly read-only —
|
|
1040
|
+
* never mutates, never deploys. Deletes are never proposed without ownership
|
|
1041
|
+
* data (added in #121); an undeclared live resource is `adopt`; a declared
|
|
1042
|
+
* entity chant could not read is `unobserved` and gets no proposal at all
|
|
1043
|
+
* (#1089) — a create is only ever proposed against a confirmed absence.
|
|
1044
|
+
*/
|
|
1045
|
+
export async function runLifecyclePlan(ctx) {
|
|
1046
|
+
const { args, plugins, serializers } = ctx;
|
|
1047
|
+
const environment = args.extraPositional;
|
|
1048
|
+
const lexiconFilter = args.extraPositional2;
|
|
1049
|
+
if (!environment) {
|
|
1050
|
+
console.error(formatError({ message: "Environment is required: chant lifecycle plan <environment> [lexicon]" }));
|
|
1051
|
+
return 1;
|
|
1052
|
+
}
|
|
1053
|
+
const targetSerializers = lexiconFilter
|
|
1054
|
+
? plugins.filter((p) => p.name === lexiconFilter).map((p) => p.serializer)
|
|
1055
|
+
: serializers;
|
|
1056
|
+
const { config } = await loadChantConfig(resolve("."));
|
|
1057
|
+
// This invocation's parameters, so the declared side of the comparison is the estate
|
|
1058
|
+
// the caller asked for rather than the parameter defaults (#1483).
|
|
1059
|
+
const declaredParams = await commandBuildParams(config.buildParams, args);
|
|
1060
|
+
if (!declaredParams)
|
|
1061
|
+
return 1;
|
|
1062
|
+
const buildResult = await build(resolveBuildRoot(args, config), targetSerializers, undefined, {
|
|
1063
|
+
buildParams: declaredParams,
|
|
1064
|
+
buildRoots: collectBuildRootContributors(plugins, config, resolve(".")),
|
|
1065
|
+
});
|
|
1066
|
+
if (buildResult.errors.length > 0) {
|
|
1067
|
+
console.error(formatError({ message: "Build failed — fix errors before planning" }));
|
|
1068
|
+
return 1;
|
|
1069
|
+
}
|
|
1070
|
+
await fetchLifecycle();
|
|
1071
|
+
const lexicons = lexiconFilter ? [lexiconFilter] : Array.from(buildResult.manifest.lexicons);
|
|
1072
|
+
// Multi-stack component projects deploy one CFN stack per component, so the
|
|
1073
|
+
// single-stack-per-env observe below would see nothing live and plan every
|
|
1074
|
+
// resource as "create". Resolve each component's cfn-deploy stack(s) and
|
|
1075
|
+
// observe them all — the same fix runGraphLive uses (graph.ts). Empty for a
|
|
1076
|
+
// single-stack project → the original single-call path is kept.
|
|
1077
|
+
let componentStacks = [];
|
|
1078
|
+
try {
|
|
1079
|
+
const disc = await discoverComponents(resolveBuildRoot(args, config), { sandbox: args.sandbox });
|
|
1080
|
+
const set = new Set();
|
|
1081
|
+
for (const { component } of disc.components.values())
|
|
1082
|
+
for (const s of cfnDeployStacks(component.deploy))
|
|
1083
|
+
set.add(s);
|
|
1084
|
+
componentStacks = [...set];
|
|
1085
|
+
}
|
|
1086
|
+
catch {
|
|
1087
|
+
// no components / discovery failed → single-stack path
|
|
1088
|
+
}
|
|
1089
|
+
const merged = { env: environment, entries: [] };
|
|
1090
|
+
let checked = 0;
|
|
1091
|
+
// Held properties (#2162), across every lexicon this plan checks — not an
|
|
1092
|
+
// action, so it rides beside `entries` rather than inside it.
|
|
1093
|
+
const heldSets = [];
|
|
1094
|
+
// Effect receipts (#1832): declared, diffed, and observed like any resource,
|
|
1095
|
+
// but observe-only to the generic apply path — the plan compares live value
|
|
1096
|
+
// to resolved expectation and proposes the fire, never a create/update.
|
|
1097
|
+
const receipts = collectEffectReceipts(buildResult.entities);
|
|
1098
|
+
const receiptReadings = new Map();
|
|
1099
|
+
// Every lexicon's observed resources merged, for resolving a receipt's
|
|
1100
|
+
// reference inputs against observed values / stack outputs at plan time.
|
|
1101
|
+
const allObservedResources = {};
|
|
1102
|
+
// #1166 — same self-sufficiency as `chant graph --live`: an environment can
|
|
1103
|
+
// declare its own endpoint, applied here unless the ambient shell already
|
|
1104
|
+
// set it. `chant lifecycle plan` is always a live read (no `--live` flag of
|
|
1105
|
+
// its own), so this applies unconditionally.
|
|
1106
|
+
// Names here, plugins there: the endpoint vars come from each lexicon's own
|
|
1107
|
+
// emulator capability (#1345), so this needs the loaded plugin, not the name.
|
|
1108
|
+
const endpointResult = applyLiveEndpoint(config.environments, environment, lexicons.map((name) => plugins.find((p) => p.name === name)).filter((p) => p !== undefined));
|
|
1109
|
+
if (endpointResult.notice)
|
|
1110
|
+
console.error(formatWarning({ message: endpointResult.notice }));
|
|
1111
|
+
try {
|
|
1112
|
+
for (const lexiconName of lexicons) {
|
|
1113
|
+
const plugin = plugins.find((p) => p.name === lexiconName);
|
|
1114
|
+
if (!plugin)
|
|
1115
|
+
continue;
|
|
1116
|
+
if (!plugin.describeResources) {
|
|
1117
|
+
// Plan is entity-keyed; artifact-only lexicons have no declared axis.
|
|
1118
|
+
if (!args.json) {
|
|
1119
|
+
console.error(formatWarning({
|
|
1120
|
+
message: `${lexiconName}: lexicon does not implement describeResources — skipping (no declared axis to plan against)`,
|
|
1121
|
+
}));
|
|
1122
|
+
}
|
|
1123
|
+
continue;
|
|
1124
|
+
}
|
|
1125
|
+
const declared = new Set();
|
|
1126
|
+
const entities = new Map();
|
|
1127
|
+
for (const [name, entity] of buildResult.entities) {
|
|
1128
|
+
// A receipt has no `props` payload of its own but is declared, diffed,
|
|
1129
|
+
// and observed like any resource (#1832) — it joins the declared axis
|
|
1130
|
+
// so its lexicon's observation can confirm presence or absence.
|
|
1131
|
+
if (entity.lexicon === lexiconName && (isObservableDeclarable(entity) || isEffectReceipt(entity))) {
|
|
1132
|
+
declared.add(name);
|
|
1133
|
+
entities.set(name, {
|
|
1134
|
+
entityType: entity.entityType,
|
|
1135
|
+
props: (isObservableDeclarable(entity) && entity.props != null ? entity.props : {}),
|
|
1136
|
+
});
|
|
1137
|
+
}
|
|
1138
|
+
}
|
|
1139
|
+
const rawOutput = buildResult.outputs.get(lexiconName);
|
|
1140
|
+
const buildOutput = rawOutput === undefined
|
|
1141
|
+
? ""
|
|
1142
|
+
: typeof rawOutput === "string"
|
|
1143
|
+
? rawOutput
|
|
1144
|
+
: rawOutput.primary;
|
|
1145
|
+
const observed = await observeLexicon(plugin, {
|
|
1146
|
+
environment,
|
|
1147
|
+
buildOutput,
|
|
1148
|
+
declared,
|
|
1149
|
+
entities,
|
|
1150
|
+
componentStacks,
|
|
1151
|
+
owned: args.owned,
|
|
1152
|
+
// A plan reading the wrong namespace proposes creates for resources
|
|
1153
|
+
// that are running (#1629) — the same override the live diff takes.
|
|
1154
|
+
...(args.namespace ? { namespace: args.namespace } : {}),
|
|
1155
|
+
});
|
|
1156
|
+
Object.assign(allObservedResources, observed.resources);
|
|
1157
|
+
// Held properties (#2162), behind `--deep` since #2405.
|
|
1158
|
+
//
|
|
1159
|
+
// A held property is never a proposal: it is computed against this same
|
|
1160
|
+
// observation and carried on the plan beside `entries`, never folded
|
|
1161
|
+
// into one. So the entries a plan proposes are identical whether this
|
|
1162
|
+
// ran or not, and the only thing `--deep` buys is the HELD section.
|
|
1163
|
+
//
|
|
1164
|
+
// It used to run whenever the lexicon could, which made the read's cost
|
|
1165
|
+
// a function of how many resources an estate holds rather than how it is
|
|
1166
|
+
// organised. Measured on 528 resources across 8 stacks: 24 stack-shaped
|
|
1167
|
+
// calls, and 496 from this pass. At ten thousand resources that is
|
|
1168
|
+
// roughly twenty thousand calls against eighty, for a section the caller
|
|
1169
|
+
// did not ask for.
|
|
1170
|
+
//
|
|
1171
|
+
// `--deep` is the flag `lifecycle snapshot` already uses for the same
|
|
1172
|
+
// choice, and `lifecycle diff` still runs the pass unconditionally
|
|
1173
|
+
// because property drift is the question it exists to answer.
|
|
1174
|
+
if (args.deep && plugin.observeResourcesDeep) {
|
|
1175
|
+
const deep = await deepDiffForLexicon(plugin, {
|
|
1176
|
+
environment,
|
|
1177
|
+
buildOutput,
|
|
1178
|
+
entities,
|
|
1179
|
+
componentStacks,
|
|
1180
|
+
});
|
|
1181
|
+
for (const entity of deep.held)
|
|
1182
|
+
heldSets.push({ ...entity, lexicon: lexiconName });
|
|
1183
|
+
for (const s of suspiciousHeld(deep)) {
|
|
1184
|
+
console.error(formatWarning({
|
|
1185
|
+
message: `${lexiconName}: ${s.name}.${s.path} is declared heldElsewhere(by: "${s.by}") but no live value ever showed up there — the hand-over may never have happened.`,
|
|
1186
|
+
}));
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
for (const [receiptName, receipt] of receipts) {
|
|
1190
|
+
if (receipt.lexicon !== lexiconName)
|
|
1191
|
+
continue;
|
|
1192
|
+
const live = observed.resources[receiptName];
|
|
1193
|
+
const hole = observed.unobserved[receiptName];
|
|
1194
|
+
if (live) {
|
|
1195
|
+
receiptReadings.set(receiptName, {
|
|
1196
|
+
observed: true,
|
|
1197
|
+
present: true,
|
|
1198
|
+
value: readReceiptValue(live.attributes),
|
|
1199
|
+
type: live.type,
|
|
1200
|
+
...(live.physicalId ? { physicalId: live.physicalId } : {}),
|
|
1201
|
+
lexicon: lexiconName,
|
|
1202
|
+
});
|
|
1203
|
+
}
|
|
1204
|
+
else if (hole) {
|
|
1205
|
+
receiptReadings.set(receiptName, {
|
|
1206
|
+
observed: false,
|
|
1207
|
+
present: false,
|
|
1208
|
+
lexicon: lexiconName,
|
|
1209
|
+
...(hole.type ? { type: hole.type } : {}),
|
|
1210
|
+
unobservedReason: hole.reason,
|
|
1211
|
+
...(hole.detail ? { unobservedDetail: hole.detail } : {}),
|
|
1212
|
+
});
|
|
1213
|
+
}
|
|
1214
|
+
else {
|
|
1215
|
+
// Neither returned nor named unobserved: the lexicon looked and
|
|
1216
|
+
// confirmed the receipt absent — the same claim the change set
|
|
1217
|
+
// reads off a bare observation (#1089).
|
|
1218
|
+
receiptReadings.set(receiptName, { observed: true, present: false, lexicon: lexiconName });
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
const content = await readSnapshot(environment, lexiconName);
|
|
1222
|
+
const observedThen = content ? JSON.parse(content).resources : undefined;
|
|
1223
|
+
const cs = buildChangeSet(environment, {
|
|
1224
|
+
declared,
|
|
1225
|
+
observedNow: observed.resources,
|
|
1226
|
+
observedThen,
|
|
1227
|
+
unobserved: observed.unobserved,
|
|
1228
|
+
// The resolved read address rides every entry, not just unobserved
|
|
1229
|
+
// ones — the diff path already passes it (#1620); plan lost it.
|
|
1230
|
+
queried: observed.queried,
|
|
1231
|
+
// A create carries its declaration's fingerprint (#3652), so the
|
|
1232
|
+
// plan's digest moves when what it would create does.
|
|
1233
|
+
definitions: declaredDefinitions(entities),
|
|
1234
|
+
}, {
|
|
1235
|
+
// Attribution survives the flat merge below (#1674).
|
|
1236
|
+
lexicon: lexiconName,
|
|
1237
|
+
});
|
|
1238
|
+
// Disruption (#1665) is asked of THIS lexicon, before the merge: the
|
|
1239
|
+
// deltas are paths into its own observation shape, and the replacement
|
|
1240
|
+
// rules are in the spec it compiled. A lexicon that answers nothing
|
|
1241
|
+
// leaves every update `unknown` — never `in-place`.
|
|
1242
|
+
const classified = await annotateDisruption(cs, environment, plugin.classifyDisruption ? (o) => plugin.classifyDisruption(o) : undefined);
|
|
1243
|
+
merged.entries.push(...classified.entries);
|
|
1244
|
+
checked++;
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
finally {
|
|
1248
|
+
endpointResult.restore();
|
|
1249
|
+
}
|
|
1250
|
+
if (checked === 0) {
|
|
1251
|
+
console.error(formatWarning({
|
|
1252
|
+
message: "No lexicons implement describeResources — nothing to plan",
|
|
1253
|
+
}));
|
|
1254
|
+
return 1;
|
|
1255
|
+
}
|
|
1256
|
+
// Receipt classification (#1832): live value vs resolved expectation.
|
|
1257
|
+
// Absent/stale proposes the fire; unobservable is a loud hole; whatever the
|
|
1258
|
+
// generic classification proposed for a receipt (a create, a delete) is
|
|
1259
|
+
// replaced — the effect step is the sole writer, and a receipt is never a
|
|
1260
|
+
// prune candidate.
|
|
1261
|
+
if (receipts.size > 0) {
|
|
1262
|
+
const receiptEntries = planReceipts(receipts, receiptReadings, observedValueResolver(allObservedResources));
|
|
1263
|
+
mergeReceiptEntries(merged, receipts, receiptEntries);
|
|
1264
|
+
}
|
|
1265
|
+
merged.entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
1266
|
+
if (heldSets.length > 0)
|
|
1267
|
+
merged.held = heldSets.sort((a, b) => a.name.localeCompare(b.name));
|
|
1268
|
+
// Say it on stderr too, so `--json` and `--report gitlab-mr` consumers (whose
|
|
1269
|
+
// shapes have no room for it) still learn the plan has a hole (#1089). The
|
|
1270
|
+
// `markdown` report carries the same wording in its own body instead — a
|
|
1271
|
+
// reviewer reading a comment never sees this stderr.
|
|
1272
|
+
for (const notice of unobservedPlanNotice(merged)) {
|
|
1273
|
+
console.error(formatWarning({ message: notice }));
|
|
1274
|
+
}
|
|
1275
|
+
// Same reason as above (#1665): the `--json` and `--report gitlab-mr` shapes
|
|
1276
|
+
// have no column for how much an update hurts.
|
|
1277
|
+
for (const notice of disruptionNotices(merged)) {
|
|
1278
|
+
console.error(formatWarning({ message: notice }));
|
|
1279
|
+
}
|
|
1280
|
+
// `--report gitlab-mr` emits the GitLab MR plan-widget artifact instead of the
|
|
1281
|
+
// human render. Write it to a file (`tfplan.json`) in CI and declare it as
|
|
1282
|
+
// `artifacts:reports:terraform` to light up the merge-request widget.
|
|
1283
|
+
if (args.reportFile === "gitlab-mr") {
|
|
1284
|
+
console.log(JSON.stringify(gitlabMrReport(merged)));
|
|
1285
|
+
return 0;
|
|
1286
|
+
}
|
|
1287
|
+
// `--report markdown` emits the reviewer-facing projection (#1983) — a
|
|
1288
|
+
// counts header, entries grouped and attributed to their lexicon, holes and
|
|
1289
|
+
// disruption both carried in the body rather than left to stderr.
|
|
1290
|
+
if (args.reportFile === "markdown") {
|
|
1291
|
+
console.log(renderChangeSetMarkdown(merged));
|
|
1292
|
+
return 0;
|
|
1293
|
+
}
|
|
1294
|
+
if (args.json) {
|
|
1295
|
+
console.log(JSON.stringify(merged, null, 2));
|
|
1296
|
+
}
|
|
1297
|
+
else {
|
|
1298
|
+
console.log(renderChangeSet(merged));
|
|
1299
|
+
}
|
|
1300
|
+
return 0;
|
|
1301
|
+
}
|
|
1302
|
+
/**
|
|
1303
|
+
* chant lifecycle teardown <environment> (#1222).
|
|
1304
|
+
*
|
|
1305
|
+
* Enumerates what deleting the environment would remove: every live resource
|
|
1306
|
+
* carrying this project's ownership marker (managed-by + `ownership.stack`)
|
|
1307
|
+
* with the requested env identity. Stateless — live markers only, no build, no
|
|
1308
|
+
* snapshot. Without `--yes` nothing is deleted; with it the planned set is
|
|
1309
|
+
* executed per lexicon with one bounded retry pass over failures, and every
|
|
1310
|
+
* candidate's outcome is reported (deleted / failed / not-prunable / retained / skipped —
|
|
1311
|
+
* never silence). A production-like environment name additionally requires an
|
|
1312
|
+
* interactive confirmation, or `--confirm-prod` non-interactively.
|
|
1313
|
+
*/
|
|
1314
|
+
export async function runLifecycleTeardown(ctx) {
|
|
1315
|
+
const { args, plugins } = ctx;
|
|
1316
|
+
const environment = args.extraPositional;
|
|
1317
|
+
if (!environment) {
|
|
1318
|
+
console.error(formatError({ message: "Environment is required: chant lifecycle teardown <environment>" }));
|
|
1319
|
+
return 1;
|
|
1320
|
+
}
|
|
1321
|
+
const { config } = await loadChantConfig(resolve("."));
|
|
1322
|
+
// Refuse an env the project does not declare — a typo here is the difference
|
|
1323
|
+
// between tearing down `dev` and tearing down `prod`. Literal `environments`
|
|
1324
|
+
// entries only (#1221's pattern entries would extend this same check).
|
|
1325
|
+
const envErr = unknownEnvError(environment, config.environments);
|
|
1326
|
+
if (envErr) {
|
|
1327
|
+
console.error(formatError({ message: envErr }));
|
|
1328
|
+
return 1;
|
|
1329
|
+
}
|
|
1330
|
+
// Teardown selects on the ownership marker; a project that stamps none has
|
|
1331
|
+
// nothing to key on, and "delete what looks like mine" is not a fallback.
|
|
1332
|
+
const stack = resolveOwnershipStack(config);
|
|
1333
|
+
if (stack === undefined) {
|
|
1334
|
+
console.error(formatError({
|
|
1335
|
+
message: "This project declares no ownership.stack — teardown is marker-scoped and has nothing to select on",
|
|
1336
|
+
hint: 'Set `ownership: { stack: "<name>" }` in chant.config.ts and deploy, so resources carry the marker teardown keys on.',
|
|
1337
|
+
}));
|
|
1338
|
+
return 1;
|
|
1339
|
+
}
|
|
1340
|
+
// The prod guard (#1222): a production-like name never falls to `--yes`
|
|
1341
|
+
// alone. Interactive runs re-type the environment name; non-interactive
|
|
1342
|
+
// runs say `--confirm-prod` explicitly. Checked before any live read so a
|
|
1343
|
+
// refused teardown touches nothing at all.
|
|
1344
|
+
if (args.yes && isProdLikeEnvironment(environment) && !args.confirmProd) {
|
|
1345
|
+
if (!process.stdin.isTTY) {
|
|
1346
|
+
console.error(formatError({
|
|
1347
|
+
message: `"${environment}" looks like a production environment — --yes alone is not enough`,
|
|
1348
|
+
hint: "Re-run with --yes --confirm-prod to tear it down non-interactively.",
|
|
1349
|
+
}));
|
|
1350
|
+
return 1;
|
|
1351
|
+
}
|
|
1352
|
+
const confirmed = await promptProdTeardown(environment);
|
|
1353
|
+
if (!confirmed) {
|
|
1354
|
+
console.error(formatError({ message: "Confirmation did not match — nothing was deleted." }));
|
|
1355
|
+
return 1;
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
// #1166 — teardown is always a live read (and with --yes, a live write), so
|
|
1359
|
+
// an environment's declared endpoint applies here too, unless the ambient
|
|
1360
|
+
// shell already set it.
|
|
1361
|
+
const readingPlugins = plugins.filter((p) => p.teardownOwned || p.describeResources || p.executeTeardown);
|
|
1362
|
+
const endpointResult = applyLiveEndpoint(config.environments, environment, readingPlugins);
|
|
1363
|
+
if (endpointResult.notice)
|
|
1364
|
+
console.error(formatWarning({ message: endpointResult.notice }));
|
|
1365
|
+
let plan;
|
|
1366
|
+
let report;
|
|
1367
|
+
try {
|
|
1368
|
+
// A multi-stack project's declared stacks, for stack-shaped teardowns
|
|
1369
|
+
// (aws enumerates and deletes whole CloudFormation stacks). A single-stack
|
|
1370
|
+
// project passes nothing and the env-named default convention applies.
|
|
1371
|
+
const deployedStacks = (config.stacks ?? []).map((s) => ({
|
|
1372
|
+
name: s.name,
|
|
1373
|
+
...(s.region ? { region: s.region } : {}),
|
|
1374
|
+
}));
|
|
1375
|
+
plan = await planTeardown({
|
|
1376
|
+
environment,
|
|
1377
|
+
stack,
|
|
1378
|
+
plugins,
|
|
1379
|
+
...(deployedStacks.length > 0 ? { deployedStacks } : {}),
|
|
1380
|
+
});
|
|
1381
|
+
if (args.yes) {
|
|
1382
|
+
report = await executeTeardown({
|
|
1383
|
+
environment,
|
|
1384
|
+
stack,
|
|
1385
|
+
plugins,
|
|
1386
|
+
plan,
|
|
1387
|
+
...(deployedStacks.length > 0 ? { deployedStacks } : {}),
|
|
1388
|
+
});
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
finally {
|
|
1392
|
+
endpointResult.restore();
|
|
1393
|
+
}
|
|
1394
|
+
if (args.json) {
|
|
1395
|
+
console.log(JSON.stringify(report ?? plan, null, 2));
|
|
1396
|
+
return report && report.outcomes.some((o) => o.outcome === "failed") ? 1 : 0;
|
|
1397
|
+
}
|
|
1398
|
+
console.log(formatBold(args.yes
|
|
1399
|
+
? `Teardown — environment: ${environment}, stack: ${stack}`
|
|
1400
|
+
: `Teardown plan — environment: ${environment}, stack: ${stack} (plan only — nothing is deleted)`));
|
|
1401
|
+
if (plan.entries.length === 0) {
|
|
1402
|
+
console.error(formatWarning({
|
|
1403
|
+
message: `No live resources carry the marker stack "${stack}" + env "${environment}" — nothing would be deleted.` +
|
|
1404
|
+
(plan.holes.length > 0
|
|
1405
|
+
? " But this plan has holes (below) — parts of the estate could not be read, so \"nothing\" is a claim about what was readable, not about the environment."
|
|
1406
|
+
: " If this environment is deployed, check that its resources were stamped (ownership marking on, and the env identity resolved at build time)."),
|
|
1407
|
+
}));
|
|
1408
|
+
}
|
|
1409
|
+
else {
|
|
1410
|
+
console.log(`\n${plan.entries.length} resource(s) would be deleted:`);
|
|
1411
|
+
console.log("RESOURCE".padEnd(28) + "TYPE".padEnd(32) + "MARKER".padEnd(24) + "LEXICON");
|
|
1412
|
+
console.log("-".repeat(96));
|
|
1413
|
+
for (const entry of plan.entries) {
|
|
1414
|
+
console.log(entry.name.padEnd(28) +
|
|
1415
|
+
entry.type.padEnd(32) +
|
|
1416
|
+
`${entry.marker.stack}/${entry.marker.env}`.padEnd(24) +
|
|
1417
|
+
entry.lexicon);
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
// Holes are loud (#1089): an unreadable kind is unknown, not absent, and the
|
|
1421
|
+
// execution half must not treat this plan as the whole delete set.
|
|
1422
|
+
if (plan.holes.length > 0) {
|
|
1423
|
+
console.error(formatWarning({
|
|
1424
|
+
message: `${plan.holes.length} hole(s) — resources chant may own but could not read. This plan is incomplete, not clean.`,
|
|
1425
|
+
}));
|
|
1426
|
+
console.log(formatBold("\nHOLES (stamped kinds the read could not cover):"));
|
|
1427
|
+
for (const hole of plan.holes) {
|
|
1428
|
+
console.log(` ? ${hole.lexicon}: ${hole.name}${hole.type ? ` (${hole.type})` : ""} — ${hole.reason}${hole.detail ? `: ${hole.detail}` : ""}`);
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
if (plan.skipped.length > 0) {
|
|
1432
|
+
console.error(formatWarning({
|
|
1433
|
+
message: `Skipped (no teardown enumeration and no describeResources): ${plan.skipped.join(", ")} — those lexicons' resources are not in this plan.`,
|
|
1434
|
+
}));
|
|
1435
|
+
}
|
|
1436
|
+
if (!report) {
|
|
1437
|
+
console.error(formatWarning({ message: "Plan only — re-run with --yes to execute." }));
|
|
1438
|
+
return 0;
|
|
1439
|
+
}
|
|
1440
|
+
if (report.outcomes.length > 0) {
|
|
1441
|
+
console.log(formatBold("\nOutcomes:"));
|
|
1442
|
+
console.log("RESOURCE".padEnd(28) + "OUTCOME".padEnd(14) + "LEXICON".padEnd(12) + "DETAIL");
|
|
1443
|
+
console.log("-".repeat(96));
|
|
1444
|
+
for (const o of report.outcomes) {
|
|
1445
|
+
console.log(o.name.padEnd(28) +
|
|
1446
|
+
o.outcome.padEnd(14) +
|
|
1447
|
+
o.lexicon.padEnd(12) +
|
|
1448
|
+
(o.detail ?? "") +
|
|
1449
|
+
(o.retried ? " (after retry)" : ""));
|
|
1450
|
+
}
|
|
1451
|
+
}
|
|
1452
|
+
const counts = { deleted: 0, failed: 0, "not-prunable": 0, retained: 0, skipped: 0 };
|
|
1453
|
+
for (const o of report.outcomes)
|
|
1454
|
+
counts[o.outcome]++;
|
|
1455
|
+
console.log(`\n${counts.deleted} deleted, ${counts.failed} failed, ${counts["not-prunable"]} not prunable, ` +
|
|
1456
|
+
`${counts.retained} retained, ${counts.skipped} skipped`);
|
|
1457
|
+
if (report.unimplemented.length > 0) {
|
|
1458
|
+
console.error(formatWarning({
|
|
1459
|
+
message: `Not executed (no teardown execution in these lexicons yet): ${report.unimplemented.join(", ")} — their candidates are reported as skipped, not deleted.`,
|
|
1460
|
+
}));
|
|
1461
|
+
}
|
|
1462
|
+
if (counts.retained > 0) {
|
|
1463
|
+
console.error(formatWarning({
|
|
1464
|
+
message: `${counts.retained} resource(s) retained — owned by this env but deliberately kept (generated-once secrets are never swept). ` +
|
|
1465
|
+
`The environment is NOT clean while they exist; delete them explicitly (e.g. kubectl delete) if you mean to.`,
|
|
1466
|
+
}));
|
|
1467
|
+
}
|
|
1468
|
+
if (plan.holes.length > 0) {
|
|
1469
|
+
console.error(formatWarning({
|
|
1470
|
+
message: "This teardown ran over an incomplete plan (holes above) — the environment cannot be called clean.",
|
|
1471
|
+
}));
|
|
1472
|
+
}
|
|
1473
|
+
if (counts.failed > 0) {
|
|
1474
|
+
console.error(formatError({
|
|
1475
|
+
message: `${counts.failed} candidate(s) failed to delete after the retry pass — see the outcomes above.`,
|
|
1476
|
+
}));
|
|
1477
|
+
return 1;
|
|
1478
|
+
}
|
|
1479
|
+
return 0;
|
|
1480
|
+
}
|
|
1481
|
+
/**
|
|
1482
|
+
* The interactive half of the prod guard: the operator re-types the
|
|
1483
|
+
* environment name. Anything else — including EOF — refuses.
|
|
1484
|
+
*/
|
|
1485
|
+
async function promptProdTeardown(environment) {
|
|
1486
|
+
const { createInterface } = await import("node:readline");
|
|
1487
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
1488
|
+
return new Promise((resolvePrompt) => {
|
|
1489
|
+
rl.question(`"${environment}" looks like a production environment. Type the environment name to confirm teardown: `, (answer) => {
|
|
1490
|
+
resolvePrompt(answer.trim() === environment);
|
|
1491
|
+
rl.close();
|
|
1492
|
+
});
|
|
1493
|
+
// EOF (Ctrl-D) closes the interface without answering — that is a refusal.
|
|
1494
|
+
rl.on("close", () => resolvePrompt(false));
|
|
1495
|
+
});
|
|
1496
|
+
}
|
|
1497
|
+
/**
|
|
1498
|
+
* chant lifecycle log [environment]
|
|
1499
|
+
*/
|
|
1500
|
+
export async function runLifecycleLog(ctx) {
|
|
1501
|
+
const environment = ctx.args.extraPositional;
|
|
1502
|
+
await fetchLifecycle();
|
|
1503
|
+
const entries = await listSnapshots({ environment });
|
|
1504
|
+
if (entries.length === 0) {
|
|
1505
|
+
console.error(formatError({ message: "No state snapshots found" }));
|
|
1506
|
+
return 1;
|
|
1507
|
+
}
|
|
1508
|
+
for (const entry of entries) {
|
|
1509
|
+
const date = entry.date.split("T")[0];
|
|
1510
|
+
console.log(`${entry.commit.slice(0, 7)} ${date} ${entry.message}`);
|
|
1511
|
+
}
|
|
1512
|
+
return 0;
|
|
1513
|
+
}
|
|
1514
|
+
/**
|
|
1515
|
+
* The region core hands `describeIdentity` — the one the environment's stacks
|
|
1516
|
+
* declare, when they declare exactly one (#1261's `stacks[].region`).
|
|
1517
|
+
*
|
|
1518
|
+
* Stacks that declare none, or that disagree, resolve to `undefined`, and the
|
|
1519
|
+
* lexicon then resolves the region its own read path would. Guessing one of
|
|
1520
|
+
* several would report a binding half the estate does not use, which is the
|
|
1521
|
+
* failure this command exists to prevent.
|
|
1522
|
+
*/
|
|
1523
|
+
function singleDeclaredRegion(targets) {
|
|
1524
|
+
const regions = new Set(targets.map((t) => t.region).filter((r) => Boolean(r)));
|
|
1525
|
+
return regions.size === 1 ? [...regions][0] : undefined;
|
|
1526
|
+
}
|
|
1527
|
+
/**
|
|
1528
|
+
* chant lifecycle whoami <environment> [lexicon] [--json] [--strict]
|
|
1529
|
+
*
|
|
1530
|
+
* Read-only pre-flight (#1982): who chant would act as in each configured
|
|
1531
|
+
* lexicon, and what that principal is scoped to, before it acts. Never
|
|
1532
|
+
* mutates, never gates — `--strict` is the only way this exits non-zero on an
|
|
1533
|
+
* unresolved row, and a lexicon that answers for nothing is reported as such
|
|
1534
|
+
* rather than as an empty identity.
|
|
1535
|
+
*/
|
|
1536
|
+
export async function runLifecycleWhoami(ctx) {
|
|
1537
|
+
const { args, plugins } = ctx;
|
|
1538
|
+
const environment = args.extraPositional;
|
|
1539
|
+
const lexiconFilter = args.extraPositional2;
|
|
1540
|
+
if (!environment) {
|
|
1541
|
+
console.error(formatError({
|
|
1542
|
+
message: "Environment is required: chant lifecycle whoami <environment> [lexicon] [--json] [--strict]",
|
|
1543
|
+
}));
|
|
1544
|
+
return 1;
|
|
1545
|
+
}
|
|
1546
|
+
const projectPath = resolve(".");
|
|
1547
|
+
const { config } = await loadChantConfig(projectPath);
|
|
1548
|
+
const declaredEnvNames = environmentNames(config.environments);
|
|
1549
|
+
if (declaredEnvNames && !matchesDeclaredEnvironment(config.environments, environment)) {
|
|
1550
|
+
console.error(formatError({
|
|
1551
|
+
message: `Unknown environment "${environment}"`,
|
|
1552
|
+
hint: `Defined environments: ${declaredEnvNames.join(", ")}`,
|
|
1553
|
+
}));
|
|
1554
|
+
return 1;
|
|
1555
|
+
}
|
|
1556
|
+
const targetPlugins = lexiconFilter ? plugins.filter((p) => p.name === lexiconFilter) : plugins;
|
|
1557
|
+
if (targetPlugins.length === 0) {
|
|
1558
|
+
console.error(formatError({
|
|
1559
|
+
message: `Lexicon "${lexiconFilter}" is not configured for this project`,
|
|
1560
|
+
hint: `Configured: ${plugins.map((p) => p.name).join(", ") || "(none)"}`,
|
|
1561
|
+
}));
|
|
1562
|
+
return 1;
|
|
1563
|
+
}
|
|
1564
|
+
// The environment's declared endpoint applies here exactly as it does to a
|
|
1565
|
+
// `--live` read (#1166). Without it whoami would report the identity of real
|
|
1566
|
+
// AWS while `describeResources` reads the emulator — the wrong-target answer
|
|
1567
|
+
// this command exists to make visible.
|
|
1568
|
+
const endpointResult = applyLiveEndpoint(config.environments, environment, targetPlugins);
|
|
1569
|
+
if (endpointResult.notice)
|
|
1570
|
+
console.error(formatWarning({ message: endpointResult.notice }));
|
|
1571
|
+
const region = singleDeclaredRegion(resolveStackTargets(args, config));
|
|
1572
|
+
let rows;
|
|
1573
|
+
try {
|
|
1574
|
+
rows = await describeIdentities(targetPlugins, {
|
|
1575
|
+
environment,
|
|
1576
|
+
...(region ? { region } : {}),
|
|
1577
|
+
cwd: projectPath,
|
|
1578
|
+
});
|
|
1579
|
+
}
|
|
1580
|
+
finally {
|
|
1581
|
+
endpointResult.restore();
|
|
1582
|
+
}
|
|
1583
|
+
if (args.json) {
|
|
1584
|
+
console.log(JSON.stringify({ environment, identities: rows }, null, 2));
|
|
1585
|
+
}
|
|
1586
|
+
else {
|
|
1587
|
+
printIdentityTable(rows);
|
|
1588
|
+
}
|
|
1589
|
+
// Never a gate by default: an unresolved identity is a report, and a
|
|
1590
|
+
// pre-flight command that fails the build has stopped being pre-flight.
|
|
1591
|
+
const unresolved = rows.filter((r) => r.status === "unresolved");
|
|
1592
|
+
if (args.strict && unresolved.length > 0) {
|
|
1593
|
+
console.error(formatError({
|
|
1594
|
+
message: `--strict: ${unresolved.length} lexicon(s) could not resolve an identity: ${unresolved.map((r) => r.lexicon).join(", ")}`,
|
|
1595
|
+
}));
|
|
1596
|
+
return 1;
|
|
1597
|
+
}
|
|
1598
|
+
return 0;
|
|
1599
|
+
}
|
|
1600
|
+
function printIdentityTable(rows) {
|
|
1601
|
+
if (rows.length === 0) {
|
|
1602
|
+
console.error(formatWarning({ message: "No lexicons are configured for this project" }));
|
|
1603
|
+
return;
|
|
1604
|
+
}
|
|
1605
|
+
const width = (pick) => Math.max(...rows.map((r) => pick(r).length));
|
|
1606
|
+
const lexWidth = Math.max(width((r) => r.lexicon), "LEXICON".length);
|
|
1607
|
+
const idWidth = Math.max(width((r) => r.identity ?? identityStatusText(r)), "IDENTITY".length);
|
|
1608
|
+
const scopeWidth = Math.max(width((r) => r.scope ?? ""), "SCOPE".length);
|
|
1609
|
+
console.log("LEXICON".padEnd(lexWidth + 2) + "IDENTITY".padEnd(idWidth + 2) + "SCOPE".padEnd(scopeWidth + 2) + "SOURCE");
|
|
1610
|
+
for (const row of rows) {
|
|
1611
|
+
const identity = row.status === "reported" ? (row.identity ?? "") : identityStatusText(row);
|
|
1612
|
+
console.log(row.lexicon.padEnd(lexWidth + 2) +
|
|
1613
|
+
identity.padEnd(idWidth + 2) +
|
|
1614
|
+
(row.scope ?? "").padEnd(scopeWidth + 2) +
|
|
1615
|
+
(row.source ?? ""));
|
|
1616
|
+
}
|
|
1617
|
+
// The resolved address and the reason's detail are both sentences, so they
|
|
1618
|
+
// go under the table rather than widening a column every row pays for.
|
|
1619
|
+
for (const row of rows) {
|
|
1620
|
+
if (row.endpoint)
|
|
1621
|
+
console.error(` ${row.lexicon}: resolved endpoint ${row.endpoint}`);
|
|
1622
|
+
if (row.detail)
|
|
1623
|
+
console.error(` ${row.lexicon}: ${row.detail}`);
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
/**
|
|
1627
|
+
* Fallback for unknown state subcommands.
|
|
1628
|
+
*/
|
|
1629
|
+
export async function runLifecycleUnknown(ctx) {
|
|
1630
|
+
console.error(formatError({
|
|
1631
|
+
message: `Unknown state subcommand: ${ctx.args.extraPositional ?? ctx.args.path}`,
|
|
1632
|
+
hint: "Available: chant lifecycle snapshot, chant lifecycle show, chant lifecycle diff, chant lifecycle plan, chant lifecycle whoami, chant lifecycle teardown, chant lifecycle log",
|
|
1633
|
+
}));
|
|
1634
|
+
return 1;
|
|
1635
|
+
}
|
|
1636
|
+
function printSnapshotTable(snapshot) {
|
|
1637
|
+
console.log("RESOURCE".padEnd(20) + "TYPE".padEnd(28) + "PHYSICAL ID".padEnd(44) + "STATUS");
|
|
1638
|
+
console.log("-".repeat(100));
|
|
1639
|
+
for (const [name, meta] of Object.entries(snapshot.resources)) {
|
|
1640
|
+
const physicalId = meta.physicalId ?? "";
|
|
1641
|
+
const truncId = physicalId.length > 40 ? physicalId.slice(0, 37) + "..." : physicalId;
|
|
1642
|
+
console.log(name.padEnd(20) +
|
|
1643
|
+
meta.type.padEnd(28) +
|
|
1644
|
+
truncId.padEnd(44) +
|
|
1645
|
+
meta.status);
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
/**
|
|
1649
|
+
* chant lifecycle affected --base <ref> [--head <ref>] [--include-dependents] [--json]
|
|
1650
|
+
*
|
|
1651
|
+
* Read-only: report which stacks a change affects (directly-changed via artifact
|
|
1652
|
+
* diff, dependents via the cross-stack graph, external-input as indeterminate).
|
|
1653
|
+
* Returns the set; fanning plan/apply over it is an Op the user composes.
|
|
1654
|
+
*
|
|
1655
|
+
* `config.stacks` is passed through, so on a multi-stack project the answer is
|
|
1656
|
+
* keyed by deployed **stack name** — the name a component's `cfn-deploy` step
|
|
1657
|
+
* uses — instead of by lexicon partition. That is what lets `chant components
|
|
1658
|
+
* fan-out` join a change set to the components that deploy it; an aws-only
|
|
1659
|
+
* estate of fifteen stacks used to answer "aws" no matter which one moved. A
|
|
1660
|
+
* project with no `stacks` declared is untouched: one build of the source root,
|
|
1661
|
+
* one lexicon-keyed answer.
|
|
1662
|
+
*
|
|
1663
|
+
* The cost of per-stack mode is that `dependents` comes back empty (the build
|
|
1664
|
+
* carries cross-lexicon edges, not stack ones), so `--include-dependents` prints
|
|
1665
|
+
* a note saying where the downstream relation actually lives rather than showing
|
|
1666
|
+
* an empty list as if it meant "nothing downstream".
|
|
1667
|
+
*/
|
|
1668
|
+
export async function runLifecycleAffected(ctx) {
|
|
1669
|
+
const { args, plugins } = ctx;
|
|
1670
|
+
if (!args.base) {
|
|
1671
|
+
console.error(formatError({
|
|
1672
|
+
message: "Base ref is required: chant lifecycle affected --base <ref> [--head <ref>] [--include-dependents]",
|
|
1673
|
+
}));
|
|
1674
|
+
return 1;
|
|
1675
|
+
}
|
|
1676
|
+
const { config } = await loadChantConfig(resolve("."));
|
|
1677
|
+
// A multi-stack project's `stacks[].src` entries are written relative to the
|
|
1678
|
+
// project root, and they are themselves the scoping, so `--src`/`sourceDir`
|
|
1679
|
+
// does not narrow the root a second time (see `AffectedStacksOptions.stacks`).
|
|
1680
|
+
const projectPath = config.stacks?.length ? resolve(".") : resolveBuildRoot(args, config);
|
|
1681
|
+
let result;
|
|
1682
|
+
try {
|
|
1683
|
+
result = await affectedStacks({
|
|
1684
|
+
projectPath,
|
|
1685
|
+
serializers: plugins.map((p) => p.serializer),
|
|
1686
|
+
stacks: config.stacks,
|
|
1687
|
+
baseRef: args.base,
|
|
1688
|
+
headRef: args.head,
|
|
1689
|
+
includeDependents: args.includeDependents,
|
|
1690
|
+
});
|
|
1691
|
+
}
|
|
1692
|
+
catch (err) {
|
|
1693
|
+
console.error(formatError({ message: err instanceof Error ? err.message : String(err) }));
|
|
1694
|
+
return 1;
|
|
1695
|
+
}
|
|
1696
|
+
// Per-stack mode has no stack graph to walk, so an empty `dependents` here
|
|
1697
|
+
// means "not computed", not "nothing downstream". Say so on stderr — silently
|
|
1698
|
+
// handing back an empty list is the failure this command exists to avoid.
|
|
1699
|
+
const multiStack = (config.stacks?.length ?? 0) > 0;
|
|
1700
|
+
if (multiStack && args.includeDependents) {
|
|
1701
|
+
console.error(formatWarning({
|
|
1702
|
+
message: "Dependents are not computed for a multi-stack project: the downstream relation between deployed stacks is stated by the components' dependsOn, not by the build.",
|
|
1703
|
+
hint: "Run chant components fan-out to walk it.",
|
|
1704
|
+
}));
|
|
1705
|
+
}
|
|
1706
|
+
if (args.json) {
|
|
1707
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1708
|
+
return 0;
|
|
1709
|
+
}
|
|
1710
|
+
if (result.changed.length === 0 && result.dependents.length === 0) {
|
|
1711
|
+
console.error(formatSuccess("No stacks affected"));
|
|
1712
|
+
}
|
|
1713
|
+
else {
|
|
1714
|
+
console.log(formatBold("Directly changed:"));
|
|
1715
|
+
console.log(result.changed.length ? result.changed.map((s) => ` ${s}`).join("\n") : " (none)");
|
|
1716
|
+
if (args.includeDependents) {
|
|
1717
|
+
console.log(formatBold("\nDependents (consume a changed stack):"));
|
|
1718
|
+
console.log(result.dependents.length ? result.dependents.map((s) => ` ${s}`).join("\n") : " (none)");
|
|
1719
|
+
}
|
|
1720
|
+
}
|
|
1721
|
+
if (result.indeterminate.length > 0) {
|
|
1722
|
+
console.error(formatWarning({
|
|
1723
|
+
message: `External-input stacks — cannot confirm from source: ${result.indeterminate.join(", ")}`,
|
|
1724
|
+
}));
|
|
1725
|
+
}
|
|
1726
|
+
return 0;
|
|
1727
|
+
}
|