@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,981 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The index store (#4519): one seam for every snapshot and every other index,
|
|
3
|
+
* with the store chosen per project.
|
|
4
|
+
*
|
|
5
|
+
* A snapshot is one kind of index, a chant project's observed resources. The
|
|
6
|
+
* same store holds what behold indexes from a direct cloud read and the
|
|
7
|
+
* graphs choudoufu and plunge print (INTENTIUS/behold#1025). Every index is
|
|
8
|
+
* derived, stamped, never read for a mutation decision, and safe to lose:
|
|
9
|
+
* losing the store costs history and the cost of observing again, never
|
|
10
|
+
* correctness. The rule has a test, ./index-store-load-bearing.test.ts.
|
|
11
|
+
*
|
|
12
|
+
* ## The seam
|
|
13
|
+
*
|
|
14
|
+
* {@link SnapshotStore}: write, read latest, list, read at, for snapshots;
|
|
15
|
+
* the same four for index versions. Every snapshot read and write in chant
|
|
16
|
+
* goes through it. The git plumbing in ./git.ts (`readSnapshot` and the rest)
|
|
17
|
+
* is now the read side of the orphan-branch store and nothing else.
|
|
18
|
+
*
|
|
19
|
+
* ## One logical key space, two families
|
|
20
|
+
*
|
|
21
|
+
* - `snapshots`: `<env>/<key>.json` and `_members/<member>/<env>/<key>.json`,
|
|
22
|
+
* exactly the orphan branch's layout (`snapshotStorageKey`). A snapshot
|
|
23
|
+
* keeps one key, and its history is the store's own: commits on a git ref,
|
|
24
|
+
* object versions in a versioned bucket.
|
|
25
|
+
* - `index`: `<source path>/<scope path>/<observedAt>-<digest12>.json`
|
|
26
|
+
* (./index-document.ts), one immutable object per read, written once and
|
|
27
|
+
* never overwritten, with `scope.json` beside the versions.
|
|
28
|
+
*
|
|
29
|
+
* ## Stores
|
|
30
|
+
*
|
|
31
|
+
* | kind | snapshots | index versions |
|
|
32
|
+
* |---|---|---|
|
|
33
|
+
* | `git-ref` (default) | `refs/chant/snapshots`, root of the ref | `_index/...` on the same ref |
|
|
34
|
+
* | `local` | `<dir>/snapshots/...` | `<dir>/index/...` |
|
|
35
|
+
* | `s3` | `<prefix>/snapshots/...` | `<prefix>/index/...` |
|
|
36
|
+
* | `none` | dropped | dropped |
|
|
37
|
+
* | `orphan-branch` | read-only: `chant/lifecycle` | none |
|
|
38
|
+
*
|
|
39
|
+
* The default `git-ref` store also reads the `chant/lifecycle` branch when
|
|
40
|
+
* the ref has no answer, so snapshots written before #4519 stay readable. It
|
|
41
|
+
* never writes there. A project created after #4519 has no `chant/lifecycle`
|
|
42
|
+
* branch unless it writes an Op record: gates, run ledgers, deploy records,
|
|
43
|
+
* releases and the other Ops records are not an index and stay on the branch.
|
|
44
|
+
*
|
|
45
|
+
* ## Choosing a store
|
|
46
|
+
*
|
|
47
|
+
* `snapshots` in chant.config.ts, and these environment variables over it:
|
|
48
|
+
* `CHANT_SNAPSHOT_STORE` (`git-ref`, `local`, `s3`, `none`, `orphan-branch`),
|
|
49
|
+
* `CHANT_SNAPSHOT_STORE_DIR` (local), and `CHANT_SNAPSHOT_S3_BUCKET`,
|
|
50
|
+
* `_PREFIX`, `_REGION`, `_ENDPOINT`, `_ACCESS_KEY_ID`, `_SECRET_ACCESS_KEY`,
|
|
51
|
+
* `_SESSION_TOKEN` (s3). See ./snapshot-store-s3.ts for the credential rule.
|
|
52
|
+
*
|
|
53
|
+
* Secret masking happens before a store sees the data (./snapshot.ts and
|
|
54
|
+
* ../deep-observation.ts's one list), so no store holds a secret.
|
|
55
|
+
*/
|
|
56
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, renameSync, statSync, writeFileSync, mkdtempSync, unlinkSync, rmdirSync } from "node:fs";
|
|
57
|
+
import { tmpdir } from "node:os";
|
|
58
|
+
import { dirname, join, resolve } from "node:path";
|
|
59
|
+
import { getRuntime } from "../runtime-adapter";
|
|
60
|
+
import { NEXT_RELEASE } from "../release-floors";
|
|
61
|
+
import {
|
|
62
|
+
fetchLifecycle,
|
|
63
|
+
ledgerDir,
|
|
64
|
+
listFilesInDir,
|
|
65
|
+
listLedgerTrees,
|
|
66
|
+
listSnapshots as listOrphanSnapshots,
|
|
67
|
+
readBlobFromPath,
|
|
68
|
+
readRefSha,
|
|
69
|
+
readSnapshotAt as readOrphanSnapshotAt,
|
|
70
|
+
RefCASConflictError,
|
|
71
|
+
updateRefCAS,
|
|
72
|
+
type LedgerLocation,
|
|
73
|
+
} from "./git";
|
|
74
|
+
import { ledgerRecordingOff } from "./no-record";
|
|
75
|
+
import {
|
|
76
|
+
INDEX_SCOPE_FILE,
|
|
77
|
+
indexVersionName,
|
|
78
|
+
isIndexVersionName,
|
|
79
|
+
validateIndexDocument,
|
|
80
|
+
type IndexDocument,
|
|
81
|
+
type IndexScope,
|
|
82
|
+
} from "./index-document";
|
|
83
|
+
import { S3Medium, type S3Fetch, type S3StoreSettings } from "./snapshot-store-s3";
|
|
84
|
+
|
|
85
|
+
/** The first chant that writes snapshots through the store, to `refs/chant/snapshots` by default. */
|
|
86
|
+
export const SNAPSHOT_STORE_FLOOR = NEXT_RELEASE;
|
|
87
|
+
|
|
88
|
+
/** The ref the default store writes. */
|
|
89
|
+
export const SNAPSHOT_REF = "refs/chant/snapshots";
|
|
90
|
+
|
|
91
|
+
/** Where index versions sit on a git ref. The underscore keeps it clear of every environment name, as `_gates` and `_members` are. */
|
|
92
|
+
export const GIT_INDEX_DIR = "_index";
|
|
93
|
+
|
|
94
|
+
export type SnapshotStoreKind = "git-ref" | "orphan-branch" | "local" | "s3" | "none";
|
|
95
|
+
|
|
96
|
+
/** One entry of a snapshot history, newest first. */
|
|
97
|
+
export interface SnapshotVersion {
|
|
98
|
+
/** What {@link SnapshotStore.readAt} takes: a commit on a git store, an object version id on s3, a modification time on local. */
|
|
99
|
+
version: string;
|
|
100
|
+
/** ISO time of the write. */
|
|
101
|
+
date: string;
|
|
102
|
+
message: string;
|
|
103
|
+
/** The snapshot key the entry is for, when the store keeps history per key (s3, local). */
|
|
104
|
+
key?: string;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** What a write did. A store that keeps nothing, or a run that records nothing, says so instead of throwing. */
|
|
108
|
+
export interface StoreWrite {
|
|
109
|
+
written: boolean;
|
|
110
|
+
/** The commit, object version or file time the write made. */
|
|
111
|
+
version?: string;
|
|
112
|
+
/** The store path written, relative to its family root. */
|
|
113
|
+
path?: string;
|
|
114
|
+
/** Why nothing was written. */
|
|
115
|
+
reason?: string;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** What publishing a store's writes did: pushed to a remote, kept local, or refused. */
|
|
119
|
+
export interface StorePublish {
|
|
120
|
+
status: "pushed" | "local" | "nothing" | "stale" | "failed";
|
|
121
|
+
detail?: string;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* The index store. Snapshot methods take the snapshot's environment and key
|
|
126
|
+
* (`snapshotStorageKey(lexicon, stack)`), and a {@link LedgerLocation} for a
|
|
127
|
+
* workspace member; index methods take a scope key (`stamp.scopeKey`).
|
|
128
|
+
*/
|
|
129
|
+
export interface SnapshotStore {
|
|
130
|
+
readonly kind: SnapshotStoreKind;
|
|
131
|
+
/** `refs/chant/snapshots`, a directory, `s3://bucket/prefix`, `nowhere`: for messages. */
|
|
132
|
+
readonly where: string;
|
|
133
|
+
/** False for `none` and the read-only orphan-branch store. */
|
|
134
|
+
readonly writable: boolean;
|
|
135
|
+
|
|
136
|
+
write(environment: string, key: string, json: string, at?: LedgerLocation): Promise<StoreWrite>;
|
|
137
|
+
readLatest(environment: string, key: string, at?: LedgerLocation): Promise<string | null>;
|
|
138
|
+
readAt(environment: string, key: string, version: string, at?: LedgerLocation): Promise<string | null>;
|
|
139
|
+
/** Snapshot history, newest first; with `environment`, only that environment's. */
|
|
140
|
+
list(opts?: LedgerLocation & { environment?: string }): Promise<SnapshotVersion[]>;
|
|
141
|
+
/** The snapshot keys an environment holds now. */
|
|
142
|
+
keys(environment: string, at?: LedgerLocation): Promise<string[]>;
|
|
143
|
+
/** The environments that hold a snapshot now. */
|
|
144
|
+
environments(at?: LedgerLocation): Promise<string[]>;
|
|
145
|
+
|
|
146
|
+
/** Write one index version. Refuses a stamp that disagrees with itself; never overwrites. */
|
|
147
|
+
writeIndex(doc: IndexDocument): Promise<StoreWrite>;
|
|
148
|
+
/** Version names under a scope key, oldest first (name order is time order). */
|
|
149
|
+
listIndex(scopeKey: string): Promise<string[]>;
|
|
150
|
+
readIndexAt(scopeKey: string, version: string): Promise<IndexDocument | null>;
|
|
151
|
+
readIndexLatest(scopeKey: string): Promise<IndexDocument | null>;
|
|
152
|
+
/** The scope a scope key's digest stands for, from its `scope.json`. */
|
|
153
|
+
readIndexScope(scopeKey: string): Promise<IndexScope | null>;
|
|
154
|
+
|
|
155
|
+
/** Bring remote history in, when the store has a remote. Never rewinds local writes. Never throws. */
|
|
156
|
+
fetch(at?: LedgerLocation): Promise<void>;
|
|
157
|
+
/** Send this process's writes to the remote, when the store has one. Never throws. */
|
|
158
|
+
publish(at?: LedgerLocation): Promise<StorePublish>;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Every snapshot an environment holds, by key. The old `readEnvironmentSnapshots`, through a store. */
|
|
162
|
+
export async function readEnvironmentSnapshotsFrom(
|
|
163
|
+
store: SnapshotStore,
|
|
164
|
+
environment: string,
|
|
165
|
+
at?: LedgerLocation,
|
|
166
|
+
): Promise<Map<string, string>> {
|
|
167
|
+
const out = new Map<string, string>();
|
|
168
|
+
for (const key of await store.keys(environment, at)) {
|
|
169
|
+
const content = await store.readLatest(environment, key, at);
|
|
170
|
+
if (content !== null) out.set(key, content);
|
|
171
|
+
}
|
|
172
|
+
return out;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
// ── Media ────────────────────────────────────────────────────────────────
|
|
176
|
+
|
|
177
|
+
/** Names directly under a directory. */
|
|
178
|
+
interface Listing {
|
|
179
|
+
files: string[];
|
|
180
|
+
dirs: string[];
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** Get, put and list over paths relative to the store's root. */
|
|
184
|
+
interface Medium {
|
|
185
|
+
get(path: string): Promise<string | null>;
|
|
186
|
+
put(path: string, content: string, opts?: { ifAbsent?: boolean; message?: string }): Promise<{ exists: boolean; version?: string }>;
|
|
187
|
+
list(dir: string): Promise<Listing>;
|
|
188
|
+
/** History of every path under `dir` ("" for all), newest first. */
|
|
189
|
+
history(dir: string): Promise<SnapshotVersion[]>;
|
|
190
|
+
/** The content of `path` at a version {@link history} returned. */
|
|
191
|
+
getAt(path: string, version: string): Promise<string | null>;
|
|
192
|
+
fetch(): Promise<void>;
|
|
193
|
+
publish(): Promise<StorePublish>;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const C_LOCALE = { LC_ALL: "C", LANGUAGE: "" };
|
|
197
|
+
const ISO_TIME_RE = /^\d{4}-\d{2}-\d{2}T/;
|
|
198
|
+
|
|
199
|
+
async function git(args: string[], opts: { cwd?: string; stdin?: string; env?: Record<string, string> }) {
|
|
200
|
+
return getRuntime().spawn(["git", ...args], { ...(opts.cwd ? { cwd: opts.cwd } : {}), ...(opts.stdin !== undefined ? { stdin: opts.stdin } : {}), env: { ...C_LOCALE, ...(opts.env ?? {}) } });
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** The remote a ref is pushed to: `origin` when it exists, else the first. */
|
|
204
|
+
async function remoteOf(cwd?: string): Promise<string | null> {
|
|
205
|
+
const result = await git(["remote"], { cwd });
|
|
206
|
+
if (result.exitCode !== 0) return null;
|
|
207
|
+
const names = result.stdout.trim().split("\n").map((n) => n.trim()).filter(Boolean);
|
|
208
|
+
if (names.length === 0) return null;
|
|
209
|
+
return names.includes("origin") ? "origin" : names[0];
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** `user.name`/`user.email` for a checkout that has none, as the ledger's own writes do (#2301). */
|
|
213
|
+
async function identityEnv(cwd?: string): Promise<Record<string, string>> {
|
|
214
|
+
const probe = await git(["var", "GIT_COMMITTER_IDENT"], { cwd });
|
|
215
|
+
if (probe.exitCode === 0) return {};
|
|
216
|
+
const name = process.env.CHANT_LIFECYCLE_COMMITTER_NAME?.trim() || "chant";
|
|
217
|
+
const email = process.env.CHANT_LIFECYCLE_COMMITTER_EMAIL?.trim() || "chant@localhost";
|
|
218
|
+
return { GIT_AUTHOR_NAME: name, GIT_AUTHOR_EMAIL: email, GIT_COMMITTER_NAME: name, GIT_COMMITTER_EMAIL: email };
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** One file written to a ref. */
|
|
222
|
+
export interface RefFileWrite {
|
|
223
|
+
path: string;
|
|
224
|
+
content: string;
|
|
225
|
+
/** Leave an existing file alone and report it. */
|
|
226
|
+
ifAbsent?: boolean;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Commit files onto `ref`, keeping every other path the ref holds, with a
|
|
231
|
+
* compare-and-swap on the ref. Builds the tree in a scratch index, so the
|
|
232
|
+
* working tree, the real index and HEAD are never touched. A conflict with
|
|
233
|
+
* another writer is retried against the new tip: every caller writes a
|
|
234
|
+
* self-contained replacement or a new path, so a retry is always correct.
|
|
235
|
+
*
|
|
236
|
+
* Returns the new commit, or null when every `ifAbsent` file existed.
|
|
237
|
+
*/
|
|
238
|
+
export async function commitFilesToRef(
|
|
239
|
+
ref: string,
|
|
240
|
+
files: RefFileWrite[],
|
|
241
|
+
message: string,
|
|
242
|
+
opts?: {
|
|
243
|
+
cwd?: string;
|
|
244
|
+
/** Build on this commit instead of the ref's tip (`null`: a root commit). The ref is still swapped from its current value. */
|
|
245
|
+
parent?: string | null;
|
|
246
|
+
},
|
|
247
|
+
): Promise<{ commit: string | null; existed: string[] }> {
|
|
248
|
+
const cwd = opts?.cwd;
|
|
249
|
+
const top = await git(["rev-parse", "--show-toplevel"], { cwd });
|
|
250
|
+
const root = top.exitCode === 0 && top.stdout.trim() !== "" ? top.stdout.trim() : cwd;
|
|
251
|
+
const blobs: Array<{ path: string; sha: string; ifAbsent: boolean }> = [];
|
|
252
|
+
for (const f of files) {
|
|
253
|
+
if (f.path.split("/").some((s) => s === "" || s === "." || s === "..")) throw new Error(`cannot write ${f.path} on ${ref}: not a clean path`);
|
|
254
|
+
const h = await git(["hash-object", "-w", "--stdin"], { cwd: root, stdin: f.content });
|
|
255
|
+
if (h.exitCode !== 0) throw new Error(`cannot write ${f.path} on ${ref}: git hash-object failed: ${h.stderr.trim()}`);
|
|
256
|
+
blobs.push({ path: f.path, sha: h.stdout.trim(), ifAbsent: !!f.ifAbsent });
|
|
257
|
+
}
|
|
258
|
+
const ident = await identityEnv(root);
|
|
259
|
+
const scratch = mkdtempSync(join(tmpdir(), "chant-store-"));
|
|
260
|
+
const indexFile = join(scratch, "index");
|
|
261
|
+
const env = { ...ident, GIT_INDEX_FILE: indexFile };
|
|
262
|
+
let lastErr: unknown;
|
|
263
|
+
try {
|
|
264
|
+
for (let attempt = 1; attempt <= 5; attempt++) {
|
|
265
|
+
// With `parent` the commit builds on that commit (a remote tip) and swaps the ref from whatever it holds now.
|
|
266
|
+
const current = await readRefSha(ref, { cwd: root });
|
|
267
|
+
const tip = opts?.parent !== undefined ? opts.parent : current;
|
|
268
|
+
const read = await git(tip ? ["read-tree", tip] : ["read-tree", "--empty"], { cwd: root, env });
|
|
269
|
+
if (read.exitCode !== 0) throw new Error(`cannot write ${ref}: git read-tree failed: ${read.stderr.trim()}`);
|
|
270
|
+
const existed: string[] = [];
|
|
271
|
+
const writes: typeof blobs = [];
|
|
272
|
+
for (const b of blobs) {
|
|
273
|
+
if (b.ifAbsent && tip) {
|
|
274
|
+
const there = await git(["cat-file", "-e", `${tip}:${b.path}`], { cwd: root });
|
|
275
|
+
if (there.exitCode === 0) {
|
|
276
|
+
existed.push(b.path);
|
|
277
|
+
continue;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
writes.push(b);
|
|
281
|
+
}
|
|
282
|
+
if (writes.length === 0) return { commit: null, existed };
|
|
283
|
+
for (const b of writes) {
|
|
284
|
+
const u = await git(["update-index", "--add", "--cacheinfo", `100644,${b.sha},${b.path}`], { cwd: root, env });
|
|
285
|
+
if (u.exitCode !== 0) throw new Error(`cannot write ${b.path} on ${ref}: git update-index failed: ${u.stderr.trim()}`);
|
|
286
|
+
}
|
|
287
|
+
const tree = await git(["write-tree"], { cwd: root, env });
|
|
288
|
+
if (tree.exitCode !== 0) throw new Error(`cannot write ${ref}: git write-tree failed: ${tree.stderr.trim()}`);
|
|
289
|
+
const commit = await git(["commit-tree", ...(tip ? ["-p", tip] : []), "-m", message, tree.stdout.trim()], { cwd: root, env: ident });
|
|
290
|
+
if (commit.exitCode !== 0) throw new Error(`cannot write ${ref}: git commit-tree failed: ${commit.stderr.trim()}`);
|
|
291
|
+
const sha = commit.stdout.trim();
|
|
292
|
+
try {
|
|
293
|
+
await updateRefCAS(ref, sha, current, { cwd: root });
|
|
294
|
+
return { commit: sha, existed };
|
|
295
|
+
} catch (err) {
|
|
296
|
+
if (!(err instanceof RefCASConflictError)) throw err;
|
|
297
|
+
lastErr = err;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
throw lastErr;
|
|
301
|
+
} finally {
|
|
302
|
+
try {
|
|
303
|
+
if (existsSync(indexFile)) unlinkSync(indexFile);
|
|
304
|
+
rmdirSync(scratch);
|
|
305
|
+
} catch {
|
|
306
|
+
// A scratch directory left in tmpdir is harmless.
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/** `refs/chant/snapshots` → `refs/chant/remote/snapshots`: where a fetch lands, so a read never rewinds local writes. */
|
|
312
|
+
function trackingRefOf(ref: string): string {
|
|
313
|
+
return ref.replace(/^refs\/chant\//, "refs/chant/remote/");
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Fetch `ref` and fast-forward the local ref when it is behind. A local ref
|
|
318
|
+
* ahead of or diverged from the remote is kept as it is: its writes go out
|
|
319
|
+
* on the next {@link publishRef}, which rebases them.
|
|
320
|
+
*/
|
|
321
|
+
export async function fetchRefFastForward(ref: string, opts?: { cwd?: string }): Promise<"fetched" | "no-remote" | "missing" | "kept" | "failed"> {
|
|
322
|
+
const cwd = opts?.cwd;
|
|
323
|
+
const remote = await remoteOf(cwd);
|
|
324
|
+
if (!remote) return "no-remote";
|
|
325
|
+
const tracking = trackingRefOf(ref);
|
|
326
|
+
const f = await git(["fetch", "--no-tags", remote, `+${ref}:${tracking}`], { cwd });
|
|
327
|
+
if (f.exitCode !== 0) return /couldn't find remote ref|no such ref was fetched/i.test(f.stderr ?? "") ? "missing" : "failed";
|
|
328
|
+
const remoteSha = await readRefSha(tracking, { cwd });
|
|
329
|
+
if (!remoteSha) return "missing";
|
|
330
|
+
const local = await readRefSha(ref, { cwd });
|
|
331
|
+
if (local === remoteSha) return "fetched";
|
|
332
|
+
if (local === null) {
|
|
333
|
+
await updateRefCAS(ref, remoteSha, null, { cwd }).catch(() => undefined);
|
|
334
|
+
return "fetched";
|
|
335
|
+
}
|
|
336
|
+
const behind = await git(["merge-base", "--is-ancestor", local, remoteSha], { cwd });
|
|
337
|
+
if (behind.exitCode === 0) {
|
|
338
|
+
await updateRefCAS(ref, remoteSha, local, { cwd }).catch(() => undefined);
|
|
339
|
+
return "fetched";
|
|
340
|
+
}
|
|
341
|
+
return "kept";
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Push `ref` under a lease on the remote tip last fetched. A refused push
|
|
346
|
+
* (another machine pushed first) fetches, commits `pending` again on the
|
|
347
|
+
* remote tip, and pushes once more, up to three times.
|
|
348
|
+
*/
|
|
349
|
+
export async function publishRef(
|
|
350
|
+
ref: string,
|
|
351
|
+
pending: ReadonlyMap<string, string>,
|
|
352
|
+
message: string,
|
|
353
|
+
opts?: { cwd?: string },
|
|
354
|
+
): Promise<StorePublish> {
|
|
355
|
+
const cwd = opts?.cwd;
|
|
356
|
+
if (ledgerRecordingOff()) return { status: "nothing", detail: "recording is off for this run" };
|
|
357
|
+
const local = await readRefSha(ref, { cwd });
|
|
358
|
+
if (!local) return { status: "nothing" };
|
|
359
|
+
const remote = await remoteOf(cwd);
|
|
360
|
+
if (!remote) return { status: "local", detail: "no remote configured" };
|
|
361
|
+
const tracking = trackingRefOf(ref);
|
|
362
|
+
for (let attempt = 1; attempt <= 3; attempt++) {
|
|
363
|
+
const expected = await readRefSha(tracking, { cwd });
|
|
364
|
+
const tip = await readRefSha(ref, { cwd });
|
|
365
|
+
if (tip === expected) return { status: "pushed", detail: `${remote} ${ref}` };
|
|
366
|
+
const push = await git(["push", "--no-verify", `--force-with-lease=${ref}:${expected ?? ""}`, remote, `${ref}:${ref}`], { cwd });
|
|
367
|
+
if (push.exitCode === 0) {
|
|
368
|
+
if (tip) await git(["update-ref", tracking, tip], { cwd });
|
|
369
|
+
return { status: "pushed", detail: `${remote} ${ref}` };
|
|
370
|
+
}
|
|
371
|
+
// Refused: bring the remote in and put this process's writes on top of it.
|
|
372
|
+
const fetched = await fetchRefFastForward(ref, { cwd });
|
|
373
|
+
if (fetched === "failed") return { status: "failed", detail: (push.stderr ?? "").trim().split("\n")[0] };
|
|
374
|
+
const remoteTip = await readRefSha(tracking, { cwd });
|
|
375
|
+
if (pending.size === 0 || !remoteTip) return { status: "stale", detail: (push.stderr ?? "").trim().split("\n")[0] };
|
|
376
|
+
const files = [...pending].map(([path, content]) => ({ path, content }));
|
|
377
|
+
// Local commits that are not this process's writes are dropped here: a snapshot is observed again, never needed.
|
|
378
|
+
const rebuilt = await commitFilesToRef(ref, files, message, { cwd, parent: remoteTip });
|
|
379
|
+
if (rebuilt.commit === null) return { status: "pushed", detail: `${remote} ${ref}` };
|
|
380
|
+
}
|
|
381
|
+
return { status: "stale", detail: `${remote} kept moving; run the command again` };
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
class GitRefMedium implements Medium {
|
|
385
|
+
/** This process's writes, by path, for {@link publishRef} to replay on a refused push. */
|
|
386
|
+
private readonly pending = new Map<string, string>();
|
|
387
|
+
constructor(readonly ref: string, private readonly cwd?: string) {}
|
|
388
|
+
|
|
389
|
+
async get(path: string): Promise<string | null> {
|
|
390
|
+
const r = await git(["cat-file", "blob", `${this.ref}:${path}`], { cwd: this.cwd });
|
|
391
|
+
return r.exitCode === 0 ? r.stdout : null;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
async put(path: string, content: string, opts?: { ifAbsent?: boolean; message?: string }) {
|
|
395
|
+
if (ledgerRecordingOff()) throw new NotRecorded();
|
|
396
|
+
const { commit, existed } = await commitFilesToRef(this.ref, [{ path, content, ifAbsent: opts?.ifAbsent }], opts?.message ?? "Index store write", { cwd: this.cwd });
|
|
397
|
+
if (existed.includes(path)) return { exists: true };
|
|
398
|
+
this.pending.set(path, content);
|
|
399
|
+
return { exists: false, ...(commit ? { version: commit } : {}) };
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
async list(dir: string): Promise<Listing> {
|
|
403
|
+
const tip = await readRefSha(this.ref, { cwd: this.cwd });
|
|
404
|
+
if (!tip) return { files: [], dirs: [] };
|
|
405
|
+
const r = await git(["ls-tree", "--full-tree", dir === "" ? tip : `${tip}:${dir}`], { cwd: this.cwd });
|
|
406
|
+
const out: Listing = { files: [], dirs: [] };
|
|
407
|
+
if (r.exitCode !== 0) return out;
|
|
408
|
+
for (const line of r.stdout.split("\n")) {
|
|
409
|
+
const m = line.match(/^\d+\s+(\w+)\s+[0-9a-f]+\t(.+)$/);
|
|
410
|
+
if (!m) continue;
|
|
411
|
+
(m[1] === "tree" ? out.dirs : out.files).push(m[2]);
|
|
412
|
+
}
|
|
413
|
+
return out;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
async history(dir: string): Promise<SnapshotVersion[]> {
|
|
417
|
+
const tip = await readRefSha(this.ref, { cwd: this.cwd });
|
|
418
|
+
if (!tip) return [];
|
|
419
|
+
const r = await git(["log", "--format=%H %aI %s", tip, ...(dir === "" ? [] : ["--", `:(top)${dir}`])], { cwd: this.cwd });
|
|
420
|
+
if (r.exitCode !== 0) return [];
|
|
421
|
+
return r.stdout.trim().split("\n").filter(Boolean).map((line) => {
|
|
422
|
+
const [version, date, ...rest] = line.split(" ");
|
|
423
|
+
return { version, date, message: rest.join(" ") };
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
async getAt(path: string, version: string): Promise<string | null> {
|
|
428
|
+
const r = await git(["cat-file", "blob", `${version}:${path}`], { cwd: this.cwd });
|
|
429
|
+
return r.exitCode === 0 ? r.stdout : null;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
async fetch(): Promise<void> {
|
|
433
|
+
try {
|
|
434
|
+
await fetchRefFastForward(this.ref, { cwd: this.cwd });
|
|
435
|
+
} catch {
|
|
436
|
+
// A fetch that fails leaves local history as it is, which every reader handles.
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
async publish(): Promise<StorePublish> {
|
|
441
|
+
try {
|
|
442
|
+
return await publishRef(this.ref, this.pending, "Index store write", { cwd: this.cwd });
|
|
443
|
+
} catch (err) {
|
|
444
|
+
return { status: "failed", detail: err instanceof Error ? err.message : String(err) };
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
class NotRecorded extends Error {}
|
|
450
|
+
|
|
451
|
+
class LocalMedium implements Medium {
|
|
452
|
+
constructor(readonly root: string) {}
|
|
453
|
+
|
|
454
|
+
async get(path: string): Promise<string | null> {
|
|
455
|
+
try {
|
|
456
|
+
return readFileSync(join(this.root, path), "utf8");
|
|
457
|
+
} catch {
|
|
458
|
+
return null;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
async put(path: string, content: string, opts?: { ifAbsent?: boolean }) {
|
|
463
|
+
const file = join(this.root, path);
|
|
464
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
465
|
+
if (opts?.ifAbsent) {
|
|
466
|
+
try {
|
|
467
|
+
writeFileSync(file, content, { flag: "wx" });
|
|
468
|
+
} catch (err) {
|
|
469
|
+
if ((err as NodeJS.ErrnoException).code === "EEXIST") return { exists: true };
|
|
470
|
+
throw err;
|
|
471
|
+
}
|
|
472
|
+
} else {
|
|
473
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
474
|
+
writeFileSync(tmp, content);
|
|
475
|
+
renameSync(tmp, file);
|
|
476
|
+
}
|
|
477
|
+
return { exists: false, version: statSync(file).mtime.toISOString() };
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
async list(dir: string): Promise<Listing> {
|
|
481
|
+
const out: Listing = { files: [], dirs: [] };
|
|
482
|
+
try {
|
|
483
|
+
for (const e of readdirSync(join(this.root, dir), { withFileTypes: true })) {
|
|
484
|
+
if (e.name.endsWith(".tmp")) continue;
|
|
485
|
+
(e.isDirectory() ? out.dirs : out.files).push(e.name);
|
|
486
|
+
}
|
|
487
|
+
} catch {
|
|
488
|
+
// Absent: nothing there.
|
|
489
|
+
}
|
|
490
|
+
return out;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/** A directory keeps no history: one entry per file there now, at its modification time. */
|
|
494
|
+
async history(dir: string): Promise<SnapshotVersion[]> {
|
|
495
|
+
const out: SnapshotVersion[] = [];
|
|
496
|
+
const walk = async (d: string): Promise<void> => {
|
|
497
|
+
const l = await this.list(d);
|
|
498
|
+
for (const f of l.files) {
|
|
499
|
+
const rel = d === "" ? f : `${d}/${f}`;
|
|
500
|
+
const date = statSync(join(this.root, rel)).mtime.toISOString();
|
|
501
|
+
out.push({ version: date, date, message: rel, key: rel });
|
|
502
|
+
}
|
|
503
|
+
for (const sub of l.dirs) await walk(d === "" ? sub : `${d}/${sub}`);
|
|
504
|
+
};
|
|
505
|
+
await walk(dir);
|
|
506
|
+
return out.sort((a, b) => (a.date < b.date ? 1 : a.date > b.date ? -1 : 0));
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/** `latest`, or a time: the file as it is now, when it was written at or before that time. */
|
|
510
|
+
async getAt(path: string, version: string): Promise<string | null> {
|
|
511
|
+
const content = await this.get(path);
|
|
512
|
+
if (content === null || version === "latest") return content;
|
|
513
|
+
if (!ISO_TIME_RE.test(version)) return null;
|
|
514
|
+
return statSync(join(this.root, path)).mtime.toISOString() <= new Date(version).toISOString() ? content : null;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
async fetch(): Promise<void> {}
|
|
518
|
+
async publish(): Promise<StorePublish> {
|
|
519
|
+
return { status: "local", detail: this.root };
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
class S3StoreMedium implements Medium {
|
|
524
|
+
constructor(readonly s3: S3Medium) {}
|
|
525
|
+
|
|
526
|
+
get(path: string) {
|
|
527
|
+
return this.s3.get(path);
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
async put(path: string, content: string, opts?: { ifAbsent?: boolean }) {
|
|
531
|
+
const r = await this.s3.put(path, content, opts);
|
|
532
|
+
return { exists: r.exists, ...(r.versionId ? { version: r.versionId } : {}) };
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
async list(dir: string): Promise<Listing> {
|
|
536
|
+
const out: Listing = { files: [], dirs: [] };
|
|
537
|
+
for (const name of await this.s3.list(dir.replace(/\/$/, ""))) {
|
|
538
|
+
if (name.endsWith("/")) out.dirs.push(name.slice(0, -1));
|
|
539
|
+
else out.files.push(name);
|
|
540
|
+
}
|
|
541
|
+
return out;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/** The bucket's object versions under `dir`. Without versioning on, only the latest of each key. */
|
|
545
|
+
async history(dir: string): Promise<SnapshotVersion[]> {
|
|
546
|
+
return (await this.s3.versions(dir.replace(/\/$/, ""))).map((v) => ({ version: v.versionId, date: v.lastModified, message: v.key, key: v.key }));
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/** A version id, or a time: the newest version written at or before it. */
|
|
550
|
+
async getAt(path: string, version: string): Promise<string | null> {
|
|
551
|
+
if (!ISO_TIME_RE.test(version)) return this.s3.get(path, version);
|
|
552
|
+
const at = new Date(version).toISOString();
|
|
553
|
+
const versions = (await this.s3.versions(dirname(path))).filter((v) => v.key === path && new Date(v.lastModified).toISOString() <= at);
|
|
554
|
+
return versions.length === 0 ? null : this.s3.get(path, versions[0].versionId);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
async fetch(): Promise<void> {}
|
|
558
|
+
async publish(): Promise<StorePublish> {
|
|
559
|
+
return { status: "pushed", detail: this.s3.where };
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// ── Stores ───────────────────────────────────────────────────────────────
|
|
564
|
+
|
|
565
|
+
interface Layout {
|
|
566
|
+
/** Prefix of the snapshot family: `""` on a git ref, `snapshots/` elsewhere. */
|
|
567
|
+
snapshots: string;
|
|
568
|
+
/** Prefix of the index family: `_index/` on a git ref, `index/` elsewhere. */
|
|
569
|
+
index: string;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
const isSnapshotFile = (name: string): boolean => name.endsWith(".json") && name !== "observation-baseline.json";
|
|
573
|
+
|
|
574
|
+
/** A store over one medium, for the `git-ref`, `local` and `s3` kinds. */
|
|
575
|
+
class MediumStore implements SnapshotStore {
|
|
576
|
+
readonly writable = true;
|
|
577
|
+
constructor(
|
|
578
|
+
readonly kind: SnapshotStoreKind,
|
|
579
|
+
readonly where: string,
|
|
580
|
+
private readonly medium: Medium,
|
|
581
|
+
private readonly layout: Layout,
|
|
582
|
+
private readonly cwd?: string,
|
|
583
|
+
) {}
|
|
584
|
+
|
|
585
|
+
private loc(at?: LedgerLocation): LedgerLocation {
|
|
586
|
+
return { ...(this.cwd ? { cwd: this.cwd } : {}), ...(at ?? {}) };
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
private async envDir(environment: string, at?: LedgerLocation): Promise<string> {
|
|
590
|
+
return `${this.layout.snapshots}${await ledgerDir(environment, this.loc(at))}`;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
async write(environment: string, key: string, json: string, at?: LedgerLocation): Promise<StoreWrite> {
|
|
594
|
+
const path = `${await this.envDir(environment, at)}/${key}.json`;
|
|
595
|
+
try {
|
|
596
|
+
const r = await this.medium.put(path, json, { message: "State snapshot" });
|
|
597
|
+
return { written: true, path, ...(r.version ? { version: r.version } : {}) };
|
|
598
|
+
} catch (err) {
|
|
599
|
+
if (err instanceof NotRecorded) return { written: false, path, reason: "recording is off for this run" };
|
|
600
|
+
throw err;
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
async readLatest(environment: string, key: string, at?: LedgerLocation): Promise<string | null> {
|
|
605
|
+
return this.medium.get(`${await this.envDir(environment, at)}/${key}.json`);
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
async readAt(environment: string, key: string, version: string, at?: LedgerLocation): Promise<string | null> {
|
|
609
|
+
return this.medium.getAt(`${await this.envDir(environment, at)}/${key}.json`, version);
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
async list(opts?: LedgerLocation & { environment?: string }): Promise<SnapshotVersion[]> {
|
|
613
|
+
const { environment, ...at } = opts ?? {};
|
|
614
|
+
const base = `${this.layout.snapshots}${await ledgerDir(environment ?? "", this.loc(at))}`.replace(/\/$/, "");
|
|
615
|
+
const all = await this.medium.history(base);
|
|
616
|
+
// A git ref's root also holds the index family; its commits are not snapshots.
|
|
617
|
+
return all.filter((v) => !(v.key ?? "").startsWith(this.layout.index) && v.message !== "Index version");
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
async keys(environment: string, at?: LedgerLocation): Promise<string[]> {
|
|
621
|
+
const l = await this.medium.list(await this.envDir(environment, at));
|
|
622
|
+
return l.files.filter(isSnapshotFile).map((f) => f.slice(0, -".json".length)).sort();
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
async environments(at?: LedgerLocation): Promise<string[]> {
|
|
626
|
+
const base = `${this.layout.snapshots}${await ledgerDir("", this.loc(at))}`.replace(/\/$/, "");
|
|
627
|
+
return (await this.medium.list(base)).dirs.filter((d) => !d.startsWith("_")).sort();
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
async writeIndex(doc: IndexDocument): Promise<StoreWrite> {
|
|
631
|
+
validateIndexDocument(doc);
|
|
632
|
+
const { stamp } = doc;
|
|
633
|
+
const dir = `${this.layout.index}${stamp.scopeKey}`;
|
|
634
|
+
const path = `${dir}/${indexVersionName(stamp.observedAt, stamp.digest)}`;
|
|
635
|
+
try {
|
|
636
|
+
await this.medium.put(`${dir}/${INDEX_SCOPE_FILE}`, `${JSON.stringify(stamp.scope, null, 2)}\n`, { ifAbsent: true, message: "Index version" });
|
|
637
|
+
const r = await this.medium.put(path, `${JSON.stringify(doc, null, 2)}\n`, { ifAbsent: true, message: "Index version" });
|
|
638
|
+
if (r.exists) return { written: false, path, reason: "this version is already in the store, and a version is never overwritten" };
|
|
639
|
+
return { written: true, path, ...(r.version ? { version: r.version } : {}) };
|
|
640
|
+
} catch (err) {
|
|
641
|
+
if (err instanceof NotRecorded) return { written: false, path, reason: "recording is off for this run" };
|
|
642
|
+
throw err;
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
async listIndex(scopeKey: string): Promise<string[]> {
|
|
647
|
+
return (await this.medium.list(`${this.layout.index}${scopeKey}`)).files.filter(isIndexVersionName).sort();
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
async readIndexAt(scopeKey: string, version: string): Promise<IndexDocument | null> {
|
|
651
|
+
if (!isIndexVersionName(version)) return null;
|
|
652
|
+
const text = await this.medium.get(`${this.layout.index}${scopeKey}/${version}`);
|
|
653
|
+
return text === null ? null : (JSON.parse(text) as IndexDocument);
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
async readIndexLatest(scopeKey: string): Promise<IndexDocument | null> {
|
|
657
|
+
const versions = await this.listIndex(scopeKey);
|
|
658
|
+
return versions.length === 0 ? null : this.readIndexAt(scopeKey, versions[versions.length - 1]);
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
async readIndexScope(scopeKey: string): Promise<IndexScope | null> {
|
|
662
|
+
const text = await this.medium.get(`${this.layout.index}${scopeKey}/${INDEX_SCOPE_FILE}`);
|
|
663
|
+
return text === null ? null : (JSON.parse(text) as IndexScope);
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
fetch(): Promise<void> {
|
|
667
|
+
return this.medium.fetch();
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
publish(): Promise<StorePublish> {
|
|
671
|
+
return this.medium.publish();
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
/** `none`: every write is dropped and every read is empty, which is every reader's fallback. */
|
|
676
|
+
class NoneStore implements SnapshotStore {
|
|
677
|
+
readonly kind = "none" as const;
|
|
678
|
+
readonly where = "nowhere (snapshots are off)";
|
|
679
|
+
readonly writable = false;
|
|
680
|
+
private readonly dropped = (path?: string): StoreWrite => ({ written: false, ...(path ? { path } : {}), reason: "the snapshot store is none" });
|
|
681
|
+
async write(environment: string, key: string) {
|
|
682
|
+
return this.dropped(`${environment}/${key}.json`);
|
|
683
|
+
}
|
|
684
|
+
async readLatest() {
|
|
685
|
+
return null;
|
|
686
|
+
}
|
|
687
|
+
async readAt() {
|
|
688
|
+
return null;
|
|
689
|
+
}
|
|
690
|
+
async list() {
|
|
691
|
+
return [];
|
|
692
|
+
}
|
|
693
|
+
async keys() {
|
|
694
|
+
return [];
|
|
695
|
+
}
|
|
696
|
+
async environments() {
|
|
697
|
+
return [];
|
|
698
|
+
}
|
|
699
|
+
async writeIndex(doc: IndexDocument) {
|
|
700
|
+
validateIndexDocument(doc);
|
|
701
|
+
return this.dropped();
|
|
702
|
+
}
|
|
703
|
+
async listIndex() {
|
|
704
|
+
return [];
|
|
705
|
+
}
|
|
706
|
+
async readIndexAt() {
|
|
707
|
+
return null;
|
|
708
|
+
}
|
|
709
|
+
async readIndexLatest() {
|
|
710
|
+
return null;
|
|
711
|
+
}
|
|
712
|
+
async readIndexScope() {
|
|
713
|
+
return null;
|
|
714
|
+
}
|
|
715
|
+
async fetch() {}
|
|
716
|
+
async publish(): Promise<StorePublish> {
|
|
717
|
+
return { status: "nothing" };
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
/** `orphan-branch`: the snapshots on `chant/lifecycle`, read-only, so the ones written before #4519 stay readable. */
|
|
722
|
+
class OrphanBranchStore implements SnapshotStore {
|
|
723
|
+
readonly kind = "orphan-branch" as const;
|
|
724
|
+
readonly where = "chant/lifecycle (read-only)";
|
|
725
|
+
readonly writable = false;
|
|
726
|
+
constructor(private readonly cwd?: string) {}
|
|
727
|
+
private loc(at?: LedgerLocation): LedgerLocation {
|
|
728
|
+
return { ...(this.cwd ? { cwd: this.cwd } : {}), ...(at ?? {}) };
|
|
729
|
+
}
|
|
730
|
+
async write(environment: string, key: string): Promise<StoreWrite> {
|
|
731
|
+
return { written: false, path: `${environment}/${key}.json`, reason: "the orphan-branch store is read-only; snapshots are written to refs/chant/snapshots" };
|
|
732
|
+
}
|
|
733
|
+
readLatest(environment: string, key: string, at?: LedgerLocation) {
|
|
734
|
+
return readBlobFromPath(environment, `${key}.json`, this.loc(at));
|
|
735
|
+
}
|
|
736
|
+
readAt(environment: string, key: string, version: string, at?: LedgerLocation) {
|
|
737
|
+
return readOrphanSnapshotAt(environment, key, version, this.loc(at));
|
|
738
|
+
}
|
|
739
|
+
async list(opts?: LedgerLocation & { environment?: string }): Promise<SnapshotVersion[]> {
|
|
740
|
+
const { environment: _env, ...at } = opts ?? {};
|
|
741
|
+
return (await listOrphanSnapshots(this.loc(at)))
|
|
742
|
+
.filter((e) => e.message === "State snapshot")
|
|
743
|
+
.map((e) => ({ version: e.commit, date: e.date, message: e.message }));
|
|
744
|
+
}
|
|
745
|
+
async keys(environment: string, at?: LedgerLocation) {
|
|
746
|
+
return (await listFilesInDir(environment, this.loc(at))).filter(isSnapshotFile).map((f) => f.slice(0, -".json".length)).sort();
|
|
747
|
+
}
|
|
748
|
+
async environments(at?: LedgerLocation) {
|
|
749
|
+
const out: string[] = [];
|
|
750
|
+
for (const t of await listLedgerTrees(this.loc(at))) {
|
|
751
|
+
if (t.startsWith("_")) continue;
|
|
752
|
+
if ((await this.keys(t, at)).length > 0) out.push(t);
|
|
753
|
+
}
|
|
754
|
+
return out.sort();
|
|
755
|
+
}
|
|
756
|
+
async writeIndex(doc: IndexDocument): Promise<StoreWrite> {
|
|
757
|
+
validateIndexDocument(doc);
|
|
758
|
+
return { written: false, reason: "the orphan-branch store is read-only and never held an index" };
|
|
759
|
+
}
|
|
760
|
+
async listIndex() {
|
|
761
|
+
return [];
|
|
762
|
+
}
|
|
763
|
+
async readIndexAt() {
|
|
764
|
+
return null;
|
|
765
|
+
}
|
|
766
|
+
async readIndexLatest() {
|
|
767
|
+
return null;
|
|
768
|
+
}
|
|
769
|
+
async readIndexScope() {
|
|
770
|
+
return null;
|
|
771
|
+
}
|
|
772
|
+
async fetch(): Promise<void> {
|
|
773
|
+
try {
|
|
774
|
+
await fetchLifecycle(this.loc());
|
|
775
|
+
} catch {
|
|
776
|
+
// Unreadable remote: local history is what there is.
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
async publish(): Promise<StorePublish> {
|
|
780
|
+
return { status: "nothing" };
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
/**
|
|
785
|
+
* A store that writes to `primary` and reads `legacy` when `primary` has no
|
|
786
|
+
* answer: the default, `refs/chant/snapshots` over the old orphan branch.
|
|
787
|
+
*/
|
|
788
|
+
class FallbackStore implements SnapshotStore {
|
|
789
|
+
constructor(private readonly primary: SnapshotStore, private readonly legacy: SnapshotStore) {}
|
|
790
|
+
get kind() {
|
|
791
|
+
return this.primary.kind;
|
|
792
|
+
}
|
|
793
|
+
get where() {
|
|
794
|
+
return `${this.primary.where} (reading ${this.legacy.where} for older snapshots)`;
|
|
795
|
+
}
|
|
796
|
+
get writable() {
|
|
797
|
+
return this.primary.writable;
|
|
798
|
+
}
|
|
799
|
+
write(environment: string, key: string, json: string, at?: LedgerLocation) {
|
|
800
|
+
return this.primary.write(environment, key, json, at);
|
|
801
|
+
}
|
|
802
|
+
async readLatest(environment: string, key: string, at?: LedgerLocation) {
|
|
803
|
+
return (await this.primary.readLatest(environment, key, at)) ?? this.legacy.readLatest(environment, key, at);
|
|
804
|
+
}
|
|
805
|
+
async readAt(environment: string, key: string, version: string, at?: LedgerLocation) {
|
|
806
|
+
// Both are git commits with the same layout, so one version id reads from either history.
|
|
807
|
+
return (await this.primary.readAt(environment, key, version, at)) ?? this.legacy.readAt(environment, key, version, at);
|
|
808
|
+
}
|
|
809
|
+
async list(opts?: LedgerLocation & { environment?: string }) {
|
|
810
|
+
const merged = [...(await this.primary.list(opts)), ...(await this.legacy.list(opts))];
|
|
811
|
+
return merged.sort((a, b) => (a.date < b.date ? 1 : a.date > b.date ? -1 : 0));
|
|
812
|
+
}
|
|
813
|
+
async keys(environment: string, at?: LedgerLocation) {
|
|
814
|
+
return [...new Set([...(await this.primary.keys(environment, at)), ...(await this.legacy.keys(environment, at))])].sort();
|
|
815
|
+
}
|
|
816
|
+
async environments(at?: LedgerLocation) {
|
|
817
|
+
return [...new Set([...(await this.primary.environments(at)), ...(await this.legacy.environments(at))])].sort();
|
|
818
|
+
}
|
|
819
|
+
writeIndex(doc: IndexDocument) {
|
|
820
|
+
return this.primary.writeIndex(doc);
|
|
821
|
+
}
|
|
822
|
+
listIndex(scopeKey: string) {
|
|
823
|
+
return this.primary.listIndex(scopeKey);
|
|
824
|
+
}
|
|
825
|
+
readIndexAt(scopeKey: string, version: string) {
|
|
826
|
+
return this.primary.readIndexAt(scopeKey, version);
|
|
827
|
+
}
|
|
828
|
+
readIndexLatest(scopeKey: string) {
|
|
829
|
+
return this.primary.readIndexLatest(scopeKey);
|
|
830
|
+
}
|
|
831
|
+
readIndexScope(scopeKey: string) {
|
|
832
|
+
return this.primary.readIndexScope(scopeKey);
|
|
833
|
+
}
|
|
834
|
+
async fetch(at?: LedgerLocation) {
|
|
835
|
+
await this.primary.fetch(at);
|
|
836
|
+
await this.legacy.fetch(at);
|
|
837
|
+
}
|
|
838
|
+
publish(at?: LedgerLocation) {
|
|
839
|
+
return this.primary.publish(at);
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
// ── Configuration ────────────────────────────────────────────────────────
|
|
844
|
+
|
|
845
|
+
/** `snapshots` in chant.config.ts. */
|
|
846
|
+
export type SnapshotStoreConfig =
|
|
847
|
+
| { store: "git-ref"; ref?: string; /** Read `chant/lifecycle` when the ref has no answer. Default true. */ readOrphanBranch?: boolean }
|
|
848
|
+
| { store: "orphan-branch" }
|
|
849
|
+
| { store: "local"; /** Relative to the project. Default `.chant/store`. */ dir?: string }
|
|
850
|
+
| ({ store: "s3" } & S3StoreSettings)
|
|
851
|
+
| { store: "none" };
|
|
852
|
+
|
|
853
|
+
export const SNAPSHOT_STORE_KINDS: readonly SnapshotStoreKind[] = ["git-ref", "orphan-branch", "local", "s3", "none"];
|
|
854
|
+
|
|
855
|
+
/** Default directory of a `local` store, relative to the project. */
|
|
856
|
+
export const DEFAULT_LOCAL_STORE_DIR = ".chant/store";
|
|
857
|
+
|
|
858
|
+
/**
|
|
859
|
+
* The store configuration in effect: `CHANT_SNAPSHOT_STORE` and its
|
|
860
|
+
* companions over `snapshots` in chant.config.ts over the default `git-ref`.
|
|
861
|
+
*/
|
|
862
|
+
export function snapshotStoreConfig(
|
|
863
|
+
configured?: unknown,
|
|
864
|
+
env: Record<string, string | undefined> = process.env,
|
|
865
|
+
): SnapshotStoreConfig {
|
|
866
|
+
const base = (configured && typeof configured === "object" ? configured : {}) as Record<string, unknown>;
|
|
867
|
+
const kind = (env.CHANT_SNAPSHOT_STORE?.trim() || (base.store as string | undefined) || "git-ref") as SnapshotStoreKind;
|
|
868
|
+
if (!SNAPSHOT_STORE_KINDS.includes(kind)) {
|
|
869
|
+
throw new Error(`snapshot store "${kind}" is not one of ${SNAPSHOT_STORE_KINDS.join(", ")}`);
|
|
870
|
+
}
|
|
871
|
+
const fromBase = base.store === kind ? base : {};
|
|
872
|
+
switch (kind) {
|
|
873
|
+
case "git-ref":
|
|
874
|
+
return { store: "git-ref", ...(typeof fromBase.ref === "string" ? { ref: fromBase.ref } : {}), ...(fromBase.readOrphanBranch === false ? { readOrphanBranch: false } : {}) };
|
|
875
|
+
case "orphan-branch":
|
|
876
|
+
return { store: "orphan-branch" };
|
|
877
|
+
case "none":
|
|
878
|
+
return { store: "none" };
|
|
879
|
+
case "local": {
|
|
880
|
+
const dir = env.CHANT_SNAPSHOT_STORE_DIR?.trim() || (typeof fromBase.dir === "string" ? fromBase.dir : undefined);
|
|
881
|
+
return { store: "local", ...(dir ? { dir } : {}) };
|
|
882
|
+
}
|
|
883
|
+
case "s3": {
|
|
884
|
+
const pick = (envName: string, key: string): string | undefined => env[envName]?.trim() || (typeof fromBase[key] === "string" ? (fromBase[key] as string) : undefined);
|
|
885
|
+
const bucket = pick("CHANT_SNAPSHOT_S3_BUCKET", "bucket");
|
|
886
|
+
if (!bucket) throw new Error("the s3 snapshot store needs a bucket: snapshots.bucket in chant.config.ts or CHANT_SNAPSHOT_S3_BUCKET");
|
|
887
|
+
const prefix = pick("CHANT_SNAPSHOT_S3_PREFIX", "prefix");
|
|
888
|
+
const region = pick("CHANT_SNAPSHOT_S3_REGION", "region");
|
|
889
|
+
const endpoint = pick("CHANT_SNAPSHOT_S3_ENDPOINT", "endpoint");
|
|
890
|
+
const creds = fromBase.credentials as S3StoreSettings["credentials"] | undefined;
|
|
891
|
+
return {
|
|
892
|
+
store: "s3",
|
|
893
|
+
bucket,
|
|
894
|
+
...(prefix ? { prefix } : {}),
|
|
895
|
+
...(region ? { region } : {}),
|
|
896
|
+
...(endpoint ? { endpoint } : {}),
|
|
897
|
+
...(typeof fromBase.forcePathStyle === "boolean" ? { forcePathStyle: fromBase.forcePathStyle } : {}),
|
|
898
|
+
...(creds ? { credentials: creds } : {}),
|
|
899
|
+
};
|
|
900
|
+
}
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
/** Seams for tests: the s3 transport and the environment its credentials come from. */
|
|
905
|
+
export interface SnapshotStoreDeps {
|
|
906
|
+
fetch?: S3Fetch;
|
|
907
|
+
env?: Record<string, string | undefined>;
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
/** Build a store from its configuration. `cwd` is the project directory. */
|
|
911
|
+
export function createSnapshotStore(config: SnapshotStoreConfig, opts?: { cwd?: string } & SnapshotStoreDeps): SnapshotStore {
|
|
912
|
+
const cwd = opts?.cwd;
|
|
913
|
+
switch (config.store) {
|
|
914
|
+
case "git-ref": {
|
|
915
|
+
const ref = config.ref ?? SNAPSHOT_REF;
|
|
916
|
+
const store = new MediumStore("git-ref", ref, new GitRefMedium(ref, cwd), { snapshots: "", index: `${GIT_INDEX_DIR}/` }, cwd);
|
|
917
|
+
return config.readOrphanBranch === false ? store : new FallbackStore(store, new OrphanBranchStore(cwd));
|
|
918
|
+
}
|
|
919
|
+
case "orphan-branch":
|
|
920
|
+
return new OrphanBranchStore(cwd);
|
|
921
|
+
case "local": {
|
|
922
|
+
const root = resolve(cwd ?? process.cwd(), config.dir ?? DEFAULT_LOCAL_STORE_DIR);
|
|
923
|
+
return new MediumStore("local", root, new LocalMedium(root), { snapshots: "snapshots/", index: "index/" }, cwd);
|
|
924
|
+
}
|
|
925
|
+
case "s3": {
|
|
926
|
+
const { store: _s, ...settings } = config;
|
|
927
|
+
const s3 = new S3Medium(settings, { ...(opts?.fetch ? { fetch: opts.fetch } : {}), ...(opts?.env ? { env: opts.env } : {}) });
|
|
928
|
+
return new MediumStore("s3", s3.where, new S3StoreMedium(s3), { snapshots: "snapshots/", index: "index/" }, cwd);
|
|
929
|
+
}
|
|
930
|
+
case "none":
|
|
931
|
+
return new NoneStore();
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
const resolved = new Map<string, SnapshotStore>();
|
|
936
|
+
|
|
937
|
+
/**
|
|
938
|
+
* The project's store, built once per directory and configuration so a
|
|
939
|
+
* write and the publish after it share one instance. `config` is the
|
|
940
|
+
* project's loaded chant.config; without it the config is loaded from `cwd`
|
|
941
|
+
* upward, and a directory with none uses the default.
|
|
942
|
+
*/
|
|
943
|
+
export async function resolveSnapshotStore(opts?: { cwd?: string; config?: { snapshots?: unknown } }): Promise<SnapshotStore> {
|
|
944
|
+
const cwd = resolve(opts?.cwd ?? process.cwd());
|
|
945
|
+
let configured = opts?.config?.snapshots;
|
|
946
|
+
if (opts?.config === undefined) {
|
|
947
|
+
try {
|
|
948
|
+
const { loadChantConfigUpward } = await import("../config");
|
|
949
|
+
configured = ((await loadChantConfigUpward(cwd)).config as { snapshots?: unknown }).snapshots;
|
|
950
|
+
} catch {
|
|
951
|
+
configured = undefined;
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
const config = snapshotStoreConfig(configured);
|
|
955
|
+
const cacheKey = `${cwd}\0${JSON.stringify(config)}`;
|
|
956
|
+
let store = resolved.get(cacheKey);
|
|
957
|
+
if (!store) {
|
|
958
|
+
store = createSnapshotStore(config, { cwd });
|
|
959
|
+
resolved.set(cacheKey, store);
|
|
960
|
+
}
|
|
961
|
+
return store;
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
/** Forget every resolved store. For tests. */
|
|
965
|
+
export function resetResolvedSnapshotStores(): void {
|
|
966
|
+
resolved.clear();
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
/**
|
|
970
|
+
* The snapshot family as a {@link import("./ledger-port").LedgerReadPort}:
|
|
971
|
+
* `trees` are the environments, `list(env)` the snapshot files, `read` a file.
|
|
972
|
+
* `chant query` reads its `resources` and `snapshots` tables through this, so
|
|
973
|
+
* the tables are the same whichever store holds them.
|
|
974
|
+
*/
|
|
975
|
+
export function snapshotReadPort(store: SnapshotStore, at?: LedgerLocation) {
|
|
976
|
+
return {
|
|
977
|
+
read: async (dir: string, file: string) => (file.endsWith(".json") ? store.readLatest(dir, file.slice(0, -".json".length), at) : null),
|
|
978
|
+
list: async (dir: string) => (await store.keys(dir, at)).map((k) => `${k}.json`),
|
|
979
|
+
trees: () => store.environments(at),
|
|
980
|
+
};
|
|
981
|
+
}
|