@intentius/chant 0.124.51 → 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 +17 -3
- 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 +224 -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/covered-round.d.ts +173 -0
- package/dist/components/covered-round.d.ts.map +1 -0
- package/dist/components/covered-round.js +200 -0
- 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 +42 -1
- package/dist/components/driver.d.ts.map +1 -1
- package/dist/components/driver.js +74 -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 +7 -2
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/index.js +5 -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 +62 -7
- package/dist/components/pr-run.d.ts.map +1 -1
- package/dist/components/pr-run.js +354 -53
- 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/components/wave-plan.d.ts +2 -0
- package/dist/components/wave-plan.d.ts.map +1 -1
- package/dist/components/wave-plan.js +4 -1
- 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 +92 -3
- package/dist/pr-loop.d.ts.map +1 -1
- package/dist/pr-loop.js +62 -3
- 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 +76 -3
- 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 +16 -3
- 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 +238 -0
- package/src/components/cli-support.ts +70 -14
- package/src/components/covered-round.test.ts +166 -0
- package/src/components/covered-round.ts +293 -0
- 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 +101 -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 +59 -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 +728 -4
- package/src/components/pr-run.ts +395 -55
- 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/components/wave-plan.ts +5 -2
- 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 +57 -1
- package/src/pr-loop.ts +154 -6
- 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 +76 -3
- 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,198 @@
|
|
|
1
|
+
// #4533 (moved from INTENTIUS/behold#1028): the diff of two index versions. Two fixture
|
|
2
|
+
// versions of one aws scope (./__fixtures__/index-diff/), the scope-mismatch refusal, a type
|
|
3
|
+
// unobserved in one read never reported as removed, edges, and reading versions through a store.
|
|
4
|
+
import { copyFileSync, mkdirSync, mkdtempSync, readFileSync } from "node:fs";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
6
|
+
import { join } from "node:path";
|
|
7
|
+
import { describe, expect, it } from "vitest";
|
|
8
|
+
import {
|
|
9
|
+
diffIndexVersionRefs,
|
|
10
|
+
diffIndexVersions,
|
|
11
|
+
indexAbsence,
|
|
12
|
+
IndexScopeMismatch,
|
|
13
|
+
NotAnIndexVersion,
|
|
14
|
+
parseIndexVersionRef,
|
|
15
|
+
readIndexVersionRef,
|
|
16
|
+
type IndexDiffDocument,
|
|
17
|
+
} from "./index-diff";
|
|
18
|
+
import { indexCoverageFor, type IndexDocument } from "./index-document";
|
|
19
|
+
import { createSnapshotStore } from "./snapshot-store";
|
|
20
|
+
|
|
21
|
+
const DIR = join(import.meta.dirname, "__fixtures__", "index-diff");
|
|
22
|
+
const V1 = "20261026T120000Z-aaaaaaaaaaaa.json";
|
|
23
|
+
const V2 = "20261026T130000Z-bbbbbbbbbbbb.json";
|
|
24
|
+
const SCOPE = "cloud/aws/000000000000/5c1f0e2a9b7d";
|
|
25
|
+
const load = (name: string): IndexDiffDocument => JSON.parse(readFileSync(join(DIR, name), "utf8")) as IndexDiffDocument;
|
|
26
|
+
const B = (n: string) => `aws/000000000000/us-east-1/AWS::S3::Bucket/${n}`;
|
|
27
|
+
const ROLE = "aws/000000000000/us-east-1/AWS::IAM::Role/app-role";
|
|
28
|
+
const QUEUE = "aws/000000000000/us-east-1/AWS::SQS::Queue/jobs";
|
|
29
|
+
|
|
30
|
+
describe("diffIndexVersions over two fixture versions", () => {
|
|
31
|
+
const d = diffIndexVersions(load(V1), load(V2), { fromVersion: V1, toVersion: V2 });
|
|
32
|
+
|
|
33
|
+
it("shows both stamps and the scope", () => {
|
|
34
|
+
expect(d.scopeKey).toBe(SCOPE);
|
|
35
|
+
expect(d.from).toMatchObject({ version: V1, observedAt: "2026-10-26T12:00:00Z" });
|
|
36
|
+
expect(d.to).toMatchObject({ version: V2, observedAt: "2026-10-26T13:00:00Z" });
|
|
37
|
+
expect(d.unchanged).toBe(false);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("one changed property on the edited bucket, and nothing for the tag that did not move", () => {
|
|
41
|
+
expect(d.nodes.changed).toEqual([
|
|
42
|
+
{ key: B("web-logs"), kind: "AWS::S3::Bucket", listedType: "AWS::S3::Bucket", location: "us-east-1", properties: [{ path: "attrs.VersioningConfiguration.Status", before: "Suspended", after: "Enabled" }] },
|
|
43
|
+
]);
|
|
44
|
+
expect(d.counts.properties).toBe(1);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("adds and removes nodes only where both reads listed the type", () => {
|
|
48
|
+
expect(d.nodes.added.map((n) => n.key)).toEqual([B("new-bucket")]);
|
|
49
|
+
expect(d.nodes.removed.map((n) => n.key)).toEqual([B("old-bucket")]);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it("edges: added, removed, and not observed when an end's type was not listed", () => {
|
|
53
|
+
expect(d.edges.added.map((e) => [e.from, e.to])).toEqual([[ROLE, B("new-bucket")]]);
|
|
54
|
+
expect(d.edges.removed.map((e) => [e.from, e.to])).toEqual([[ROLE, B("old-bucket")]]);
|
|
55
|
+
expect(d.edges.notObserved).toEqual([expect.objectContaining({ from: ROLE, to: QUEUE, side: "to", unobservedEnd: QUEUE, verdict: "read-failed" })]);
|
|
56
|
+
expect(d.edges.removed.some((e) => e.to === QUEUE)).toBe(false);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("names the coverage verdict that moved", () => {
|
|
60
|
+
expect(d.coverage).toEqual([{ listedType: "AWS::SQS::Queue", location: "us-east-1", before: "listed", after: "read-failed" }]);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("the reverse diff swaps added and removed, and the queue is first observed, never added", () => {
|
|
64
|
+
const r = diffIndexVersions(load(V2), load(V1));
|
|
65
|
+
expect(r.nodes.added.map((n) => n.key)).toEqual([B("old-bucket")]);
|
|
66
|
+
expect(r.nodes.removed.map((n) => n.key)).toEqual([B("new-bucket")]);
|
|
67
|
+
expect(r.nodes.notObserved).toEqual([expect.objectContaining({ key: QUEUE, side: "from", verdict: "read-failed" })]);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("same digest: unchanged, nothing walked", () => {
|
|
71
|
+
const a = load(V1);
|
|
72
|
+
const u = diffIndexVersions(a, { ...a, graph: { nodes: [], edges: [] } });
|
|
73
|
+
expect(u.unchanged).toBe(true);
|
|
74
|
+
expect(u.counts.removed).toBe(0);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("takes chant's own IndexDocument", () => {
|
|
78
|
+
const doc = load(V1) as unknown as IndexDocument;
|
|
79
|
+
expect(diffIndexVersions(doc, doc).unchanged).toBe(true);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
describe("a scope mismatch", () => {
|
|
84
|
+
it("is refused by name, with both scope keys", () => {
|
|
85
|
+
const a = load(V1);
|
|
86
|
+
const b = load(V2);
|
|
87
|
+
b.stamp = { ...b.stamp, scopeKey: "cloud/aws/000000000000/0123456789ab" };
|
|
88
|
+
expect(() => diffIndexVersions(a, b)).toThrow(IndexScopeMismatch);
|
|
89
|
+
try {
|
|
90
|
+
diffIndexVersions(a, b);
|
|
91
|
+
} catch (e) {
|
|
92
|
+
expect((e as IndexScopeMismatch).code).toBe("index-scope-mismatch");
|
|
93
|
+
expect((e as Error).message).toContain(SCOPE);
|
|
94
|
+
expect((e as Error).message).toContain("cloud/aws/000000000000/0123456789ab");
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it("a document of another shape is not an index version", () => {
|
|
99
|
+
expect(() => diffIndexVersions({ index: "v2" } as unknown as IndexDiffDocument, load(V1))).toThrow(NotAnIndexVersion);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
describe("an unobserved type is never reported as removed", () => {
|
|
104
|
+
it("a type with no coverage entry in the later read is not observed, not removed", () => {
|
|
105
|
+
const b = load(V2);
|
|
106
|
+
b.coverage = b.coverage.filter((c) => c.listedType !== "AWS::S3::Bucket");
|
|
107
|
+
const d = diffIndexVersions(load(V1), b);
|
|
108
|
+
expect(d.nodes.removed).toEqual([]);
|
|
109
|
+
expect(d.nodes.notObserved.filter((n) => n.listedType === "AWS::S3::Bucket" && n.side === "to").map((n) => [n.key, n.verdict])).toEqual([[B("old-bucket"), "not-in-coverage"]]);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("a wildcard entry speaks for every type and location; a wildcard read-failed does not", () => {
|
|
113
|
+
const b = load(V2);
|
|
114
|
+
b.coverage = [{ listedType: "*", location: "*", verdict: "listed" }];
|
|
115
|
+
expect(diffIndexVersions(load(V1), b).nodes.removed.map((n) => n.key)).toEqual([B("old-bucket"), QUEUE]);
|
|
116
|
+
b.coverage = [{ listedType: "*", location: "us-east-1", verdict: "read-failed" }];
|
|
117
|
+
const d = diffIndexVersions(load(V1), b);
|
|
118
|
+
expect(d.nodes.removed).toEqual([]);
|
|
119
|
+
expect(d.edges.removed).toEqual([]);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it("indexAbsence is indexCoverageFor's rule: exact over wildcard, listed means removed", () => {
|
|
123
|
+
const cov = [
|
|
124
|
+
{ listedType: "*", location: "*", verdict: "listed" },
|
|
125
|
+
{ listedType: "AWS::SQS::Queue", location: "*", verdict: "access-denied", detail: "no sqs:ListQueues" },
|
|
126
|
+
{ listedType: "AWS::SQS::Queue", location: "eu-west-1", verdict: "listed" },
|
|
127
|
+
];
|
|
128
|
+
expect(indexAbsence(cov, "AWS::SQS::Queue", "eu-west-1")).toEqual({ kind: "removed" });
|
|
129
|
+
expect(indexAbsence(cov, "AWS::SQS::Queue", "us-east-1")).toEqual({ kind: "not-observed", verdict: "access-denied", detail: "no sqs:ListQueues" });
|
|
130
|
+
expect(indexAbsence(cov, "AWS::S3::Bucket", "us-east-1")).toEqual({ kind: "removed" });
|
|
131
|
+
expect(indexAbsence([], "AWS::S3::Bucket", "us-east-1")).toEqual({ kind: "not-observed", verdict: "not-in-coverage" });
|
|
132
|
+
expect(indexCoverageFor(cov, "AWS::SQS::Queue", "us-east-1")?.verdict).toBe("access-denied");
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
describe("version references", () => {
|
|
137
|
+
it("parses <scopeKey>/<version>, <scopeKey>, and a bare version in another's scope", () => {
|
|
138
|
+
expect(parseIndexVersionRef(`${SCOPE}/${V1}`)).toEqual({ scopeKey: SCOPE, version: V1 });
|
|
139
|
+
expect(parseIndexVersionRef(`${SCOPE}/`)).toEqual({ scopeKey: SCOPE });
|
|
140
|
+
expect(parseIndexVersionRef(V2, SCOPE)).toEqual({ scopeKey: SCOPE, version: V2 });
|
|
141
|
+
expect(() => parseIndexVersionRef(V2)).toThrow(/no scope/);
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
/** A local store holding both fixture versions of one scope, and one version of another. */
|
|
146
|
+
function localStore() {
|
|
147
|
+
const cwd = mkdtempSync(join(tmpdir(), "chant-index-diff-"));
|
|
148
|
+
const scope = join(cwd, "store", "index", ...SCOPE.split("/"));
|
|
149
|
+
mkdirSync(scope, { recursive: true });
|
|
150
|
+
copyFileSync(join(DIR, V1), join(scope, V1));
|
|
151
|
+
copyFileSync(join(DIR, V2), join(scope, V2));
|
|
152
|
+
const other = join(cwd, "store", "index", "cloud", "aws", "000000000000", "0123456789ab");
|
|
153
|
+
mkdirSync(other, { recursive: true });
|
|
154
|
+
copyFileSync(join(DIR, V1), join(other, "20261026T140000Z-cccccccccccc.json"));
|
|
155
|
+
return createSnapshotStore({ store: "local", dir: "store" }, { cwd });
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
describe("reading versions through the store (#4519)", () => {
|
|
159
|
+
it("a scope alone diffs its last two versions", async () => {
|
|
160
|
+
const d = await diffIndexVersionRefs(localStore(), { scopeKey: SCOPE });
|
|
161
|
+
expect(d.from.version).toBe(V1);
|
|
162
|
+
expect(d.to.version).toBe(V2);
|
|
163
|
+
expect(d.counts.properties).toBe(1);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("a scope's latest version, and a version the store lacks", async () => {
|
|
167
|
+
const store = localStore();
|
|
168
|
+
expect((await readIndexVersionRef(store, { scopeKey: SCOPE })).version).toBe(V2);
|
|
169
|
+
await expect(readIndexVersionRef(store, { scopeKey: SCOPE, version: "20261026T150000Z-dddddddddddd.json" })).rejects.toThrow(NotAnIndexVersion);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it("refuses two references under different scope paths before reading either", async () => {
|
|
173
|
+
await expect(
|
|
174
|
+
diffIndexVersionRefs(localStore(), { scopeKey: SCOPE, version: V1 }, { scopeKey: "cloud/aws/000000000000/0123456789ab", version: "20261026T140000Z-cccccccccccc.json" }),
|
|
175
|
+
).rejects.toMatchObject({ code: "index-scope-mismatch" });
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it("a stored version whose stamp names another scope than its path is refused", async () => {
|
|
179
|
+
// The other scope path holds V1's document, whose stamp names SCOPE; its latest against
|
|
180
|
+
// a version of that path compares the stamps and refuses.
|
|
181
|
+
const store = localStore();
|
|
182
|
+
const other = await readIndexVersionRef(store, { scopeKey: "cloud/aws/000000000000/0123456789ab" });
|
|
183
|
+
const b = { ...load(V2), stamp: { ...load(V2).stamp, scopeKey: "cloud/aws/000000000000/0123456789ab" } };
|
|
184
|
+
expect(() => diffIndexVersions(other.doc, b)).toThrow(/different scopes/);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it("one version in a scope has nothing before it", async () => {
|
|
188
|
+
await expect(diffIndexVersionRefs(localStore(), { scopeKey: "cloud/aws/000000000000/0123456789ab" })).rejects.toThrow(/no version before/);
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
describe("the module", () => {
|
|
193
|
+
it("imports nothing outside chant core", () => {
|
|
194
|
+
const src = readFileSync(join(import.meta.dirname, "index-diff.ts"), "utf8");
|
|
195
|
+
const imports = [...src.matchAll(/from\s+"([^"]+)"/g)].map((m) => m[1]!);
|
|
196
|
+
expect(imports.every((i) => i.startsWith("./") || i.startsWith("../"))).toBe(true);
|
|
197
|
+
});
|
|
198
|
+
});
|
|
@@ -0,0 +1,476 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What changed between two versions of one index scope (#4533, moved from
|
|
3
|
+
* INTENTIUS/behold#1028's src/graph-db/index-diff.ts under the ruling that
|
|
4
|
+
* behold is a control plane and every model lives in chant,
|
|
5
|
+
* INTENTIUS/behold#1024).
|
|
6
|
+
*
|
|
7
|
+
* A pure function over two index versions (./index-document.ts): nodes added
|
|
8
|
+
* and removed, properties changed, edges added and removed. It reads nothing
|
|
9
|
+
* and writes nothing; {@link readIndexVersionRef} and
|
|
10
|
+
* {@link diffIndexVersionRefs} load the two versions through a
|
|
11
|
+
* {@link SnapshotStore}, so every store kind (git ref, local, s3) diffs the
|
|
12
|
+
* same way.
|
|
13
|
+
*
|
|
14
|
+
* The lifecycle deep diff (./deep-diff.ts) compares a declared tree with a
|
|
15
|
+
* live one, and a live path no declaration claims is "unclaimed" there rather
|
|
16
|
+
* than a change. Two index versions are both observed, so this comparison is
|
|
17
|
+
* symmetric.
|
|
18
|
+
*
|
|
19
|
+
* Rules:
|
|
20
|
+
*
|
|
21
|
+
* - One scope per diff. Two versions whose `stamp.scopeKey` differ are
|
|
22
|
+
* refused by name with {@link IndexScopeMismatch}: a node missing from one
|
|
23
|
+
* could just be out of its scope.
|
|
24
|
+
* - A node is removed only when the later read listed its (type, location).
|
|
25
|
+
* Otherwise it is `not-observed` in that read, with the coverage verdict
|
|
26
|
+
* that says why. The other way round, a node is added only when the earlier
|
|
27
|
+
* read listed its (type, location). Coverage is looked up with
|
|
28
|
+
* {@link indexCoverageFor}, where `"*"` matches every type or location.
|
|
29
|
+
* {@link indexAbsence} is that rule on its own, for #4526's drift from an
|
|
30
|
+
* index, so both read "absent" the same way.
|
|
31
|
+
* - An edge is removed (or added) only when both its ends were observed in
|
|
32
|
+
* the read that lacks it; otherwise it is `not-observed`.
|
|
33
|
+
* - Properties: `kind`, and each leaf path of `attrs` (arrays compare as
|
|
34
|
+
* whole values). `tags` is compared only for a node whose `attrs` show no
|
|
35
|
+
* change, because a lister's tags are a projection of the listed
|
|
36
|
+
* properties: one tag edit is one changed property, not two. `facts` is
|
|
37
|
+
* never compared (its `observedAt` moves on every read).
|
|
38
|
+
* - Two versions with the same `stamp.digest` hold the same graph and
|
|
39
|
+
* coverage: the diff is `unchanged` and nothing is walked.
|
|
40
|
+
*/
|
|
41
|
+
import { NEXT_RELEASE } from "../release-floors";
|
|
42
|
+
import { indexCoverageFor, isIndexVersionName, type CoverageEntry, type IndexDocument } from "./index-document";
|
|
43
|
+
import type { SnapshotStore } from "./snapshot-store";
|
|
44
|
+
|
|
45
|
+
/** The first chant that exports the index diff and `chant index diff`. */
|
|
46
|
+
export const INDEX_DIFF_FLOOR = NEXT_RELEASE;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* The words a comparison against an index uses (#4533, shared with #4526).
|
|
50
|
+
* `not-observed` is the drift record's word for the same thing
|
|
51
|
+
* (../drift-record.ts `DriftStatus`): the read could not say, and the
|
|
52
|
+
* coverage verdict says why.
|
|
53
|
+
*/
|
|
54
|
+
export type IndexChangeKind = "added" | "removed" | "changed" | "not-observed";
|
|
55
|
+
export const INDEX_CHANGE_KINDS: readonly IndexChangeKind[] = ["added", "removed", "changed", "not-observed"];
|
|
56
|
+
|
|
57
|
+
/** A coverage entry as the diff reads it: chant's `CoverageEntry`, or the same shape with any verdict string. */
|
|
58
|
+
export interface IndexDiffCoverageEntry {
|
|
59
|
+
listedType: string;
|
|
60
|
+
location: string;
|
|
61
|
+
verdict: CoverageEntry["verdict"] | (string & {});
|
|
62
|
+
detail?: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** The parts of an index node the diff reads. An `IRNode` is one. */
|
|
66
|
+
export interface IndexDiffNode {
|
|
67
|
+
id: string;
|
|
68
|
+
kind: string;
|
|
69
|
+
lexicon?: string;
|
|
70
|
+
attrs?: Record<string, unknown>;
|
|
71
|
+
tags?: Record<string, string>;
|
|
72
|
+
listedType?: string;
|
|
73
|
+
location?: string;
|
|
74
|
+
nativeId?: string;
|
|
75
|
+
physicalId?: string;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** The parts of an index edge the diff reads. An `IREdge` is one. */
|
|
79
|
+
export interface IndexDiffEdge {
|
|
80
|
+
from: string;
|
|
81
|
+
to: string;
|
|
82
|
+
kind?: string;
|
|
83
|
+
viaAttr?: string;
|
|
84
|
+
relation?: string;
|
|
85
|
+
origin?: string;
|
|
86
|
+
rule?: string;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** The parts of an index version the diff reads. An {@link IndexDocument} is one. */
|
|
90
|
+
export interface IndexDiffDocument {
|
|
91
|
+
index: string;
|
|
92
|
+
stamp: { scopeKey: string; observedAt: string; completedAt?: string; digest: string; source?: { kind: string; name: string; version?: string } };
|
|
93
|
+
coverage: readonly IndexDiffCoverageEntry[];
|
|
94
|
+
graph: { nodes: readonly IndexDiffNode[]; edges: readonly IndexDiffEdge[] };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Two versions of different scopes. Refused by name, never diffed. */
|
|
98
|
+
export class IndexScopeMismatch extends Error {
|
|
99
|
+
readonly code = "index-scope-mismatch";
|
|
100
|
+
constructor(
|
|
101
|
+
readonly fromScopeKey: string,
|
|
102
|
+
readonly toScopeKey: string,
|
|
103
|
+
) {
|
|
104
|
+
super(
|
|
105
|
+
`refused: the two index versions have different scopes (${fromScopeKey} and ${toScopeKey}), and a node missing from one could just be out of its scope. Diff two versions of one scope.`,
|
|
106
|
+
);
|
|
107
|
+
this.name = "IndexScopeMismatch";
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** A document that is not an index version, or a reference no store holds. */
|
|
112
|
+
export class NotAnIndexVersion extends Error {
|
|
113
|
+
readonly code = "not-an-index-version";
|
|
114
|
+
constructor(message: string) {
|
|
115
|
+
super(message);
|
|
116
|
+
this.name = "NotAnIndexVersion";
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export interface IndexDiffStamp {
|
|
121
|
+
scopeKey: string;
|
|
122
|
+
observedAt: string;
|
|
123
|
+
completedAt?: string;
|
|
124
|
+
digest: string;
|
|
125
|
+
source?: { kind: string; name: string; version?: string };
|
|
126
|
+
/** The version name, when the caller knows it. */
|
|
127
|
+
version?: string;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export interface IndexNodeRef {
|
|
131
|
+
key: string;
|
|
132
|
+
kind: string;
|
|
133
|
+
listedType: string;
|
|
134
|
+
location: string;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export interface IndexPropertyChange {
|
|
138
|
+
path: string;
|
|
139
|
+
before?: unknown;
|
|
140
|
+
after?: unknown;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export interface IndexNodeChanged extends IndexNodeRef {
|
|
144
|
+
properties: IndexPropertyChange[];
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** A node one read has and the other did not observe. `side` names the read that did not observe it. */
|
|
148
|
+
export interface IndexNodeNotObserved extends IndexNodeRef {
|
|
149
|
+
side: "from" | "to";
|
|
150
|
+
/** The coverage verdict for its (type, location) in that read, or `not-in-coverage`. */
|
|
151
|
+
verdict: string;
|
|
152
|
+
detail?: string;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export interface IndexEdgeRef {
|
|
156
|
+
from: string;
|
|
157
|
+
to: string;
|
|
158
|
+
kind: string;
|
|
159
|
+
viaAttr?: string;
|
|
160
|
+
relation: string;
|
|
161
|
+
origin?: string;
|
|
162
|
+
rule?: string;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export interface IndexEdgeNotObserved extends IndexEdgeRef {
|
|
166
|
+
side: "from" | "to";
|
|
167
|
+
/** The end whose (type, location) was not observed in that read. */
|
|
168
|
+
unobservedEnd: string;
|
|
169
|
+
verdict: string;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export interface IndexDiff {
|
|
173
|
+
diff: "v1";
|
|
174
|
+
scopeKey: string;
|
|
175
|
+
from: IndexDiffStamp;
|
|
176
|
+
to: IndexDiffStamp;
|
|
177
|
+
/** Same digest: same graph and coverage. */
|
|
178
|
+
unchanged: boolean;
|
|
179
|
+
nodes: {
|
|
180
|
+
added: IndexNodeRef[];
|
|
181
|
+
removed: IndexNodeRef[];
|
|
182
|
+
changed: IndexNodeChanged[];
|
|
183
|
+
notObserved: IndexNodeNotObserved[];
|
|
184
|
+
};
|
|
185
|
+
edges: {
|
|
186
|
+
added: IndexEdgeRef[];
|
|
187
|
+
removed: IndexEdgeRef[];
|
|
188
|
+
notObserved: IndexEdgeNotObserved[];
|
|
189
|
+
};
|
|
190
|
+
/** (type, location) pairs whose verdict moved between the reads. */
|
|
191
|
+
coverage: Array<{ listedType: string; location: string; before?: string; after?: string }>;
|
|
192
|
+
counts: { added: number; removed: number; changed: number; properties: number; notObserved: number; edgesAdded: number; edgesRemoved: number; edgesNotObserved: number };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export interface IndexDiffOptions {
|
|
196
|
+
/** Version names, for the stamps. */
|
|
197
|
+
fromVersion?: string;
|
|
198
|
+
toVersion?: string;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** The inverse of index-document.ts encodeIndexSegment; a segment that is not percent-encoded comes back as it is. */
|
|
202
|
+
function decodeSegment(s: string): string {
|
|
203
|
+
try {
|
|
204
|
+
return decodeURIComponent(s);
|
|
205
|
+
} catch {
|
|
206
|
+
return s;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* A node's (type, location): its own `listedType` and `location` (#4521
|
|
212
|
+
* writes both), else the path-style id's fourth and third segments
|
|
213
|
+
* (behold#1025 Q2, `<substrate>/<scopeId>/<location>/<listedType>/<nativeId>`),
|
|
214
|
+
* else its kind at every location.
|
|
215
|
+
*/
|
|
216
|
+
export function indexNodeTypeAndLocation(n: IndexDiffNode): { listedType: string; location: string } {
|
|
217
|
+
if (n.listedType && n.location) return { listedType: n.listedType, location: n.location };
|
|
218
|
+
const parts = n.id.split("/");
|
|
219
|
+
if (parts.length >= 5) return { listedType: n.listedType ?? decodeSegment(parts[3]!), location: n.location ?? decodeSegment(parts[2]!) };
|
|
220
|
+
return { listedType: n.listedType ?? n.kind, location: n.location ?? "*" };
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** What an index's coverage says about a (type, location) that holds no node. */
|
|
224
|
+
export type IndexAbsence = { kind: "removed" } | { kind: "not-observed"; verdict: string; detail?: string };
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* The rule for an absent node (#4533, shared with #4526): absent counts as
|
|
228
|
+
* gone (`removed`) only where the read's coverage says `listed` for the
|
|
229
|
+
* node's (type, location); anywhere else it is `not-observed`, with the
|
|
230
|
+
* verdict, or `not-in-coverage` when no entry speaks for it.
|
|
231
|
+
*/
|
|
232
|
+
export function indexAbsence(coverage: readonly IndexDiffCoverageEntry[], listedType: string, location: string): IndexAbsence {
|
|
233
|
+
const e = indexCoverageFor(coverage, listedType, location);
|
|
234
|
+
if (e?.verdict === "listed") return { kind: "removed" };
|
|
235
|
+
return e ? { kind: "not-observed", verdict: e.verdict, ...(e.detail ? { detail: e.detail } : {}) } : { kind: "not-observed", verdict: "not-in-coverage" };
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function stampOf(doc: IndexDiffDocument, version?: string): IndexDiffStamp {
|
|
239
|
+
const s = doc.stamp;
|
|
240
|
+
return {
|
|
241
|
+
scopeKey: s.scopeKey,
|
|
242
|
+
observedAt: s.observedAt,
|
|
243
|
+
...(s.completedAt ? { completedAt: s.completedAt } : {}),
|
|
244
|
+
digest: s.digest,
|
|
245
|
+
...(s.source ? { source: s.source } : {}),
|
|
246
|
+
...(version ? { version } : {}),
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function check(doc: IndexDiffDocument, which: string): void {
|
|
251
|
+
if (!doc || doc.index !== "v1" || typeof doc.stamp?.scopeKey !== "string" || !Array.isArray(doc.graph?.nodes)) {
|
|
252
|
+
throw new NotAnIndexVersion(`the ${which} document is not an index version (index: "v1" with a stamp and a graph)`);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const isPlain = (v: unknown): v is Record<string, unknown> => !!v && typeof v === "object" && !Array.isArray(v);
|
|
257
|
+
|
|
258
|
+
/** JSON with keys sorted, so equal values compare equal. */
|
|
259
|
+
function canon(v: unknown): string {
|
|
260
|
+
return JSON.stringify(v, (_k, x: unknown) => (isPlain(x) ? Object.fromEntries(Object.entries(x).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))) : x)) ?? "undefined";
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** Leaf paths of an object; arrays and scalars are leaves. */
|
|
264
|
+
function leaves(v: unknown, prefix: string, out: Map<string, unknown>): void {
|
|
265
|
+
if (isPlain(v) && Object.keys(v).length > 0) {
|
|
266
|
+
for (const [k, x] of Object.entries(v)) leaves(x, prefix ? `${prefix}.${k}` : k, out);
|
|
267
|
+
} else if (prefix) {
|
|
268
|
+
out.set(prefix, v);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function changedPaths(before: unknown, after: unknown, root: string): IndexPropertyChange[] {
|
|
273
|
+
const a = new Map<string, unknown>();
|
|
274
|
+
const b = new Map<string, unknown>();
|
|
275
|
+
leaves(before ?? {}, root, a);
|
|
276
|
+
leaves(after ?? {}, root, b);
|
|
277
|
+
const out: IndexPropertyChange[] = [];
|
|
278
|
+
for (const path of [...new Set([...a.keys(), ...b.keys()])].sort()) {
|
|
279
|
+
const x = a.get(path);
|
|
280
|
+
const y = b.get(path);
|
|
281
|
+
if (canon(x) === canon(y)) continue;
|
|
282
|
+
out.push({ path, ...(a.has(path) ? { before: x } : {}), ...(b.has(path) ? { after: y } : {}) });
|
|
283
|
+
}
|
|
284
|
+
return out;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/** The property changes of one node between two reads. */
|
|
288
|
+
export function indexNodePropertyChanges(before: IndexDiffNode, after: IndexDiffNode): IndexPropertyChange[] {
|
|
289
|
+
const out: IndexPropertyChange[] = [];
|
|
290
|
+
if (before.kind !== after.kind) out.push({ path: "kind", before: before.kind, after: after.kind });
|
|
291
|
+
const attrs = changedPaths(before.attrs, after.attrs, "attrs");
|
|
292
|
+
out.push(...attrs);
|
|
293
|
+
if (attrs.length === 0) out.push(...changedPaths(before.tags, after.tags, "tags"));
|
|
294
|
+
return out;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function edgeKey(e: IndexDiffEdge): string {
|
|
298
|
+
return [e.from, e.to, e.kind ?? "", e.viaAttr ?? "", e.relation ?? "reference"].join("\u0000");
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function edgeRef(e: IndexDiffEdge): IndexEdgeRef {
|
|
302
|
+
return {
|
|
303
|
+
from: e.from,
|
|
304
|
+
to: e.to,
|
|
305
|
+
kind: e.kind ?? "",
|
|
306
|
+
...(e.viaAttr ? { viaAttr: e.viaAttr } : {}),
|
|
307
|
+
relation: e.relation ?? "reference",
|
|
308
|
+
...(e.origin ? { origin: e.origin } : {}),
|
|
309
|
+
...(e.rule ? { rule: e.rule } : {}),
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const refOf = (n: IndexDiffNode): IndexNodeRef => ({ key: n.id, kind: n.kind, ...indexNodeTypeAndLocation(n) });
|
|
314
|
+
const byKey = <T>([a]: [string, T], [b]: [string, T]) => (a < b ? -1 : a > b ? 1 : 0);
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* What changed from `from` to `to`, two versions of one index scope.
|
|
318
|
+
* Throws {@link IndexScopeMismatch} when their scope keys differ, and
|
|
319
|
+
* {@link NotAnIndexVersion} for a document of another shape.
|
|
320
|
+
*/
|
|
321
|
+
export function diffIndexVersions(from: IndexDiffDocument, to: IndexDiffDocument, o: IndexDiffOptions = {}): IndexDiff {
|
|
322
|
+
check(from, "first");
|
|
323
|
+
check(to, "second");
|
|
324
|
+
if (from.stamp.scopeKey !== to.stamp.scopeKey) throw new IndexScopeMismatch(from.stamp.scopeKey, to.stamp.scopeKey);
|
|
325
|
+
const out: IndexDiff = {
|
|
326
|
+
diff: "v1",
|
|
327
|
+
scopeKey: from.stamp.scopeKey,
|
|
328
|
+
from: stampOf(from, o.fromVersion),
|
|
329
|
+
to: stampOf(to, o.toVersion),
|
|
330
|
+
unchanged: false,
|
|
331
|
+
nodes: { added: [], removed: [], changed: [], notObserved: [] },
|
|
332
|
+
edges: { added: [], removed: [], notObserved: [] },
|
|
333
|
+
coverage: [],
|
|
334
|
+
counts: { added: 0, removed: 0, changed: 0, properties: 0, notObserved: 0, edgesAdded: 0, edgesRemoved: 0, edgesNotObserved: 0 },
|
|
335
|
+
};
|
|
336
|
+
if (from.stamp.digest && from.stamp.digest === to.stamp.digest) {
|
|
337
|
+
out.unchanged = true;
|
|
338
|
+
return out;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const absence = (doc: IndexDiffDocument, n: IndexDiffNode): IndexAbsence => {
|
|
342
|
+
const { listedType, location } = indexNodeTypeAndLocation(n);
|
|
343
|
+
return indexAbsence(doc.coverage, listedType, location);
|
|
344
|
+
};
|
|
345
|
+
const unobserved = (n: IndexDiffNode, a: Extract<IndexAbsence, { kind: "not-observed" }>, side: "from" | "to"): IndexNodeNotObserved => ({
|
|
346
|
+
...refOf(n),
|
|
347
|
+
side,
|
|
348
|
+
verdict: a.verdict,
|
|
349
|
+
...(a.detail ? { detail: a.detail } : {}),
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
const before = new Map(from.graph.nodes.map((n) => [n.id, n]));
|
|
353
|
+
const after = new Map(to.graph.nodes.map((n) => [n.id, n]));
|
|
354
|
+
for (const [id, n] of [...before].sort(byKey)) {
|
|
355
|
+
const m = after.get(id);
|
|
356
|
+
if (m) {
|
|
357
|
+
const properties = indexNodePropertyChanges(n, m);
|
|
358
|
+
if (properties.length > 0) out.nodes.changed.push({ ...refOf(m), properties });
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
361
|
+
const a = absence(to, n);
|
|
362
|
+
if (a.kind === "removed") out.nodes.removed.push(refOf(n));
|
|
363
|
+
else out.nodes.notObserved.push(unobserved(n, a, "to"));
|
|
364
|
+
}
|
|
365
|
+
for (const [id, n] of [...after].sort(byKey)) {
|
|
366
|
+
if (before.has(id)) continue;
|
|
367
|
+
const a = absence(from, n);
|
|
368
|
+
if (a.kind === "removed") out.nodes.added.push(refOf(n));
|
|
369
|
+
else out.nodes.notObserved.push(unobserved(n, a, "from"));
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// An edge's ends are looked up in either read: a removed edge's ends may be gone from `to`.
|
|
373
|
+
const nodeFor = (id: string): IndexDiffNode => after.get(id) ?? before.get(id) ?? { id, kind: "" };
|
|
374
|
+
const edgesIn = (doc: IndexDiffDocument) => new Map(doc.graph.edges.map((e) => [edgeKey(e), e]));
|
|
375
|
+
const ea = edgesIn(from);
|
|
376
|
+
const eb = edgesIn(to);
|
|
377
|
+
const classify = (e: IndexDiffEdge, lacking: IndexDiffDocument, side: "from" | "to", into: IndexEdgeRef[]) => {
|
|
378
|
+
for (const end of [e.from, e.to]) {
|
|
379
|
+
const a = absence(lacking, nodeFor(end));
|
|
380
|
+
if (a.kind === "not-observed") {
|
|
381
|
+
out.edges.notObserved.push({ ...edgeRef(e), side, unobservedEnd: end, verdict: a.verdict });
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
into.push(edgeRef(e));
|
|
386
|
+
};
|
|
387
|
+
for (const [k, e] of [...ea].sort(byKey)) if (!eb.has(k)) classify(e, to, "to", out.edges.removed);
|
|
388
|
+
for (const [k, e] of [...eb].sort(byKey)) if (!ea.has(k)) classify(e, from, "from", out.edges.added);
|
|
389
|
+
|
|
390
|
+
const cov = new Map<string, { listedType: string; location: string; before?: string; after?: string }>();
|
|
391
|
+
for (const e of from.coverage) cov.set(`${e.listedType}\u0000${e.location}`, { listedType: e.listedType, location: e.location, before: e.verdict });
|
|
392
|
+
for (const e of to.coverage) {
|
|
393
|
+
const k = `${e.listedType}\u0000${e.location}`;
|
|
394
|
+
cov.set(k, { ...(cov.get(k) ?? { listedType: e.listedType, location: e.location }), after: e.verdict });
|
|
395
|
+
}
|
|
396
|
+
out.coverage = [...cov.values()]
|
|
397
|
+
.filter((c) => c.before !== c.after)
|
|
398
|
+
.sort((a, b) => (a.listedType + "\u0000" + a.location < b.listedType + "\u0000" + b.location ? -1 : 1));
|
|
399
|
+
|
|
400
|
+
out.counts = {
|
|
401
|
+
added: out.nodes.added.length,
|
|
402
|
+
removed: out.nodes.removed.length,
|
|
403
|
+
changed: out.nodes.changed.length,
|
|
404
|
+
properties: out.nodes.changed.reduce((s, c) => s + c.properties.length, 0),
|
|
405
|
+
notObserved: out.nodes.notObserved.length,
|
|
406
|
+
edgesAdded: out.edges.added.length,
|
|
407
|
+
edgesRemoved: out.edges.removed.length,
|
|
408
|
+
edgesNotObserved: out.edges.notObserved.length,
|
|
409
|
+
};
|
|
410
|
+
return out;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// ── Reading versions through the store (#4519) ──────────────────────
|
|
414
|
+
|
|
415
|
+
/** A parsed version reference: `<scopeKey>/<version>`, or `<scopeKey>` alone for its latest. */
|
|
416
|
+
export interface IndexVersionRef {
|
|
417
|
+
scopeKey: string;
|
|
418
|
+
/** Absent: the scope's latest version. */
|
|
419
|
+
version?: string;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Parse `<scopeKey>/<version>` or `<scopeKey>`. A bare version name (no
|
|
424
|
+
* scope) takes `inScope`'s scope, so `--against <version>` can name another
|
|
425
|
+
* version of the first argument's scope.
|
|
426
|
+
*/
|
|
427
|
+
export function parseIndexVersionRef(ref: string, inScope?: string): IndexVersionRef {
|
|
428
|
+
const trimmed = ref.replace(/^\/+|\/+$/g, "");
|
|
429
|
+
const slash = trimmed.lastIndexOf("/");
|
|
430
|
+
const last = slash < 0 ? trimmed : trimmed.slice(slash + 1);
|
|
431
|
+
if (isIndexVersionName(last)) {
|
|
432
|
+
if (slash >= 0) return { scopeKey: trimmed.slice(0, slash), version: last };
|
|
433
|
+
if (!inScope) throw new NotAnIndexVersion(`"${ref}" is a version name with no scope: name it as <scopeKey>/${last}`);
|
|
434
|
+
return { scopeKey: inScope, version: last };
|
|
435
|
+
}
|
|
436
|
+
if (!trimmed) throw new NotAnIndexVersion("an empty index version reference");
|
|
437
|
+
return { scopeKey: trimmed };
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/** One version read from a store, with the name it is stored under. */
|
|
441
|
+
export interface StoredIndexVersion {
|
|
442
|
+
scopeKey: string;
|
|
443
|
+
version: string;
|
|
444
|
+
doc: IndexDocument;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
/** Read one version (or a scope's latest) through the store. Throws {@link NotAnIndexVersion} when the store has none. */
|
|
448
|
+
export async function readIndexVersionRef(store: Pick<SnapshotStore, "kind" | "listIndex" | "readIndexAt">, ref: IndexVersionRef): Promise<StoredIndexVersion> {
|
|
449
|
+
const version = ref.version ?? (await store.listIndex(ref.scopeKey)).pop();
|
|
450
|
+
if (!version) throw new NotAnIndexVersion(`the ${store.kind} store holds no index version under ${ref.scopeKey}`);
|
|
451
|
+
const doc = await store.readIndexAt(ref.scopeKey, version);
|
|
452
|
+
if (!doc) throw new NotAnIndexVersion(`the ${store.kind} store holds no index version ${ref.scopeKey}/${version}`);
|
|
453
|
+
return { scopeKey: ref.scopeKey, version, doc };
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* Diff two stored versions. `to` absent: `from` names a scope (or a version
|
|
458
|
+
* in one), and the diff is the scope's last two versions, or the version
|
|
459
|
+
* before `from` against `from`. Two references under different scope paths
|
|
460
|
+
* are refused before either is read.
|
|
461
|
+
*/
|
|
462
|
+
export async function diffIndexVersionRefs(store: Pick<SnapshotStore, "kind" | "listIndex" | "readIndexAt">, from: IndexVersionRef, to?: IndexVersionRef): Promise<IndexDiff> {
|
|
463
|
+
let a = from;
|
|
464
|
+
let b = to;
|
|
465
|
+
if (!b) {
|
|
466
|
+
const versions = await store.listIndex(from.scopeKey);
|
|
467
|
+
const at = from.version ? versions.indexOf(from.version) : versions.length - 1;
|
|
468
|
+
if (at < 1) throw new NotAnIndexVersion(`the ${store.kind} store holds no version before ${from.version ?? "the latest"} under ${from.scopeKey}: name two versions`);
|
|
469
|
+
a = { scopeKey: from.scopeKey, version: versions[at - 1] };
|
|
470
|
+
b = { scopeKey: from.scopeKey, version: versions[at] };
|
|
471
|
+
}
|
|
472
|
+
if (a.scopeKey !== b.scopeKey) throw new IndexScopeMismatch(a.scopeKey, b.scopeKey);
|
|
473
|
+
const x = await readIndexVersionRef(store, a);
|
|
474
|
+
const y = await readIndexVersionRef(store, b);
|
|
475
|
+
return diffIndexVersions(x.doc, y.doc, { fromVersion: x.version, toVersion: y.version });
|
|
476
|
+
}
|