@intentius/chant 0.124.52 → 0.124.54
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/audit/discover.d.ts.map +1 -1
- package/dist/audit/discover.js +3 -1
- package/dist/behaviour.d.ts +1 -1
- package/dist/behaviour.d.ts.map +1 -1
- package/dist/capabilities/templates.d.ts +65 -0
- package/dist/capabilities/templates.d.ts.map +1 -0
- package/dist/capabilities/templates.js +193 -0
- package/dist/capabilities/templates.json +145 -0
- package/dist/change-set-graph.d.ts +35 -3
- package/dist/change-set-graph.d.ts.map +1 -1
- package/dist/change-set-graph.js +60 -9
- package/dist/change-set.d.ts +65 -7
- package/dist/change-set.d.ts.map +1 -1
- package/dist/change-set.js +23 -12
- package/dist/ci-identity.d.ts +7 -2
- package/dist/ci-identity.d.ts.map +1 -1
- package/dist/ci-identity.js +4 -1
- package/dist/cli/commands/import-index.d.ts +52 -0
- package/dist/cli/commands/import-index.d.ts.map +1 -0
- package/dist/cli/commands/import-index.js +226 -0
- package/dist/cli/commands/lint.d.ts.map +1 -1
- package/dist/cli/commands/lint.js +4 -2
- package/dist/cli/handlers/components.d.ts.map +1 -1
- package/dist/cli/handlers/components.js +11 -2
- package/dist/cli/handlers/fan-out.d.ts.map +1 -1
- package/dist/cli/handlers/fan-out.js +16 -3
- package/dist/cli/handlers/graph.d.ts.map +1 -1
- package/dist/cli/handlers/graph.js +22 -4
- package/dist/cli/handlers/index-cloud.d.ts +14 -0
- package/dist/cli/handlers/index-cloud.d.ts.map +1 -0
- package/dist/cli/handlers/index-cloud.js +22 -0
- package/dist/cli/handlers/index-diff.d.ts +18 -0
- package/dist/cli/handlers/index-diff.d.ts.map +1 -0
- package/dist/cli/handlers/index-diff.js +142 -0
- package/dist/cli/handlers/lifecycle-drift.d.ts +25 -0
- package/dist/cli/handlers/lifecycle-drift.d.ts.map +1 -0
- package/dist/cli/handlers/lifecycle-drift.js +64 -0
- package/dist/cli/handlers/lifecycle.d.ts.map +1 -1
- package/dist/cli/handlers/lifecycle.js +188 -28
- package/dist/cli/handlers/misc.d.ts +9 -0
- package/dist/cli/handlers/misc.d.ts.map +1 -1
- package/dist/cli/handlers/misc.js +77 -0
- package/dist/cli/handlers/pr.d.ts.map +1 -1
- package/dist/cli/handlers/pr.js +15 -2
- package/dist/cli/handlers/promote.d.ts.map +1 -1
- package/dist/cli/handlers/promote.js +89 -15
- package/dist/cli/handlers/query.d.ts.map +1 -1
- package/dist/cli/handlers/query.js +28 -4
- package/dist/cli/handlers/scenario.d.ts.map +1 -1
- package/dist/cli/handlers/scenario.js +8 -7
- package/dist/cli/handlers/search.d.ts +34 -4
- package/dist/cli/handlers/search.d.ts.map +1 -1
- package/dist/cli/handlers/search.js +216 -36
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/main.js +55 -1
- package/dist/cli/mcp/lifecycle-tools.d.ts.map +1 -1
- package/dist/cli/mcp/lifecycle-tools.js +3 -2
- package/dist/cli/mcp/resource-handlers.d.ts.map +1 -1
- package/dist/cli/mcp/resource-handlers.js +4 -3
- package/dist/cli/mcp/workspace-tools.js +1 -1
- package/dist/cli/parse-args.d.ts.map +1 -1
- package/dist/cli/parse-args.js +55 -2
- package/dist/cli/registry.d.ts +19 -1
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/cloud-lister/aws.d.ts +35 -0
- package/dist/cloud-lister/aws.d.ts.map +1 -0
- package/dist/cloud-lister/aws.js +203 -0
- package/dist/cloud-lister/azure.d.ts +34 -0
- package/dist/cloud-lister/azure.d.ts.map +1 -0
- package/dist/cloud-lister/azure.js +129 -0
- package/dist/cloud-lister/common.d.ts +40 -0
- package/dist/cloud-lister/common.d.ts.map +1 -0
- package/dist/cloud-lister/common.js +83 -0
- package/dist/cloud-lister/credentials.d.ts +50 -0
- package/dist/cloud-lister/credentials.d.ts.map +1 -0
- package/dist/cloud-lister/credentials.js +86 -0
- package/dist/cloud-lister/fakes.d.ts +27 -0
- package/dist/cloud-lister/fakes.d.ts.map +1 -0
- package/dist/cloud-lister/fakes.js +50 -0
- package/dist/cloud-lister/gcp.d.ts +42 -0
- package/dist/cloud-lister/gcp.d.ts.map +1 -0
- package/dist/cloud-lister/gcp.js +143 -0
- package/dist/cloud-lister/guard.d.ts +49 -0
- package/dist/cloud-lister/guard.d.ts.map +1 -0
- package/dist/cloud-lister/guard.js +103 -0
- package/dist/cloud-lister/ids.d.ts +23 -0
- package/dist/cloud-lister/ids.d.ts.map +1 -0
- package/dist/cloud-lister/ids.js +42 -0
- package/dist/cloud-lister/index.d.ts +67 -0
- package/dist/cloud-lister/index.d.ts.map +1 -0
- package/dist/cloud-lister/index.js +102 -0
- package/dist/cloud-lister/kubernetes.d.ts +64 -0
- package/dist/cloud-lister/kubernetes.d.ts.map +1 -0
- package/dist/cloud-lister/kubernetes.js +276 -0
- package/dist/cloud-lister/markers.d.ts +31 -0
- package/dist/cloud-lister/markers.d.ts.map +1 -0
- package/dist/cloud-lister/markers.js +73 -0
- package/dist/cloud-lister/mask.d.ts +31 -0
- package/dist/cloud-lister/mask.d.ts.map +1 -0
- package/dist/cloud-lister/mask.js +74 -0
- package/dist/cloud-lister/sigv4.d.ts +29 -0
- package/dist/cloud-lister/sigv4.d.ts.map +1 -0
- package/dist/cloud-lister/sigv4.js +54 -0
- package/dist/cloud-lister/transport.d.ts +55 -0
- package/dist/cloud-lister/transport.d.ts.map +1 -0
- package/dist/cloud-lister/transport.js +86 -0
- package/dist/cloud-lister/types.d.ts +121 -0
- package/dist/cloud-lister/types.d.ts.map +1 -0
- package/dist/cloud-lister/types.js +24 -0
- package/dist/components/capability-plugin-loader.d.ts.map +1 -1
- package/dist/components/capability-plugin-loader.js +6 -0
- package/dist/components/capability-plugin.d.ts +9 -0
- package/dist/components/capability-plugin.d.ts.map +1 -1
- package/dist/components/capability.d.ts +215 -0
- package/dist/components/capability.d.ts.map +1 -1
- package/dist/components/cli-support.d.ts +21 -0
- package/dist/components/cli-support.d.ts.map +1 -1
- package/dist/components/cli-support.js +58 -14
- package/dist/components/declared-reads.d.ts +38 -0
- package/dist/components/declared-reads.d.ts.map +1 -0
- package/dist/components/declared-reads.js +131 -0
- package/dist/components/deploy-units.d.ts.map +1 -1
- package/dist/components/deploy-units.js +4 -0
- package/dist/components/driver.d.ts +34 -1
- package/dist/components/driver.d.ts.map +1 -1
- package/dist/components/driver.js +66 -3
- package/dist/components/fan-out-run.d.ts +23 -1
- package/dist/components/fan-out-run.d.ts.map +1 -1
- package/dist/components/fan-out-run.js +82 -14
- package/dist/components/fan-out-support.d.ts +6 -0
- package/dist/components/fan-out-support.d.ts.map +1 -1
- package/dist/components/fan-out-support.js +4 -1
- package/dist/components/fan-out.d.ts +28 -0
- package/dist/components/fan-out.d.ts.map +1 -1
- package/dist/components/groups.d.ts +229 -0
- package/dist/components/groups.d.ts.map +1 -0
- package/dist/components/groups.js +760 -0
- package/dist/components/index.d.ts +6 -2
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +4 -0
- package/dist/components/plan-shape-conformance.d.ts +48 -0
- package/dist/components/plan-shape-conformance.d.ts.map +1 -0
- package/dist/components/plan-shape-conformance.js +122 -0
- package/dist/components/pr-run.d.ts +41 -4
- package/dist/components/pr-run.d.ts.map +1 -1
- package/dist/components/pr-run.js +209 -39
- package/dist/components/promote-rounds.d.ts +9 -0
- package/dist/components/promote-rounds.d.ts.map +1 -1
- package/dist/components/promote-rounds.js +23 -2
- package/dist/components/promote.d.ts +17 -0
- package/dist/components/promote.d.ts.map +1 -1
- package/dist/components/promote.js +36 -6
- package/dist/components/release-gate.d.ts +23 -1
- package/dist/components/release-gate.d.ts.map +1 -1
- package/dist/components/release-gate.js +97 -33
- package/dist/components/run-groups.d.ts +58 -0
- package/dist/components/run-groups.d.ts.map +1 -0
- package/dist/components/run-groups.js +78 -0
- package/dist/components/starter-plugin.d.ts +1 -0
- package/dist/components/starter-plugin.d.ts.map +1 -1
- package/dist/components/starter-plugin.js +6 -1
- package/dist/components/verbs/index.d.ts +1 -0
- package/dist/components/verbs/index.d.ts.map +1 -1
- package/dist/components/verbs/index.js +1 -0
- package/dist/components/verbs/member-outputs.d.ts +187 -0
- package/dist/components/verbs/member-outputs.d.ts.map +1 -0
- package/dist/components/verbs/member-outputs.js +218 -0
- package/dist/config.d.ts +60 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +27 -0
- package/dist/deep-observation.d.ts +1 -13
- package/dist/deep-observation.d.ts.map +1 -1
- package/dist/deep-observation.js +5 -22
- package/dist/drift-record.d.ts +542 -0
- package/dist/drift-record.d.ts.map +1 -0
- package/dist/drift-record.js +886 -0
- package/dist/engine-facts.d.ts +144 -0
- package/dist/engine-facts.d.ts.map +1 -0
- package/dist/engine-facts.js +14 -0
- package/dist/graph-ir.d.ts +153 -2
- package/dist/graph-ir.d.ts.map +1 -1
- package/dist/graph-ir.js +224 -6
- package/dist/graph-refs.d.ts +145 -0
- package/dist/graph-refs.d.ts.map +1 -1
- package/dist/graph-refs.js +305 -0
- package/dist/graph-schema/fixtures/adopt.json +15 -0
- package/dist/graph-schema/fixtures/choudoufu.engine-graph.json +169 -0
- package/dist/graph-schema/fixtures/index.json +68 -0
- package/dist/graph-schema/fixtures/plunge.engine-graph.json +158 -0
- package/dist/graph-schema/fixtures.d.ts +23 -0
- package/dist/graph-schema/fixtures.d.ts.map +1 -0
- package/dist/graph-schema/fixtures.js +246 -0
- package/dist/graph-schema/generate.d.ts +15 -0
- package/dist/graph-schema/generate.d.ts.map +1 -0
- package/dist/graph-schema/generate.js +267 -0
- package/dist/graph-schema/graph.schema.json +1387 -0
- package/dist/health.schema.json +2 -1
- package/dist/identity.d.ts.map +1 -1
- package/dist/identity.js +2 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/instance-nodes.d.ts +159 -0
- package/dist/instance-nodes.d.ts.map +1 -0
- package/dist/instance-nodes.js +231 -0
- package/dist/lexicon.d.ts +72 -0
- package/dist/lexicon.d.ts.map +1 -1
- package/dist/lifecycle/adoption-file.d.ts +56 -0
- package/dist/lifecycle/adoption-file.d.ts.map +1 -0
- package/dist/lifecycle/adoption-file.js +123 -0
- package/dist/lifecycle/cloud-index.d.ts +186 -0
- package/dist/lifecycle/cloud-index.d.ts.map +1 -0
- package/dist/lifecycle/cloud-index.js +618 -0
- package/dist/lifecycle/derive-edges.d.ts +59 -0
- package/dist/lifecycle/derive-edges.d.ts.map +1 -0
- package/dist/lifecycle/derive-edges.js +126 -0
- package/dist/lifecycle/git.d.ts +10 -9
- package/dist/lifecycle/git.d.ts.map +1 -1
- package/dist/lifecycle/git.js +7 -1
- package/dist/lifecycle/identity-match.d.ts +133 -0
- package/dist/lifecycle/identity-match.d.ts.map +1 -0
- package/dist/lifecycle/identity-match.js +147 -0
- package/dist/lifecycle/index-diff.d.ts +224 -0
- package/dist/lifecycle/index-diff.d.ts.map +1 -0
- package/dist/lifecycle/index-diff.js +327 -0
- package/dist/lifecycle/index-document.d.ts +130 -0
- package/dist/lifecycle/index-document.d.ts.map +1 -0
- package/dist/lifecycle/index-document.js +189 -0
- package/dist/lifecycle/index-drift.d.ts +121 -0
- package/dist/lifecycle/index-drift.d.ts.map +1 -0
- package/dist/lifecycle/index-drift.js +178 -0
- package/dist/lifecycle/index-import.d.ts +168 -0
- package/dist/lifecycle/index-import.d.ts.map +1 -0
- package/dist/lifecycle/index-import.js +515 -0
- package/dist/lifecycle/index-join.d.ts +129 -0
- package/dist/lifecycle/index-join.d.ts.map +1 -0
- package/dist/lifecycle/index-join.js +248 -0
- package/dist/lifecycle/index.d.ts +4 -0
- package/dist/lifecycle/index.d.ts.map +1 -1
- package/dist/lifecycle/index.js +4 -0
- package/dist/lifecycle/observation-baseline.d.ts +61 -21
- package/dist/lifecycle/observation-baseline.d.ts.map +1 -1
- package/dist/lifecycle/observation-baseline.js +144 -19
- package/dist/lifecycle/observe.d.ts.map +1 -1
- package/dist/lifecycle/observe.js +6 -1
- package/dist/lifecycle/query.d.ts +45 -4
- package/dist/lifecycle/query.d.ts.map +1 -1
- package/dist/lifecycle/query.js +89 -7
- package/dist/lifecycle/replay.d.ts +10 -3
- package/dist/lifecycle/replay.d.ts.map +1 -1
- package/dist/lifecycle/replay.js +10 -6
- package/dist/lifecycle/scenario.d.ts +4 -3
- package/dist/lifecycle/scenario.d.ts.map +1 -1
- package/dist/lifecycle/snapshot-store-fake.d.ts +9 -0
- package/dist/lifecycle/snapshot-store-fake.d.ts.map +1 -0
- package/dist/lifecycle/snapshot-store-fake.js +21 -0
- package/dist/lifecycle/snapshot-store-s3.d.ts +102 -0
- package/dist/lifecycle/snapshot-store-s3.d.ts.map +1 -0
- package/dist/lifecycle/snapshot-store-s3.js +254 -0
- package/dist/lifecycle/snapshot-store.d.ts +170 -0
- package/dist/lifecycle/snapshot-store.d.ts.map +1 -0
- package/dist/lifecycle/snapshot-store.js +832 -0
- package/dist/lifecycle/snapshot.d.ts +11 -0
- package/dist/lifecycle/snapshot.d.ts.map +1 -1
- package/dist/lifecycle/snapshot.js +18 -5
- package/dist/lifecycle/type-listing.d.ts +148 -0
- package/dist/lifecycle/type-listing.d.ts.map +1 -0
- package/dist/lifecycle/type-listing.js +166 -0
- package/dist/lint/component-checks.d.ts +15 -1
- package/dist/lint/component-checks.d.ts.map +1 -1
- package/dist/lint/component-checks.js +1 -0
- package/dist/lint/presets/relaxed.json +2 -1
- package/dist/lint/presets/strict.json +2 -1
- package/dist/lint/rules/__fixtures__/comp/comp008/fail/dns.component.d.ts +4 -0
- package/dist/lint/rules/__fixtures__/comp/comp008/fail/dns.component.d.ts.map +1 -0
- package/dist/lint/rules/__fixtures__/comp/comp008/fail/dns.component.js +13 -0
- package/dist/lint/rules/__fixtures__/comp/comp008/pass/dns.component.d.ts +4 -0
- package/dist/lint/rules/__fixtures__/comp/comp008/pass/dns.component.d.ts.map +1 -0
- package/dist/lint/rules/__fixtures__/comp/comp008/pass/dns.component.js +8 -0
- package/dist/lint/rules/comp/comp008-applies-signal-only-root.d.ts +33 -0
- package/dist/lint/rules/comp/comp008-applies-signal-only-root.d.ts.map +1 -0
- package/dist/lint/rules/comp/comp008-applies-signal-only-root.js +123 -0
- package/dist/lint/rules/comp/index.d.ts +2 -1
- package/dist/lint/rules/comp/index.d.ts.map +1 -1
- package/dist/lint/rules/comp/index.js +4 -1
- package/dist/observation.d.ts +19 -2
- package/dist/observation.d.ts.map +1 -1
- package/dist/observation.js +18 -1
- package/dist/op/activities/activity-contracts.d.ts +1 -1
- package/dist/op/activities/activity-contracts.js +1 -1
- package/dist/op/activities/change-set.d.ts +6 -4
- package/dist/op/activities/change-set.d.ts.map +1 -1
- package/dist/op/activities/change-set.js +4 -2
- package/dist/op/activity-failure.d.ts +27 -0
- package/dist/op/activity-failure.d.ts.map +1 -1
- package/dist/op/activity-failure.js +31 -0
- package/dist/op/index.d.ts +2 -1
- package/dist/op/index.d.ts.map +1 -1
- package/dist/op/index.js +1 -1
- package/dist/op/local-executor.d.ts +5 -1
- package/dist/op/local-executor.d.ts.map +1 -1
- package/dist/op/local-executor.js +22 -8
- package/dist/pr-loop.d.ts +56 -1
- package/dist/pr-loop.d.ts.map +1 -1
- package/dist/pr-loop.js +51 -1
- package/dist/pulumi-urn.d.ts +101 -0
- package/dist/pulumi-urn.d.ts.map +1 -0
- package/dist/pulumi-urn.js +131 -0
- package/dist/release-floors.d.ts +23 -0
- package/dist/release-floors.d.ts.map +1 -0
- package/dist/release-floors.js +22 -0
- package/dist/sensitive-key.d.ts +23 -0
- package/dist/sensitive-key.d.ts.map +1 -0
- package/dist/sensitive-key.js +31 -0
- package/dist/wave-runtime.mjs +234 -234
- package/dist/workspace/adoption-report.d.ts +53 -0
- package/dist/workspace/adoption-report.d.ts.map +1 -0
- package/dist/workspace/adoption-report.js +76 -0
- package/dist/workspace/capabilities-contract.d.ts +7 -0
- package/dist/workspace/capabilities-contract.d.ts.map +1 -1
- package/dist/workspace/capabilities-contract.js +2 -1
- package/dist/workspace/capabilities.schema.json +98 -0
- package/dist/workspace/change-set.schema.json +42 -3
- package/dist/workspace/ci-cloud-identity.d.ts +13 -1
- package/dist/workspace/ci-cloud-identity.d.ts.map +1 -1
- package/dist/workspace/ci-cloud-identity.js +30 -1
- package/dist/workspace/compose-graph.d.ts +10 -1
- package/dist/workspace/compose-graph.d.ts.map +1 -1
- package/dist/workspace/compose-graph.js +6 -1
- package/dist/workspace/conformance/index.d.ts +1 -1
- package/dist/workspace/conformance/index.d.ts.map +1 -1
- package/dist/workspace/conformance/index.js +6 -1
- package/dist/workspace/drift-cli.d.ts +81 -0
- package/dist/workspace/drift-cli.d.ts.map +1 -0
- package/dist/workspace/drift-cli.js +317 -0
- package/dist/workspace/drift.schema.json +672 -0
- package/dist/workspace/graph-cli.d.ts +2 -0
- package/dist/workspace/graph-cli.d.ts.map +1 -1
- package/dist/workspace/graph-cli.js +3 -0
- package/dist/workspace/graph.schema.json +72 -0
- package/dist/workspace/init.d.ts +5 -1
- package/dist/workspace/init.d.ts.map +1 -1
- package/dist/workspace/init.js +55 -9
- package/dist/workspace/ledger.schema.json +1 -1
- package/dist/workspace/nested-graph.d.ts.map +1 -1
- package/dist/workspace/nested-graph.js +2 -0
- package/dist/workspace/pr-report.schema.json +51 -2
- package/dist/workspace/promote-waves.d.ts +112 -0
- package/dist/workspace/promote-waves.d.ts.map +1 -0
- package/dist/workspace/promote-waves.js +290 -0
- package/dist/workspace/promote.d.ts +84 -2
- package/dist/workspace/promote.d.ts.map +1 -1
- package/dist/workspace/promote.js +347 -61
- package/dist/workspace/reason-codes.d.ts +12 -0
- package/dist/workspace/reason-codes.d.ts.map +1 -1
- package/dist/workspace/reason-codes.js +14 -0
- package/dist/workspace/search-contract.d.ts +3 -3
- package/dist/workspace/search-contract.d.ts.map +1 -1
- package/dist/workspace/search-contract.js +2 -0
- package/dist/workspace/search.schema.json +103 -5
- package/package.json +14 -1
- package/src/__fixtures__/drift-record/README.md +58 -0
- package/src/__fixtures__/drift-record/choudoufu-cone.ndjson +4 -0
- package/src/__fixtures__/drift-record/choudoufu-resource.ndjson +3 -0
- package/src/__fixtures__/drift-record/component-status-ledger.json +65 -0
- package/src/__fixtures__/drift-record/component-status.json +91 -0
- package/src/__fixtures__/drift-record/deep-diff.json +259 -0
- package/src/__fixtures__/drift-record/lifecycle-diff-live.json +825 -0
- package/src/__fixtures__/drift-record/live-diff.json +125 -0
- package/src/audit/discover.e2e.test.ts +14 -0
- package/src/audit/discover.ts +3 -1
- package/src/behaviour.ts +1 -1
- package/src/capabilities/templates.json +145 -0
- package/src/capabilities/templates.test.ts +212 -0
- package/src/capabilities/templates.ts +204 -0
- package/src/change-set-bundle.test.ts +83 -0
- package/src/change-set-graph.ts +80 -9
- package/src/change-set.test.ts +24 -0
- package/src/change-set.ts +77 -14
- package/src/ci-identity-targets.test.ts +23 -0
- package/src/ci-identity.ts +10 -4
- package/src/cli/commands/import-index.test.ts +117 -0
- package/src/cli/commands/import-index.ts +265 -0
- package/src/cli/commands/lint.ts +4 -2
- package/src/cli/handlers/components.test.ts +9 -0
- package/src/cli/handlers/components.ts +12 -2
- package/src/cli/handlers/fan-out.ts +17 -3
- package/src/cli/handlers/graph.test.ts +42 -0
- package/src/cli/handlers/graph.ts +26 -4
- package/src/cli/handlers/index-cloud.ts +32 -0
- package/src/cli/handlers/index-diff.test.ts +68 -0
- package/src/cli/handlers/index-diff.ts +146 -0
- package/src/cli/handlers/ledger-root-member.test.ts +161 -0
- package/src/cli/handlers/lifecycle-drift.ts +68 -0
- package/src/cli/handlers/lifecycle.test.ts +33 -11
- package/src/cli/handlers/lifecycle.ts +205 -30
- package/src/cli/handlers/misc.ts +73 -0
- package/src/cli/handlers/pr-waves-blocked.test.ts +37 -2
- package/src/cli/handlers/pr.ts +14 -2
- package/src/cli/handlers/promote.test.ts +64 -0
- package/src/cli/handlers/promote.ts +81 -6
- package/src/cli/handlers/query.ts +27 -4
- package/src/cli/handlers/run-declared-reads-cli.test.ts +187 -0
- package/src/cli/handlers/run-groups-cli.test.ts +229 -0
- package/src/cli/handlers/scenario.test.ts +13 -0
- package/src/cli/handlers/scenario.ts +8 -7
- package/src/cli/handlers/search-index.test.ts +255 -0
- package/src/cli/handlers/search-json.test.ts +1 -1
- package/src/cli/handlers/search-ownership.test.ts +194 -0
- package/src/cli/handlers/search.ts +271 -38
- package/src/cli/main.test.ts +7 -0
- package/src/cli/main.ts +56 -1
- package/src/cli/mcp/lifecycle-tools.ts +3 -2
- package/src/cli/mcp/resource-handlers.ts +4 -3
- package/src/cli/mcp/workspace-tools.ts +1 -1
- package/src/cli/parse-args.ts +40 -2
- package/src/cli/registry.ts +19 -1
- package/src/cloud-lister/aws.test.ts +202 -0
- package/src/cloud-lister/aws.ts +226 -0
- package/src/cloud-lister/azure.test.ts +132 -0
- package/src/cloud-lister/azure.ts +142 -0
- package/src/cloud-lister/common.ts +115 -0
- package/src/cloud-lister/credentials.ts +110 -0
- package/src/cloud-lister/fakes.ts +66 -0
- package/src/cloud-lister/gcp.test.ts +121 -0
- package/src/cloud-lister/gcp.ts +159 -0
- package/src/cloud-lister/guard.test.ts +198 -0
- package/src/cloud-lister/guard.ts +111 -0
- package/src/cloud-lister/ids.ts +54 -0
- package/src/cloud-lister/index.ts +134 -0
- package/src/cloud-lister/kubernetes.test.ts +160 -0
- package/src/cloud-lister/kubernetes.ts +284 -0
- package/src/cloud-lister/markers.ts +74 -0
- package/src/cloud-lister/mask.ts +77 -0
- package/src/cloud-lister/shape.test.ts +81 -0
- package/src/cloud-lister/sigv4.ts +70 -0
- package/src/cloud-lister/transport.ts +118 -0
- package/src/cloud-lister/types.ts +135 -0
- package/src/components/capability-plugin-loader.ts +4 -0
- package/src/components/capability-plugin.ts +10 -0
- package/src/components/capability.ts +229 -0
- package/src/components/cli-support.ts +70 -14
- package/src/components/declared-reads.test.ts +103 -0
- package/src/components/declared-reads.ts +134 -0
- package/src/components/deploy-units.ts +4 -0
- package/src/components/driver.ts +89 -4
- package/src/components/fan-out-run.ts +95 -15
- package/src/components/fan-out-support.ts +10 -1
- package/src/components/fan-out.ts +30 -0
- package/src/components/groups.test.ts +320 -0
- package/src/components/groups.ts +880 -0
- package/src/components/index.ts +42 -0
- package/src/components/plan-shape-conformance.ts +130 -0
- package/src/components/pr-env-run-bundle.test.ts +2 -0
- package/src/components/pr-run.test.ts +516 -3
- package/src/components/pr-run.ts +231 -41
- package/src/components/promote-rounds.ts +25 -2
- package/src/components/promote.ts +55 -5
- package/src/components/registry.test.ts +1 -1
- package/src/components/release-gate.ts +115 -30
- package/src/components/release-groups.test.ts +362 -0
- package/src/components/run-groups.ts +113 -0
- package/src/components/starter-plugin.ts +6 -0
- package/src/components/verbs/index.ts +1 -0
- package/src/components/verbs/member-outputs.test.ts +205 -0
- package/src/components/verbs/member-outputs.ts +308 -0
- package/src/config.ts +44 -0
- package/src/deep-observation.ts +5 -23
- package/src/drift-record.test.ts +596 -0
- package/src/drift-record.ts +1206 -0
- package/src/engine-facts.guard.test.ts +111 -0
- package/src/engine-facts.test.ts +178 -0
- package/src/engine-facts.ts +154 -0
- package/src/gated-waves-bundle.test.ts +2 -0
- package/src/graph-ir-live.test.ts +92 -0
- package/src/graph-ir.ts +317 -7
- package/src/graph-refs.ts +416 -0
- package/src/graph-schema/fixtures/adopt.json +15 -0
- package/src/graph-schema/fixtures/choudoufu.engine-graph.json +169 -0
- package/src/graph-schema/fixtures/index.json +68 -0
- package/src/graph-schema/fixtures/plunge.engine-graph.json +158 -0
- package/src/graph-schema/fixtures.ts +267 -0
- package/src/graph-schema/generate.ts +263 -0
- package/src/graph-schema/graph-schema.test.ts +153 -0
- package/src/graph-schema/graph.schema.json +1387 -0
- package/src/health.schema.json +2 -1
- package/src/identity.ts +2 -0
- package/src/index.ts +2 -0
- package/src/instance-nodes.ts +288 -0
- package/src/lexicon.ts +76 -0
- package/src/lifecycle/__fixtures__/index-diff/20261026T120000Z-aaaaaaaaaaaa.json +149 -0
- package/src/lifecycle/__fixtures__/index-diff/20261026T130000Z-bbbbbbbbbbbb.json +127 -0
- package/src/lifecycle/adoption-file.test.ts +100 -0
- package/src/lifecycle/adoption-file.ts +154 -0
- package/src/lifecycle/cloud-index.test.ts +517 -0
- package/src/lifecycle/cloud-index.ts +736 -0
- package/src/lifecycle/derive-edges.test.ts +193 -0
- package/src/lifecycle/derive-edges.ts +182 -0
- package/src/lifecycle/git.ts +10 -4
- package/src/lifecycle/identity-match.test.ts +308 -0
- package/src/lifecycle/identity-match.ts +257 -0
- package/src/lifecycle/index-diff.test.ts +198 -0
- package/src/lifecycle/index-diff.ts +476 -0
- package/src/lifecycle/index-document.test.ts +64 -0
- package/src/lifecycle/index-document.ts +299 -0
- package/src/lifecycle/index-drift.test.ts +191 -0
- package/src/lifecycle/index-drift.ts +304 -0
- package/src/lifecycle/index-import.test.ts +255 -0
- package/src/lifecycle/index-import.ts +634 -0
- package/src/lifecycle/index-join.test.ts +250 -0
- package/src/lifecycle/index-join.ts +304 -0
- package/src/lifecycle/index-store-load-bearing.test.ts +187 -0
- package/src/lifecycle/index.ts +4 -0
- package/src/lifecycle/member-ledger.test.ts +25 -7
- package/src/lifecycle/observation-baseline.test.ts +72 -1
- package/src/lifecycle/observation-baseline.ts +190 -27
- package/src/lifecycle/observe.ts +6 -1
- package/src/lifecycle/query.test.ts +69 -1
- package/src/lifecycle/query.ts +103 -10
- package/src/lifecycle/replay.test.ts +9 -5
- package/src/lifecycle/replay.ts +9 -5
- package/src/lifecycle/scenario.ts +4 -3
- package/src/lifecycle/snapshot-store-fake.ts +29 -0
- package/src/lifecycle/snapshot-store-s3.ts +316 -0
- package/src/lifecycle/snapshot-store.test.ts +385 -0
- package/src/lifecycle/snapshot-store.ts +981 -0
- package/src/lifecycle/snapshot.test.ts +13 -0
- package/src/lifecycle/snapshot.ts +23 -6
- package/src/lifecycle/type-listing.test.ts +166 -0
- package/src/lifecycle/type-listing.ts +309 -0
- package/src/lint/component-checks.ts +17 -1
- package/src/lint/config.test.ts +3 -0
- package/src/lint/presets/relaxed.json +2 -1
- package/src/lint/presets/strict.json +2 -1
- package/src/lint/rules/__fixtures__/comp/comp008/fail/dns.component.ts +15 -0
- package/src/lint/rules/__fixtures__/comp/comp008/pass/dns.component.ts +10 -0
- package/src/lint/rules/comp/comp.test.ts +2 -1
- package/src/lint/rules/comp/comp008-applies-signal-only-root.ts +121 -0
- package/src/lint/rules/comp/comp008.test.ts +77 -0
- package/src/lint/rules/comp/index.ts +4 -1
- package/src/observation-edges.test.ts +33 -0
- package/src/observation.ts +36 -1
- package/src/op/activities/activity-contracts.ts +1 -1
- package/src/op/activities/change-set.ts +8 -5
- package/src/op/activity-failure.ts +36 -0
- package/src/op/index.ts +2 -1
- package/src/op/local-executor.test.ts +80 -0
- package/src/op/local-executor.ts +27 -9
- package/src/pr-loop-bundle.test.ts +5 -2
- package/src/pr-loop.test.ts +43 -0
- package/src/pr-loop.ts +111 -2
- package/src/pulumi-urn.ts +151 -0
- package/src/release-floors.ts +22 -0
- package/src/sensitive-key.ts +34 -0
- package/src/workspace/adoption-report.test.ts +61 -0
- package/src/workspace/adoption-report.ts +103 -0
- package/src/workspace/capabilities-contract.test.ts +4 -0
- package/src/workspace/capabilities-contract.ts +9 -1
- package/src/workspace/capabilities.schema.json +98 -0
- package/src/workspace/change-set.schema.json +42 -3
- package/src/workspace/ci-cloud-identity.ts +31 -1
- package/src/workspace/compose-graph.test.ts +12 -0
- package/src/workspace/compose-graph.ts +12 -1
- package/src/workspace/conformance/conformance.e2e.test.ts +2 -1
- package/src/workspace/conformance/index.ts +6 -1
- package/src/workspace/drift-cli.test.ts +65 -0
- package/src/workspace/drift-cli.ts +365 -0
- package/src/workspace/drift.schema.json +672 -0
- package/src/workspace/graph-cli.ts +5 -0
- package/src/workspace/graph.schema.json +72 -0
- package/src/workspace/init.test.ts +38 -1
- package/src/workspace/init.ts +50 -8
- package/src/workspace/intent-gaps.test.ts +18 -2
- package/src/workspace/ledger.schema.json +1 -1
- package/src/workspace/nested-graph.ts +1 -0
- package/src/workspace/pr-report.schema.json +51 -2
- package/src/workspace/promote-waves.test.ts +296 -0
- package/src/workspace/promote-waves.ts +340 -0
- package/src/workspace/promote.test.ts +158 -4
- package/src/workspace/promote.ts +398 -57
- package/src/workspace/reason-codes.test.ts +4 -1
- package/src/workspace/reason-codes.ts +14 -0
- package/src/workspace/search-contract.ts +2 -0
- package/src/workspace/search.schema.json +103 -5
|
@@ -0,0 +1,886 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One drift record across engines (#4450, #4451).
|
|
3
|
+
*
|
|
4
|
+
* Drift had four vocabularies for one question: the converge observers'
|
|
5
|
+
* `in-sync | drifted | unknown` (`./lifecycle/symptoms.ts`), component
|
|
6
|
+
* status's `reconciled | unrecorded | stale | drifted | unknown`
|
|
7
|
+
* (`./lifecycle/status.ts`), the graph overlay's tones, and the live diff's
|
|
8
|
+
* buckets (`./lifecycle/live-diff.ts`). A renderer colouring a terraform, a
|
|
9
|
+
* choudoufu and a chant member side by side had to know all four, and behold
|
|
10
|
+
* read choudoufu's documents itself. This module is the one shape they all
|
|
11
|
+
* become: a {@link DriftRecord} per resource instance, and a
|
|
12
|
+
* {@link DriftDocument} (`drift: "v1"`) that holds them. Its schema is
|
|
13
|
+
* `./workspace/drift.schema.json`, `$id` {@link DRIFT_SCHEMA_ID}.
|
|
14
|
+
*
|
|
15
|
+
* A record says which read answered (`read`) and what it found (`status`).
|
|
16
|
+
* The two travel together because they are different claims: a `state` read
|
|
17
|
+
* says what the last apply recorded and can never say the account matches,
|
|
18
|
+
* so {@link driftRecord} refuses a state read whose status is anything but
|
|
19
|
+
* `recorded`, `pending` or `not-observed`. Every record goes through that
|
|
20
|
+
* constructor, so the rule holds for every adapter, and for a document read
|
|
21
|
+
* back with {@link parseDriftDocument}.
|
|
22
|
+
*
|
|
23
|
+
* `not-observed` is never absence. It always carries a reason from the
|
|
24
|
+
* observation contract's closed list (`./observation.ts`), and
|
|
25
|
+
* `not-reached` is the reason a stream gives an address it expected and
|
|
26
|
+
* never heard about ({@link driftSettle}).
|
|
27
|
+
*
|
|
28
|
+
* ## Redaction
|
|
29
|
+
*
|
|
30
|
+
* An attribute the planner marked sensitive keeps its path and loses both
|
|
31
|
+
* values (`sensitive: true`), as the change set spells it
|
|
32
|
+
* (`./change-set.ts`). A second pass redacts any path with a segment
|
|
33
|
+
* {@link isSensitiveKey} names, whatever the planner said, so an engine
|
|
34
|
+
* that marks nothing still never ships a `password` value. Values are
|
|
35
|
+
* optional: a card needs only the names.
|
|
36
|
+
*
|
|
37
|
+
* ## Not digest-bound
|
|
38
|
+
*
|
|
39
|
+
* A drift read authorizes nothing and goes stale on the next read, so a
|
|
40
|
+
* record is ephemeral. {@link driftDocumentDigest} exists for one case, a
|
|
41
|
+
* WatchOp writing its document into its own run ledger as evidence, and
|
|
42
|
+
* leaves out `observedAt` and `evidence`, which change on every read.
|
|
43
|
+
*
|
|
44
|
+
* ## Adapters
|
|
45
|
+
*
|
|
46
|
+
* The chant ones are here: `lifecycle diff --live` ({@link liveDiffDriftRecords}),
|
|
47
|
+
* deep observe ({@link deepDiffDriftRecords}, {@link mergeDeepDrift}) and
|
|
48
|
+
* component status ({@link componentStatusDriftRecords}). The terraform and
|
|
49
|
+
* choudoufu ones live in the terraform lexicon's `drift-record` subpath,
|
|
50
|
+
* beside its readers, and the Pulumi one (`refresh --preview-only`, #4453)
|
|
51
|
+
* in the pulumi lexicon's.
|
|
52
|
+
*
|
|
53
|
+
* ## The graph and the rollup
|
|
54
|
+
*
|
|
55
|
+
* A record's `node` and `instanceOf` come from the change set's own rule
|
|
56
|
+
* (`graphNodeFor`, #4454), and a block's status from core's one rollup
|
|
57
|
+
* (`rollupInstances`) over {@link DRIFT_INSTANCE_ROLLUP}. Neither is
|
|
58
|
+
* written a second time here.
|
|
59
|
+
*
|
|
60
|
+
* ## The import graph
|
|
61
|
+
*
|
|
62
|
+
* This module ships in the change-set bundle (`change-set-bundle.test.ts`,
|
|
63
|
+
* #3421): at run time it imports the plan digest, the observation
|
|
64
|
+
* contract's reason list, the change-set graph's node rule, the instance
|
|
65
|
+
* rollup and the sensitive-key list, and types only otherwise.
|
|
66
|
+
*/
|
|
67
|
+
import { graphNodeFor } from "./change-set-graph.js";
|
|
68
|
+
import { rollupInstances } from "./instance-nodes.js";
|
|
69
|
+
import { computePlanDigest } from "./lifecycle/plan-digest.js";
|
|
70
|
+
import { isUnobservedReason } from "./observation.js";
|
|
71
|
+
import { isSensitiveKey } from "./sensitive-key.js";
|
|
72
|
+
/** The schema a drift document names in `$schema`. */
|
|
73
|
+
export const DRIFT_SCHEMA_ID = "https://intentius.io/chant/schemas/workspace/drift/v1/drift.schema.json";
|
|
74
|
+
/** The document's version. */
|
|
75
|
+
export const DRIFT_VERSION = "v1";
|
|
76
|
+
/** The planner whose node rule places an engine's records: plunge's URNs go by `pulumi`'s (#4453). */
|
|
77
|
+
export function driftPlanner(engine) {
|
|
78
|
+
return engine === "plunge" ? "pulumi" : engine;
|
|
79
|
+
}
|
|
80
|
+
const DRIFT_ENGINE_WITNESS = {
|
|
81
|
+
terraform: true,
|
|
82
|
+
tofu: true,
|
|
83
|
+
choudoufu: true,
|
|
84
|
+
chant: true,
|
|
85
|
+
warden: true,
|
|
86
|
+
sql: true,
|
|
87
|
+
pulumi: true,
|
|
88
|
+
plunge: true,
|
|
89
|
+
};
|
|
90
|
+
/** Every engine, in the order the schema lists them. */
|
|
91
|
+
export const DRIFT_ENGINES = Object.keys(DRIFT_ENGINE_WITNESS);
|
|
92
|
+
const DRIFT_READ_WITNESS = { state: true, plan: true, refresh: true, "live-read": true, ownership: true };
|
|
93
|
+
/** Every read, in the order the schema lists them. */
|
|
94
|
+
export const DRIFT_READS = Object.keys(DRIFT_READ_WITNESS);
|
|
95
|
+
const DRIFT_STATUS_WITNESS = {
|
|
96
|
+
"in-sync": true,
|
|
97
|
+
drifted: true,
|
|
98
|
+
pending: true,
|
|
99
|
+
missing: true,
|
|
100
|
+
unclaimed: true,
|
|
101
|
+
undeclared: true,
|
|
102
|
+
runtime: true,
|
|
103
|
+
recorded: true,
|
|
104
|
+
"not-observed": true,
|
|
105
|
+
};
|
|
106
|
+
/** Every status, in the order the schema lists them. */
|
|
107
|
+
export const DRIFT_STATUSES = Object.keys(DRIFT_STATUS_WITNESS);
|
|
108
|
+
/** The only statuses a `state` read may give. It never says in-sync or drifted. */
|
|
109
|
+
export const STATE_READ_STATUSES = ["recorded", "pending", "not-observed"];
|
|
110
|
+
const OWNERSHIP_VERDICT_WITNESS = { owned: true, foreign: true, adoptable: true, unknown: true };
|
|
111
|
+
/** Every ownership verdict. */
|
|
112
|
+
export const DRIFT_OWNERSHIP_VERDICTS = Object.keys(OWNERSHIP_VERDICT_WITNESS);
|
|
113
|
+
const OWNERSHIP_BY_WITNESS = { marker: true, derived: true, record: true, cache: true, state: true, label: true };
|
|
114
|
+
/** Every ownership basis. */
|
|
115
|
+
export const DRIFT_OWNERSHIP_BY = Object.keys(OWNERSHIP_BY_WITNESS);
|
|
116
|
+
/** A record {@link driftRecord} refused, naming the record and the rule. */
|
|
117
|
+
export class DriftRecordError extends Error {
|
|
118
|
+
constructor(rule, message) {
|
|
119
|
+
super(message);
|
|
120
|
+
this.name = "DriftRecordError";
|
|
121
|
+
this.rule = rule;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
const byString = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
|
|
125
|
+
const isRecordObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
126
|
+
const nonEmpty = (v) => typeof v === "string" && v.length > 0;
|
|
127
|
+
/** RFC 3339 date-time, as JSON Schema's `date-time` format reads it. */
|
|
128
|
+
const RFC3339 = /^\d{4}-\d{2}-\d{2}[Tt]\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:[Zz]|[+-]\d{2}:\d{2})$/;
|
|
129
|
+
/**
|
|
130
|
+
* The key segments of an attribute path, indexes left out:
|
|
131
|
+
* `spec.env[0]["db_password"]` is `spec`, `env`, `db_password`. A keyed
|
|
132
|
+
* element (`Tags[#api-token-owner].Value`, the deep read's address for an
|
|
133
|
+
* array it can key) is a key too: it names the element the way a map key
|
|
134
|
+
* does, so an environment list keyed by `DB_PASSWORD` redacts like a map's.
|
|
135
|
+
*/
|
|
136
|
+
export function attributePathKeys(path) {
|
|
137
|
+
const keys = [];
|
|
138
|
+
let current = "";
|
|
139
|
+
for (let i = 0; i < path.length; i++) {
|
|
140
|
+
const c = path[i];
|
|
141
|
+
if (c === ".") {
|
|
142
|
+
if (current)
|
|
143
|
+
keys.push(current);
|
|
144
|
+
current = "";
|
|
145
|
+
}
|
|
146
|
+
else if (c === "[") {
|
|
147
|
+
if (current)
|
|
148
|
+
keys.push(current);
|
|
149
|
+
current = "";
|
|
150
|
+
if (path[i + 1] === '"') {
|
|
151
|
+
let key = "";
|
|
152
|
+
let j = i + 2;
|
|
153
|
+
for (; j < path.length && path[j] !== '"'; j++) {
|
|
154
|
+
if (path[j] === "\\" && j + 1 < path.length)
|
|
155
|
+
j++;
|
|
156
|
+
key += path[j];
|
|
157
|
+
}
|
|
158
|
+
keys.push(key);
|
|
159
|
+
i = path.indexOf("]", j);
|
|
160
|
+
if (i < 0)
|
|
161
|
+
break;
|
|
162
|
+
}
|
|
163
|
+
else if (path[i + 1] === "#") {
|
|
164
|
+
const close = path.indexOf("]", i);
|
|
165
|
+
if (close < 0)
|
|
166
|
+
break;
|
|
167
|
+
keys.push(path.slice(i + 2, close));
|
|
168
|
+
i = close;
|
|
169
|
+
}
|
|
170
|
+
else {
|
|
171
|
+
const close = path.indexOf("]", i);
|
|
172
|
+
if (close < 0)
|
|
173
|
+
break;
|
|
174
|
+
i = close; // a numeric index: not a key
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
else
|
|
178
|
+
current += c;
|
|
179
|
+
}
|
|
180
|
+
if (current)
|
|
181
|
+
keys.push(current);
|
|
182
|
+
return keys;
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* One attribute, redacted: the planner's mark first, then any key segment
|
|
186
|
+
* {@link isSensitiveKey} names. A redacted attribute is `{ path, sensitive:
|
|
187
|
+
* true }` and nothing else.
|
|
188
|
+
*/
|
|
189
|
+
export function redactDriftAttribute(attribute) {
|
|
190
|
+
if (attribute.sensitive === true || attributePathKeys(attribute.path).some(isSensitiveKey)) {
|
|
191
|
+
return { path: attribute.path, sensitive: true };
|
|
192
|
+
}
|
|
193
|
+
return {
|
|
194
|
+
path: attribute.path,
|
|
195
|
+
...(attribute.before !== undefined ? { before: attribute.before } : {}),
|
|
196
|
+
...(attribute.after !== undefined ? { after: attribute.after } : {}),
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
function checkUnobserved(value, where, field) {
|
|
200
|
+
if (!isRecordObject(value))
|
|
201
|
+
throw new DriftRecordError("unobserved-reason", `${where}: ${field} needs a reason`);
|
|
202
|
+
if (!isUnobservedReason(value.reason)) {
|
|
203
|
+
throw new DriftRecordError("unobserved-reason", `${where}: ${field}.reason ${JSON.stringify(value.reason)} is not an unobserved reason`);
|
|
204
|
+
}
|
|
205
|
+
return {
|
|
206
|
+
reason: value.reason,
|
|
207
|
+
...(nonEmpty(value.detail) ? { detail: value.detail } : {}),
|
|
208
|
+
...(nonEmpty(value.queried) ? { queried: value.queried } : {}),
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
function checkOwnership(value, where) {
|
|
212
|
+
if (!isRecordObject(value) || !DRIFT_OWNERSHIP_VERDICTS.includes(value.verdict)) {
|
|
213
|
+
throw new DriftRecordError("shape", `${where}: ownership.verdict must be one of ${DRIFT_OWNERSHIP_VERDICTS.join(", ")}`);
|
|
214
|
+
}
|
|
215
|
+
if (value.by !== undefined && !DRIFT_OWNERSHIP_BY.includes(value.by)) {
|
|
216
|
+
throw new DriftRecordError("shape", `${where}: ownership.by ${JSON.stringify(value.by)} is not one of ${DRIFT_OWNERSHIP_BY.join(", ")}`);
|
|
217
|
+
}
|
|
218
|
+
if (value.markerOnObject !== undefined && typeof value.markerOnObject !== "boolean") {
|
|
219
|
+
throw new DriftRecordError("shape", `${where}: ownership.markerOnObject must be a boolean`);
|
|
220
|
+
}
|
|
221
|
+
return {
|
|
222
|
+
verdict: value.verdict,
|
|
223
|
+
...(value.by !== undefined ? { by: value.by } : {}),
|
|
224
|
+
...(typeof value.markerOnObject === "boolean" ? { markerOnObject: value.markerOnObject } : {}),
|
|
225
|
+
...(nonEmpty(value.ownedBy) ? { ownedBy: value.ownedBy } : {}),
|
|
226
|
+
...(nonEmpty(value.adopt) ? { adopt: value.adopt } : {}),
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Build one record, or refuse it. Every adapter and every reader goes
|
|
231
|
+
* through here, so these hold for every record that exists:
|
|
232
|
+
*
|
|
233
|
+
* - a `state` read's status is `recorded`, `pending` or `not-observed`;
|
|
234
|
+
* - `not-observed` carries `unobserved` with a legal reason, and no other
|
|
235
|
+
* status carries it;
|
|
236
|
+
* - `attributes` appear only on `drifted`, sorted by path, one per path,
|
|
237
|
+
* and redacted ({@link redactDriftAttribute});
|
|
238
|
+
* - `attributesUnobserved` appears only on `in-sync` or `drifted`.
|
|
239
|
+
*
|
|
240
|
+
* A refusal is a {@link DriftRecordError} naming the member, the address,
|
|
241
|
+
* the read and the status.
|
|
242
|
+
*/
|
|
243
|
+
export function driftRecord(input) {
|
|
244
|
+
const raw = input;
|
|
245
|
+
if (!isRecordObject(raw))
|
|
246
|
+
throw new DriftRecordError("shape", "a drift record must be an object");
|
|
247
|
+
const where = `drift record ${JSON.stringify(raw.address)} in member ${JSON.stringify(raw.member)} (${String(raw.read)} read, ${String(raw.status)})`;
|
|
248
|
+
if (!nonEmpty(raw.member))
|
|
249
|
+
throw new DriftRecordError("shape", `${where}: member must be a non-empty string`);
|
|
250
|
+
if (!nonEmpty(raw.address))
|
|
251
|
+
throw new DriftRecordError("shape", `${where}: address must be a non-empty string`);
|
|
252
|
+
if (!DRIFT_ENGINES.includes(raw.engine)) {
|
|
253
|
+
throw new DriftRecordError("shape", `${where}: engine ${JSON.stringify(raw.engine)} is not one of ${DRIFT_ENGINES.join(", ")}`);
|
|
254
|
+
}
|
|
255
|
+
if (!DRIFT_READS.includes(raw.read)) {
|
|
256
|
+
throw new DriftRecordError("shape", `${where}: read ${JSON.stringify(raw.read)} is not one of ${DRIFT_READS.join(", ")}`);
|
|
257
|
+
}
|
|
258
|
+
if (!DRIFT_STATUSES.includes(raw.status)) {
|
|
259
|
+
throw new DriftRecordError("shape", `${where}: status ${JSON.stringify(raw.status)} is not one of ${DRIFT_STATUSES.join(", ")}`);
|
|
260
|
+
}
|
|
261
|
+
if (raw.node !== null && !nonEmpty(raw.node))
|
|
262
|
+
throw new DriftRecordError("shape", `${where}: node must be a graph id or null`);
|
|
263
|
+
if (typeof raw.observedAt !== "string" || !RFC3339.test(raw.observedAt)) {
|
|
264
|
+
throw new DriftRecordError("shape", `${where}: observedAt ${JSON.stringify(raw.observedAt)} is not an RFC 3339 date-time`);
|
|
265
|
+
}
|
|
266
|
+
if (raw.instanceOf !== undefined && (!nonEmpty(raw.instanceOf) || raw.node === null)) {
|
|
267
|
+
throw new DriftRecordError("shape", `${where}: instanceOf must be a block node id, beside a node`);
|
|
268
|
+
}
|
|
269
|
+
if (raw.fellBackFrom !== undefined && !nonEmpty(raw.fellBackFrom))
|
|
270
|
+
throw new DriftRecordError("shape", `${where}: fellBackFrom must be a non-empty string`);
|
|
271
|
+
if (raw.resourceType !== undefined && !nonEmpty(raw.resourceType))
|
|
272
|
+
throw new DriftRecordError("shape", `${where}: resourceType must be a non-empty string`);
|
|
273
|
+
if (!isRecordObject(raw.evidence) || !nonEmpty(raw.evidence.tool))
|
|
274
|
+
throw new DriftRecordError("shape", `${where}: evidence.tool must be a non-empty string`);
|
|
275
|
+
const read = raw.read;
|
|
276
|
+
const status = raw.status;
|
|
277
|
+
if (read === "state" && !STATE_READ_STATUSES.includes(status)) {
|
|
278
|
+
throw new DriftRecordError("state-read-status", `${where}: a state read says what the last apply recorded, so it can only be ${STATE_READ_STATUSES.join(", ")}, never ${status}`);
|
|
279
|
+
}
|
|
280
|
+
let unobserved;
|
|
281
|
+
if (status === "not-observed") {
|
|
282
|
+
if (raw.unobserved === undefined)
|
|
283
|
+
throw new DriftRecordError("unobserved-reason", `${where}: not-observed needs unobserved.reason`);
|
|
284
|
+
unobserved = checkUnobserved(raw.unobserved, where, "unobserved");
|
|
285
|
+
}
|
|
286
|
+
else if (raw.unobserved !== undefined) {
|
|
287
|
+
throw new DriftRecordError("unobserved-on-observed", `${where}: only not-observed carries unobserved`);
|
|
288
|
+
}
|
|
289
|
+
let attributes;
|
|
290
|
+
if (raw.attributes !== undefined) {
|
|
291
|
+
if (status !== "drifted")
|
|
292
|
+
throw new DriftRecordError("attributes-status", `${where}: only drifted carries attributes`);
|
|
293
|
+
if (!Array.isArray(raw.attributes))
|
|
294
|
+
throw new DriftRecordError("shape", `${where}: attributes must be an array`);
|
|
295
|
+
const byPath = new Map();
|
|
296
|
+
for (const [i, a] of raw.attributes.entries()) {
|
|
297
|
+
if (!isRecordObject(a) || !nonEmpty(a.path))
|
|
298
|
+
throw new DriftRecordError("shape", `${where}: attributes[${i}] needs a path`);
|
|
299
|
+
if (a.sensitive !== undefined && a.sensitive !== true)
|
|
300
|
+
throw new DriftRecordError("shape", `${where}: attributes[${i}].sensitive must be true when set`);
|
|
301
|
+
const redacted = redactDriftAttribute(a);
|
|
302
|
+
const earlier = byPath.get(redacted.path);
|
|
303
|
+
// Two rows at one path (a block's instances share none, so this is one
|
|
304
|
+
// instance reported twice): a redaction on either redacts the path.
|
|
305
|
+
byPath.set(redacted.path, earlier?.sensitive === true ? earlier : redacted);
|
|
306
|
+
}
|
|
307
|
+
attributes = [...byPath.values()].sort((a, b) => byString(a.path, b.path));
|
|
308
|
+
}
|
|
309
|
+
let attributesUnobserved;
|
|
310
|
+
if (raw.attributesUnobserved !== undefined) {
|
|
311
|
+
if (status !== "in-sync" && status !== "drifted") {
|
|
312
|
+
throw new DriftRecordError("attributes-unobserved-status", `${where}: only in-sync or drifted carries attributesUnobserved`);
|
|
313
|
+
}
|
|
314
|
+
attributesUnobserved = checkUnobserved(raw.attributesUnobserved, where, "attributesUnobserved");
|
|
315
|
+
}
|
|
316
|
+
const ev = raw.evidence;
|
|
317
|
+
return {
|
|
318
|
+
member: raw.member,
|
|
319
|
+
engine: raw.engine,
|
|
320
|
+
address: raw.address,
|
|
321
|
+
...(raw.resourceType !== undefined ? { resourceType: raw.resourceType } : {}),
|
|
322
|
+
node: raw.node,
|
|
323
|
+
...(raw.instanceOf !== undefined ? { instanceOf: raw.instanceOf } : {}),
|
|
324
|
+
observedAt: raw.observedAt,
|
|
325
|
+
read,
|
|
326
|
+
...(raw.fellBackFrom !== undefined ? { fellBackFrom: raw.fellBackFrom } : {}),
|
|
327
|
+
status,
|
|
328
|
+
...(unobserved ? { unobserved } : {}),
|
|
329
|
+
...(attributes ? { attributes } : {}),
|
|
330
|
+
...(attributesUnobserved ? { attributesUnobserved } : {}),
|
|
331
|
+
...(raw.ownership !== undefined ? { ownership: checkOwnership(raw.ownership, where) } : {}),
|
|
332
|
+
evidence: {
|
|
333
|
+
tool: ev.tool,
|
|
334
|
+
...(nonEmpty(ev.version) ? { version: ev.version } : {}),
|
|
335
|
+
...(nonEmpty(ev.command) ? { command: ev.command } : {}),
|
|
336
|
+
...(nonEmpty(ev.engineCode) ? { engineCode: ev.engineCode } : {}),
|
|
337
|
+
},
|
|
338
|
+
};
|
|
339
|
+
}
|
|
340
|
+
/** Read one record from parsed JSON. The same checks as {@link driftRecord}. */
|
|
341
|
+
export function parseDriftRecord(value) {
|
|
342
|
+
if (!isRecordObject(value))
|
|
343
|
+
throw new DriftRecordError("shape", "a drift record must be an object");
|
|
344
|
+
return driftRecord(value);
|
|
345
|
+
}
|
|
346
|
+
function recordKey(r) {
|
|
347
|
+
return `${r.member}\u0000${r.address}\u0000${r.read}`;
|
|
348
|
+
}
|
|
349
|
+
/** Counts per status, zero included. */
|
|
350
|
+
export function countDriftStatuses(records) {
|
|
351
|
+
const counts = Object.fromEntries(DRIFT_STATUSES.map((s) => [s, 0]));
|
|
352
|
+
for (const r of records)
|
|
353
|
+
counts[r.status]++;
|
|
354
|
+
return counts;
|
|
355
|
+
}
|
|
356
|
+
/**
|
|
357
|
+
* Join records into one document. A later record for the same `(member,
|
|
358
|
+
* address, read)` replaces the earlier one, as a stream's later line does.
|
|
359
|
+
* Records are sorted by member, address and read. Every record is checked
|
|
360
|
+
* with {@link driftRecord}.
|
|
361
|
+
*/
|
|
362
|
+
export function driftDocument(records, options = {}) {
|
|
363
|
+
const latest = new Map();
|
|
364
|
+
for (const r of records) {
|
|
365
|
+
const checked = driftRecord(r);
|
|
366
|
+
latest.set(recordKey(checked), checked);
|
|
367
|
+
}
|
|
368
|
+
const sorted = [...latest.entries()].sort(([a], [b]) => byString(a, b)).map(([, r]) => r);
|
|
369
|
+
return {
|
|
370
|
+
$schema: DRIFT_SCHEMA_ID,
|
|
371
|
+
drift: DRIFT_VERSION,
|
|
372
|
+
...(options.chant !== undefined ? { chant: options.chant } : {}),
|
|
373
|
+
records: sorted,
|
|
374
|
+
counts: countDriftStatuses(sorted),
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
/** Read a document from parsed JSON, checking every record. Counts are recomputed. */
|
|
378
|
+
export function parseDriftDocument(value) {
|
|
379
|
+
if (!isRecordObject(value))
|
|
380
|
+
throw new DriftRecordError("shape", "a drift document must be an object");
|
|
381
|
+
if (value.drift !== DRIFT_VERSION)
|
|
382
|
+
throw new DriftRecordError("shape", `a drift document says drift ${JSON.stringify(value.drift)}, not "${DRIFT_VERSION}"`);
|
|
383
|
+
if (!Array.isArray(value.records))
|
|
384
|
+
throw new DriftRecordError("shape", "a drift document needs a records array");
|
|
385
|
+
return driftDocument(value.records.map(parseDriftRecord), nonEmpty(value.chant) ? { chant: value.chant } : {});
|
|
386
|
+
}
|
|
387
|
+
/**
|
|
388
|
+
* The document's digest, for a WatchOp that keeps its document as evidence
|
|
389
|
+
* in its own run ledger: `computePlanDigest("drift-document", …)` over the
|
|
390
|
+
* records sorted as {@link driftDocument} sorts them, without `observedAt`
|
|
391
|
+
* and `evidence`, which change on every read. No gate binds it.
|
|
392
|
+
*/
|
|
393
|
+
export function driftDocumentDigest(doc) {
|
|
394
|
+
const records = [...doc.records]
|
|
395
|
+
.sort((a, b) => byString(recordKey(a), recordKey(b)))
|
|
396
|
+
.map(({ observedAt: _at, evidence: _evidence, ...r }) => r);
|
|
397
|
+
return computePlanDigest("drift-document", records);
|
|
398
|
+
}
|
|
399
|
+
// ── the block rollup ───────────────────────────────────────────────────────────
|
|
400
|
+
/**
|
|
401
|
+
* The precedence a block's instance records roll up in, most significant
|
|
402
|
+
* first, for core's one rollup (`rollupInstances` in `./instance-nodes.ts`,
|
|
403
|
+
* #4454). Partial knowledge is not knowledge, so one not-observed instance
|
|
404
|
+
* makes the block not-observed, as `TERRAFORM_INSTANCE_ROLLUP` puts
|
|
405
|
+
* `unobserved` first. A difference outranks an absence, an absence
|
|
406
|
+
* outranks agreement, and `runtime` is last because a controller's children
|
|
407
|
+
* never decide a block.
|
|
408
|
+
*/
|
|
409
|
+
export const DRIFT_INSTANCE_ROLLUP = [
|
|
410
|
+
"not-observed",
|
|
411
|
+
"drifted",
|
|
412
|
+
"missing",
|
|
413
|
+
"unclaimed",
|
|
414
|
+
"undeclared",
|
|
415
|
+
"pending",
|
|
416
|
+
"in-sync",
|
|
417
|
+
"recorded",
|
|
418
|
+
"runtime",
|
|
419
|
+
];
|
|
420
|
+
/**
|
|
421
|
+
* Records grouped by block node (`instanceOf`, else `node`) and rolled up
|
|
422
|
+
* with `rollupInstances` over {@link DRIFT_INSTANCE_ROLLUP}. A record with a
|
|
423
|
+
* null `node` is on no block and is left out. Sorted by node.
|
|
424
|
+
*/
|
|
425
|
+
export function rollupDriftByBlock(records) {
|
|
426
|
+
const groups = new Map();
|
|
427
|
+
for (const r of records) {
|
|
428
|
+
const block = r.instanceOf ?? r.node;
|
|
429
|
+
if (block === null)
|
|
430
|
+
continue;
|
|
431
|
+
const list = groups.get(block) ?? [];
|
|
432
|
+
list.push(r);
|
|
433
|
+
groups.set(block, list);
|
|
434
|
+
}
|
|
435
|
+
return [...groups.entries()]
|
|
436
|
+
.sort(([a], [b]) => byString(a, b))
|
|
437
|
+
.map(([node, list]) => ({
|
|
438
|
+
node,
|
|
439
|
+
status: rollupInstances(list.map((r) => ({ kind: r.status })), DRIFT_INSTANCE_ROLLUP).kind,
|
|
440
|
+
reads: [...new Set(list.map((r) => r.read))].sort(byString),
|
|
441
|
+
records: list,
|
|
442
|
+
}));
|
|
443
|
+
}
|
|
444
|
+
/**
|
|
445
|
+
* The document with each record's `node` and `instanceOf` set from `graph`
|
|
446
|
+
* by `graphNodeFor` (`./change-set-graph.ts`), the rule a change-set entry
|
|
447
|
+
* uses, so a drift row and a change-set entry for one instance name the
|
|
448
|
+
* same node: an instance of a `count` or `for_each` block is on
|
|
449
|
+
* `<blockNode>[0]` with `instanceOf` the block node, and an instance under
|
|
450
|
+
* an expanded module call is on `<root>/module.app[0]/<block>[1]` with the
|
|
451
|
+
* same `instanceOf` (#4486). A Pulumi URN is on
|
|
452
|
+
* `<member>/<key>` by the URN rule (`./pulumi-urn.ts`, #4456), through the
|
|
453
|
+
* `pulumi` planner's case, and so is a plunge URN ({@link driftPlanner}).
|
|
454
|
+
* A null `graph` gives every record `node: null`.
|
|
455
|
+
*/
|
|
456
|
+
export function mapDriftToGraph(doc, graph) {
|
|
457
|
+
return {
|
|
458
|
+
...doc,
|
|
459
|
+
records: doc.records.map(({ instanceOf: _i, ...r }) => {
|
|
460
|
+
if (graph === null)
|
|
461
|
+
return { ...r, node: null };
|
|
462
|
+
const placed = graphNodeFor({ member: r.member, planner: driftPlanner(r.engine), address: r.address }, graph);
|
|
463
|
+
return { ...r, node: placed.node, ...(placed.instanceOf !== undefined ? { instanceOf: placed.instanceOf } : {}) };
|
|
464
|
+
}),
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
/** A start line's expected entries, bare addresses made objects. */
|
|
468
|
+
export function driftExpected(start) {
|
|
469
|
+
return start.expected.map((e) => (typeof e === "string" ? { address: e } : e));
|
|
470
|
+
}
|
|
471
|
+
/** A record as its stream line. */
|
|
472
|
+
export function driftRecordLine(record) {
|
|
473
|
+
return { type: "drift-record", ...record };
|
|
474
|
+
}
|
|
475
|
+
/** One line of the stream, newline-terminated. */
|
|
476
|
+
export function encodeDriftLine(line) {
|
|
477
|
+
return `${JSON.stringify(line)}\n`;
|
|
478
|
+
}
|
|
479
|
+
/** The whole stream for one or more reads, in order. */
|
|
480
|
+
export function encodeDriftLines(lines) {
|
|
481
|
+
let out = "";
|
|
482
|
+
for (const line of lines)
|
|
483
|
+
out += encodeDriftLine(line);
|
|
484
|
+
return out;
|
|
485
|
+
}
|
|
486
|
+
function checkStreamHead(v, type) {
|
|
487
|
+
if (v.drift !== DRIFT_VERSION)
|
|
488
|
+
throw new DriftRecordError("shape", `a ${type} line says drift ${JSON.stringify(v.drift)}, not "${DRIFT_VERSION}"`);
|
|
489
|
+
if (!nonEmpty(v.member))
|
|
490
|
+
throw new DriftRecordError("shape", `a ${type} line needs a member`);
|
|
491
|
+
if (!DRIFT_ENGINES.includes(v.engine))
|
|
492
|
+
throw new DriftRecordError("shape", `a ${type} line for ${v.member} names engine ${JSON.stringify(v.engine)}`);
|
|
493
|
+
if (!DRIFT_READS.includes(v.read))
|
|
494
|
+
throw new DriftRecordError("shape", `a ${type} line for ${v.member} names read ${JSON.stringify(v.read)}`);
|
|
495
|
+
if (v.at !== undefined && (typeof v.at !== "string" || !RFC3339.test(v.at)))
|
|
496
|
+
throw new DriftRecordError("shape", `a ${type} line for ${v.member} has an at that is not RFC 3339`);
|
|
497
|
+
}
|
|
498
|
+
/** Read one line, checking it. A record line goes through {@link driftRecord}. Unknown fields are dropped. */
|
|
499
|
+
export function decodeDriftLine(text) {
|
|
500
|
+
let v;
|
|
501
|
+
try {
|
|
502
|
+
v = JSON.parse(text);
|
|
503
|
+
}
|
|
504
|
+
catch (err) {
|
|
505
|
+
throw new DriftRecordError("shape", `a drift line is not JSON: ${err.message}`);
|
|
506
|
+
}
|
|
507
|
+
if (!isRecordObject(v))
|
|
508
|
+
throw new DriftRecordError("shape", "a drift line must be an object");
|
|
509
|
+
switch (v.type) {
|
|
510
|
+
case "drift-start": {
|
|
511
|
+
checkStreamHead(v, "drift-start");
|
|
512
|
+
if (!Array.isArray(v.expected))
|
|
513
|
+
throw new DriftRecordError("shape", `the drift-start line for ${v.member} needs an expected array`);
|
|
514
|
+
const expected = v.expected.map((e, i) => {
|
|
515
|
+
if (nonEmpty(e))
|
|
516
|
+
return e;
|
|
517
|
+
if (!isRecordObject(e) || !nonEmpty(e.address))
|
|
518
|
+
throw new DriftRecordError("shape", `the drift-start line for ${v.member}: expected[${i}] needs an address`);
|
|
519
|
+
return {
|
|
520
|
+
address: e.address,
|
|
521
|
+
...(e.node === null || nonEmpty(e.node) ? { node: e.node } : {}),
|
|
522
|
+
...(nonEmpty(e.instanceOf) ? { instanceOf: e.instanceOf } : {}),
|
|
523
|
+
...(nonEmpty(e.resourceType) ? { resourceType: e.resourceType } : {}),
|
|
524
|
+
};
|
|
525
|
+
});
|
|
526
|
+
if (v.evidence !== undefined && (!isRecordObject(v.evidence) || !nonEmpty(v.evidence.tool))) {
|
|
527
|
+
throw new DriftRecordError("shape", `the drift-start line for ${v.member} has evidence with no tool`);
|
|
528
|
+
}
|
|
529
|
+
return {
|
|
530
|
+
type: "drift-start",
|
|
531
|
+
drift: DRIFT_VERSION,
|
|
532
|
+
member: v.member,
|
|
533
|
+
engine: v.engine,
|
|
534
|
+
read: v.read,
|
|
535
|
+
...(nonEmpty(v.resource) ? { resource: v.resource } : {}),
|
|
536
|
+
...(nonEmpty(v.fellBackFrom) ? { fellBackFrom: v.fellBackFrom } : {}),
|
|
537
|
+
...(v.at !== undefined ? { at: v.at } : {}),
|
|
538
|
+
expected,
|
|
539
|
+
...(v.evidence !== undefined ? { evidence: v.evidence } : {}),
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
case "drift-record": {
|
|
543
|
+
const { type: _type, ...record } = v;
|
|
544
|
+
return driftRecordLine(parseDriftRecord(record));
|
|
545
|
+
}
|
|
546
|
+
case "drift-settle": {
|
|
547
|
+
checkStreamHead(v, "drift-settle");
|
|
548
|
+
if (!isRecordObject(v.counts))
|
|
549
|
+
throw new DriftRecordError("shape", `the drift-settle line for ${v.member} needs counts`);
|
|
550
|
+
const counts = {};
|
|
551
|
+
for (const [s, n] of Object.entries(v.counts)) {
|
|
552
|
+
if (!DRIFT_STATUSES.includes(s))
|
|
553
|
+
throw new DriftRecordError("shape", `the drift-settle line for ${v.member} counts status ${JSON.stringify(s)}`);
|
|
554
|
+
if (typeof n !== "number" || !Number.isInteger(n) || n < 0)
|
|
555
|
+
throw new DriftRecordError("shape", `the drift-settle line for ${v.member}: counts.${s} must be a count`);
|
|
556
|
+
counts[s] = n;
|
|
557
|
+
}
|
|
558
|
+
if (v.notReached !== undefined && (!Array.isArray(v.notReached) || !v.notReached.every(nonEmpty))) {
|
|
559
|
+
throw new DriftRecordError("shape", `the drift-settle line for ${v.member} has a notReached that is not a list of addresses`);
|
|
560
|
+
}
|
|
561
|
+
if (v.reads !== undefined && !isRecordObject(v.reads))
|
|
562
|
+
throw new DriftRecordError("shape", `the drift-settle line for ${v.member} has reads that is not an object`);
|
|
563
|
+
return {
|
|
564
|
+
type: "drift-settle",
|
|
565
|
+
drift: DRIFT_VERSION,
|
|
566
|
+
member: v.member,
|
|
567
|
+
engine: v.engine,
|
|
568
|
+
read: v.read,
|
|
569
|
+
counts,
|
|
570
|
+
...(v.reads !== undefined ? { reads: v.reads } : {}),
|
|
571
|
+
...(v.at !== undefined ? { at: v.at } : {}),
|
|
572
|
+
...(v.notReached !== undefined ? { notReached: [...v.notReached].sort(byString) } : {}),
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
default:
|
|
576
|
+
throw new DriftRecordError("shape", `a drift line has type ${JSON.stringify(v.type)}, not drift-start, drift-record or drift-settle`);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
/** Every non-blank line of an NDJSON stream, decoded. */
|
|
580
|
+
export function decodeDriftLines(text) {
|
|
581
|
+
return text
|
|
582
|
+
.split("\n")
|
|
583
|
+
.map((l) => l.trim())
|
|
584
|
+
.filter((l) => l.length > 0)
|
|
585
|
+
.map(decodeDriftLine);
|
|
586
|
+
}
|
|
587
|
+
/** Open a read: the start line for a member's expected addresses, each once. */
|
|
588
|
+
export function driftStart(input) {
|
|
589
|
+
const seen = new Set();
|
|
590
|
+
const expected = input.expected.filter((e) => {
|
|
591
|
+
const address = typeof e === "string" ? e : e.address;
|
|
592
|
+
return !seen.has(address) && seen.add(address);
|
|
593
|
+
});
|
|
594
|
+
return { type: "drift-start", drift: DRIFT_VERSION, ...input, expected };
|
|
595
|
+
}
|
|
596
|
+
function notReachedRecord(head, expected, at) {
|
|
597
|
+
return driftRecord({
|
|
598
|
+
member: head.member,
|
|
599
|
+
engine: head.engine,
|
|
600
|
+
address: expected.address,
|
|
601
|
+
...(expected.resourceType !== undefined ? { resourceType: expected.resourceType } : {}),
|
|
602
|
+
node: expected.node ?? null,
|
|
603
|
+
...(expected.instanceOf !== undefined && expected.node ? { instanceOf: expected.instanceOf } : {}),
|
|
604
|
+
observedAt: at,
|
|
605
|
+
read: head.read,
|
|
606
|
+
...(head.fellBackFrom !== undefined ? { fellBackFrom: head.fellBackFrom } : {}),
|
|
607
|
+
status: "not-observed",
|
|
608
|
+
unobserved: { reason: "not-reached", detail: `the ${head.read} read settled without reporting ${expected.address}` },
|
|
609
|
+
evidence: head.evidence ?? { tool: head.engine },
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
/**
|
|
613
|
+
* Close a read. `records` are the records sent since `start`, in order; a
|
|
614
|
+
* later one for an address and read replaces an earlier one, and records
|
|
615
|
+
* for another member are ignored. A record for an expected address answers
|
|
616
|
+
* it whatever its read, since one read can answer an address with another
|
|
617
|
+
* (a plan stream's unclaimed row is an `ownership` record). Every expected
|
|
618
|
+
* address with no record becomes `not-observed / not-reached` at `at`.
|
|
619
|
+
* Returns the settle line, which lists them in `notReached`, and the
|
|
620
|
+
* member's final records.
|
|
621
|
+
*/
|
|
622
|
+
export function driftSettle(start, records, at) {
|
|
623
|
+
const mine = new Map();
|
|
624
|
+
const answered = new Set();
|
|
625
|
+
for (const r of records) {
|
|
626
|
+
if (r.member !== start.member)
|
|
627
|
+
continue;
|
|
628
|
+
const checked = driftRecord(r);
|
|
629
|
+
mine.set(recordKey(checked), checked);
|
|
630
|
+
answered.add(checked.address);
|
|
631
|
+
}
|
|
632
|
+
const notReached = [];
|
|
633
|
+
for (const e of driftExpected(start)) {
|
|
634
|
+
if (answered.has(e.address))
|
|
635
|
+
continue;
|
|
636
|
+
notReached.push(e.address);
|
|
637
|
+
const r = notReachedRecord(start, e, at);
|
|
638
|
+
mine.set(recordKey(r), r);
|
|
639
|
+
}
|
|
640
|
+
const final = [...mine.entries()].sort(([a], [b]) => byString(a, b)).map(([, r]) => r);
|
|
641
|
+
return {
|
|
642
|
+
line: {
|
|
643
|
+
type: "drift-settle",
|
|
644
|
+
drift: DRIFT_VERSION,
|
|
645
|
+
member: start.member,
|
|
646
|
+
engine: start.engine,
|
|
647
|
+
read: start.read,
|
|
648
|
+
counts: countDriftStatuses(final),
|
|
649
|
+
at,
|
|
650
|
+
notReached: notReached.sort(byString),
|
|
651
|
+
},
|
|
652
|
+
records: final,
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
/**
|
|
656
|
+
* Fold a stream into a document. Record lines replace earlier ones for the
|
|
657
|
+
* same `(member, address, read)`. At a settle line, every address its start
|
|
658
|
+
* expected that no record answered (by member and address, whatever the
|
|
659
|
+
* read) becomes `not-observed / not-reached`, with the node and type the
|
|
660
|
+
* start gave, at the settle's `at`, else the start's, else the latest
|
|
661
|
+
* record's `observedAt` in the stream; `notReached` addresses the start did
|
|
662
|
+
* not list are added the same way. A start with no settle yet leaves its
|
|
663
|
+
* unanswered addresses in `reading`, which a renderer shows as "reading"
|
|
664
|
+
* and nothing else.
|
|
665
|
+
*/
|
|
666
|
+
export function foldDriftLines(lines, options = {}) {
|
|
667
|
+
const records = [];
|
|
668
|
+
const answered = new Set();
|
|
669
|
+
const open = new Map();
|
|
670
|
+
let latest;
|
|
671
|
+
const memberKey = (member, x) => `${member}\u0000${x}`;
|
|
672
|
+
for (const line of lines) {
|
|
673
|
+
if (line.type === "drift-start") {
|
|
674
|
+
open.set(memberKey(line.member, line.read), line);
|
|
675
|
+
}
|
|
676
|
+
else if (line.type === "drift-record") {
|
|
677
|
+
const { type: _type, ...record } = line;
|
|
678
|
+
records.push(record);
|
|
679
|
+
answered.add(memberKey(record.member, record.address));
|
|
680
|
+
if (latest === undefined || record.observedAt > latest)
|
|
681
|
+
latest = record.observedAt;
|
|
682
|
+
}
|
|
683
|
+
else {
|
|
684
|
+
const start = open.get(memberKey(line.member, line.read));
|
|
685
|
+
open.delete(memberKey(line.member, line.read));
|
|
686
|
+
const expected = new Map((start ? driftExpected(start) : []).map((e) => [e.address, e]));
|
|
687
|
+
for (const address of line.notReached ?? [])
|
|
688
|
+
if (!expected.has(address))
|
|
689
|
+
expected.set(address, { address });
|
|
690
|
+
const at = line.at ?? start?.at ?? latest ?? new Date(0).toISOString();
|
|
691
|
+
const head = start ?? { member: line.member, engine: line.engine, read: line.read };
|
|
692
|
+
for (const e of expected.values()) {
|
|
693
|
+
if (answered.has(memberKey(line.member, e.address)))
|
|
694
|
+
continue;
|
|
695
|
+
records.push(notReachedRecord(head, e, at));
|
|
696
|
+
answered.add(memberKey(line.member, e.address));
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
const reading = [];
|
|
701
|
+
for (const start of open.values()) {
|
|
702
|
+
for (const e of driftExpected(start)) {
|
|
703
|
+
if (!answered.has(memberKey(start.member, e.address))) {
|
|
704
|
+
reading.push({ member: start.member, read: start.read, address: e.address, node: e.node ?? null });
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
reading.sort((a, b) => byString(recordKey(a), recordKey(b)));
|
|
709
|
+
return { document: driftDocument(records, options), reading };
|
|
710
|
+
}
|
|
711
|
+
/**
|
|
712
|
+
* Where an adapter's record for `address` sits: `graphNodeFor` on the
|
|
713
|
+
* input's graph, plunge's by the `pulumi` rule ({@link driftPlanner}). Exported
|
|
714
|
+
* for the lexicons' adapters.
|
|
715
|
+
*/
|
|
716
|
+
export function placeDriftRecord(input, engine, address) {
|
|
717
|
+
if (!input.graph)
|
|
718
|
+
return { node: null };
|
|
719
|
+
const placed = graphNodeFor({ member: input.member, planner: driftPlanner(engine), address }, input.graph);
|
|
720
|
+
return { node: placed.node, ...(placed.instanceOf !== undefined ? { instanceOf: placed.instanceOf } : {}) };
|
|
721
|
+
}
|
|
722
|
+
function base(input, engine, read, address) {
|
|
723
|
+
return {
|
|
724
|
+
member: input.member,
|
|
725
|
+
engine,
|
|
726
|
+
address,
|
|
727
|
+
...placeDriftRecord(input, engine, address),
|
|
728
|
+
observedAt: input.observedAt,
|
|
729
|
+
read,
|
|
730
|
+
...(input.fellBackFrom !== undefined ? { fellBackFrom: input.fellBackFrom } : {}),
|
|
731
|
+
};
|
|
732
|
+
}
|
|
733
|
+
function chantEvidence(input, command) {
|
|
734
|
+
return { tool: "chant", ...(input.version ? { version: input.version } : {}), command };
|
|
735
|
+
}
|
|
736
|
+
/**
|
|
737
|
+
* A chant member's records, from `lifecycle diff --live`: `unchanged` and
|
|
738
|
+
* `newlyObserved` are in-sync, `driftedSinceSnapshot` drifted (before is the
|
|
739
|
+
* snapshot, after the read), `missing` pending, `disappeared` missing,
|
|
740
|
+
* `orphan` undeclared, `runtimeChildren` runtime, `unobserved` not-observed.
|
|
741
|
+
* An entity in both `missing` and `disappeared` was deployed and is gone,
|
|
742
|
+
* so it is missing.
|
|
743
|
+
*/
|
|
744
|
+
export function liveDiffDriftRecords(input) {
|
|
745
|
+
const { diff } = input;
|
|
746
|
+
const evidence = chantEvidence(input, "chant lifecycle diff --live");
|
|
747
|
+
const out = new Map();
|
|
748
|
+
const put = (name, rest, type) => {
|
|
749
|
+
if (out.has(name))
|
|
750
|
+
return;
|
|
751
|
+
const t = type ?? input.types?.[name];
|
|
752
|
+
out.set(name, driftRecord({ ...base(input, "chant", "live-read", name), ...(t ? { resourceType: t } : {}), evidence, ...rest }));
|
|
753
|
+
};
|
|
754
|
+
for (const u of diff.unobserved ?? []) {
|
|
755
|
+
put(u.name, {
|
|
756
|
+
status: "not-observed",
|
|
757
|
+
unobserved: { reason: u.reason, ...(u.detail ? { detail: u.detail } : {}), ...(u.queried ? { queried: u.queried } : {}) },
|
|
758
|
+
}, u.type);
|
|
759
|
+
}
|
|
760
|
+
for (const d of diff.driftedSinceSnapshot ?? []) {
|
|
761
|
+
put(d.name, {
|
|
762
|
+
status: "drifted",
|
|
763
|
+
attributes: d.changes.map((c) => ({ path: c.path, ...(c.oldValue !== undefined ? { before: c.oldValue } : {}), ...(c.newValue !== undefined ? { after: c.newValue } : {}) })),
|
|
764
|
+
}, d.type);
|
|
765
|
+
}
|
|
766
|
+
for (const name of diff.disappeared ?? [])
|
|
767
|
+
put(name, { status: "missing" });
|
|
768
|
+
for (const name of diff.missing ?? [])
|
|
769
|
+
put(name, { status: "pending" });
|
|
770
|
+
for (const name of diff.orphan ?? [])
|
|
771
|
+
put(name, { status: "undeclared" });
|
|
772
|
+
for (const r of diff.runtimeChildren ?? [])
|
|
773
|
+
put(r.name, { status: "runtime", ownership: { verdict: "foreign", ownedBy: r.owner } }, r.type);
|
|
774
|
+
for (const name of [...(diff.unchanged ?? []), ...(diff.newlyObserved ?? [])])
|
|
775
|
+
put(name, { status: "in-sync" });
|
|
776
|
+
return [...out.values()].sort((a, b) => byString(a.address, b.address));
|
|
777
|
+
}
|
|
778
|
+
/**
|
|
779
|
+
* A chant member's records from the deep diff alone: `drifted` with paths
|
|
780
|
+
* (before is the declared value, after the live one; an `absent` path has
|
|
781
|
+
* no after), `unchanged` in-sync, `unobserved` not-observed and
|
|
782
|
+
* `undeclaredEntities` undeclared. Accepted and held paths are not drift and
|
|
783
|
+
* are left out. To lay these over the thin read's records, so a deep hole
|
|
784
|
+
* stays on the attribute surface, use {@link mergeDeepDrift}.
|
|
785
|
+
*/
|
|
786
|
+
export function deepDiffDriftRecords(input) {
|
|
787
|
+
const { diff } = input;
|
|
788
|
+
const evidence = chantEvidence(input, input.command ?? "chant lifecycle diff --live --deep");
|
|
789
|
+
const out = new Map();
|
|
790
|
+
const put = (name, rest) => {
|
|
791
|
+
if (!out.has(name))
|
|
792
|
+
out.set(name, driftRecord({ ...base(input, "chant", "live-read", name), evidence, ...rest }));
|
|
793
|
+
};
|
|
794
|
+
for (const u of diff.unobserved ?? []) {
|
|
795
|
+
put(u.name, {
|
|
796
|
+
...(u.type ? { resourceType: u.type } : {}),
|
|
797
|
+
status: "not-observed",
|
|
798
|
+
unobserved: { reason: u.reason, ...(u.detail ? { detail: u.detail } : {}), ...(u.queried ? { queried: u.queried } : {}) },
|
|
799
|
+
});
|
|
800
|
+
}
|
|
801
|
+
for (const d of diff.drifted ?? []) {
|
|
802
|
+
put(d.name, {
|
|
803
|
+
resourceType: d.type,
|
|
804
|
+
status: "drifted",
|
|
805
|
+
attributes: d.changes.map((c) => ({
|
|
806
|
+
path: c.path,
|
|
807
|
+
...(c.declared !== undefined ? { before: c.declared } : {}),
|
|
808
|
+
...(c.kind === "changed" && c.live !== undefined ? { after: c.live } : {}),
|
|
809
|
+
})),
|
|
810
|
+
});
|
|
811
|
+
}
|
|
812
|
+
for (const name of diff.undeclaredEntities ?? [])
|
|
813
|
+
put(name, { status: "undeclared" });
|
|
814
|
+
for (const name of diff.unchanged ?? [])
|
|
815
|
+
put(name, { status: "in-sync" });
|
|
816
|
+
return [...out.values()].sort((a, b) => byString(a.address, b.address));
|
|
817
|
+
}
|
|
818
|
+
/**
|
|
819
|
+
* The thin read's records with the deep read laid over them, by member and
|
|
820
|
+
* address. A thin in-sync record the deep read found drifted becomes
|
|
821
|
+
* drifted with the deep paths. A deep not-observed record never changes the
|
|
822
|
+
* thin status: presence is known, so it becomes `attributesUnobserved` on an
|
|
823
|
+
* in-sync or drifted record and is dropped elsewhere. Deep records with no
|
|
824
|
+
* thin record are added as they are.
|
|
825
|
+
*/
|
|
826
|
+
export function mergeDeepDrift(thin, deep) {
|
|
827
|
+
const key = (r) => `${r.member}\u0000${r.address}`;
|
|
828
|
+
const out = new Map();
|
|
829
|
+
for (const r of thin)
|
|
830
|
+
out.set(key(r), r);
|
|
831
|
+
for (const d of deep) {
|
|
832
|
+
const t = out.get(key(d));
|
|
833
|
+
if (!t) {
|
|
834
|
+
out.set(key(d), d);
|
|
835
|
+
continue;
|
|
836
|
+
}
|
|
837
|
+
if (d.status === "not-observed") {
|
|
838
|
+
if (t.status === "in-sync" || t.status === "drifted")
|
|
839
|
+
out.set(key(d), driftRecord({ ...t, attributesUnobserved: d.unobserved }));
|
|
840
|
+
}
|
|
841
|
+
else if (d.status === "drifted" && (t.status === "in-sync" || t.status === "drifted")) {
|
|
842
|
+
out.set(key(d), driftRecord({ ...t, status: "drifted", attributes: [...(t.attributes ?? []), ...(d.attributes ?? [])] }));
|
|
843
|
+
}
|
|
844
|
+
}
|
|
845
|
+
return [...out.values()].sort((a, b) => byString(key(a), key(b)));
|
|
846
|
+
}
|
|
847
|
+
/**
|
|
848
|
+
* A member's records from component status, one per component, addressed
|
|
849
|
+
* by component name: `reconciled` in-sync, `drifted` drifted, `stale`
|
|
850
|
+
* missing (recorded and no longer observed live), `unknown` not-observed
|
|
851
|
+
* with the row's reason, or `not-reached` when live status was never asked
|
|
852
|
+
* for. `unrecorded` is drifted when the component is live, since something
|
|
853
|
+
* runs that no release accounts for, pending when nothing is, and
|
|
854
|
+
* not-observed when live status was not asked for.
|
|
855
|
+
*/
|
|
856
|
+
export function componentStatusDriftRecords(input) {
|
|
857
|
+
const evidence = chantEvidence(input, "chant components status --live");
|
|
858
|
+
return input.rows
|
|
859
|
+
.map((row) => {
|
|
860
|
+
const at = base(input, "chant", "live-read", row.component);
|
|
861
|
+
const common = { ...at, resourceType: "component", evidence };
|
|
862
|
+
switch (row.reconciliation) {
|
|
863
|
+
case "reconciled":
|
|
864
|
+
return driftRecord({ ...common, status: "in-sync" });
|
|
865
|
+
case "drifted":
|
|
866
|
+
return driftRecord({ ...common, status: "drifted" });
|
|
867
|
+
case "stale":
|
|
868
|
+
return driftRecord({ ...common, status: "missing" });
|
|
869
|
+
case "unrecorded":
|
|
870
|
+
if (row.live === true)
|
|
871
|
+
return driftRecord({ ...common, status: "drifted" });
|
|
872
|
+
if (row.live === false)
|
|
873
|
+
return driftRecord({ ...common, status: "pending" });
|
|
874
|
+
return driftRecord({ ...common, status: "not-observed", unobserved: { reason: "not-reached", detail: row.detail } });
|
|
875
|
+
case "unknown":
|
|
876
|
+
return driftRecord({
|
|
877
|
+
...common,
|
|
878
|
+
status: "not-observed",
|
|
879
|
+
unobserved: row.unobserved
|
|
880
|
+
? { reason: row.unobserved.reason, ...(row.unobserved.detail ? { detail: row.unobserved.detail } : {}) }
|
|
881
|
+
: { reason: "not-reached", detail: row.detail },
|
|
882
|
+
});
|
|
883
|
+
}
|
|
884
|
+
})
|
|
885
|
+
.sort((a, b) => byString(a.address, b.address));
|
|
886
|
+
}
|