@intentius/chant 0.80.0 → 0.81.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/audit/discover.d.ts +8 -16
- package/dist/audit/discover.d.ts.map +1 -1
- package/dist/audit/report-html.d.ts +5 -0
- package/dist/audit/report-html.d.ts.map +1 -1
- package/dist/audit/report-model.d.ts +13 -0
- package/dist/audit/report-model.d.ts.map +1 -1
- package/dist/audit/terraform-state.d.ts +4 -5
- package/dist/audit/terraform-state.d.ts.map +1 -1
- package/dist/cli/commands/audit.d.ts +25 -9
- package/dist/cli/commands/audit.d.ts.map +1 -1
- package/dist/cli/commands/check-lexicon.d.ts +8 -0
- package/dist/cli/commands/check-lexicon.d.ts.map +1 -1
- package/dist/cli/commands/doctor.d.ts.map +1 -1
- package/dist/cli/commands/import-agents.d.ts.map +1 -1
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/lint.d.ts.map +1 -1
- package/dist/cli/commands/update.d.ts +6 -0
- package/dist/cli/commands/update.d.ts.map +1 -1
- package/dist/cli/commands/vendor.d.ts +5 -0
- package/dist/cli/commands/vendor.d.ts.map +1 -1
- package/dist/cli/handlers/build.d.ts.map +1 -1
- package/dist/cli/handlers/components.d.ts.map +1 -1
- package/dist/cli/handlers/dev.d.ts.map +1 -1
- package/dist/cli/handlers/fan-out.d.ts.map +1 -1
- package/dist/cli/handlers/init.d.ts.map +1 -1
- package/dist/cli/handlers/operator.d.ts +7 -0
- package/dist/cli/handlers/operator.d.ts.map +1 -1
- package/dist/cli/handlers/promote.d.ts +7 -4
- package/dist/cli/handlers/promote.d.ts.map +1 -1
- package/dist/cli/handlers/run-generate.d.ts.map +1 -1
- package/dist/cli/handlers/run.d.ts +4 -1
- package/dist/cli/handlers/run.d.ts.map +1 -1
- package/dist/cli/handlers/vendor.d.ts +8 -3
- package/dist/cli/handlers/vendor.d.ts.map +1 -1
- package/dist/cli/main.d.ts +12 -0
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/plugins.d.ts +12 -7
- package/dist/cli/plugins.d.ts.map +1 -1
- package/dist/cli/registry.d.ts +32 -0
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/codegen/fetch.d.ts +10 -0
- package/dist/codegen/fetch.d.ts.map +1 -1
- package/dist/components/cli-support.d.ts +3 -1
- package/dist/components/cli-support.d.ts.map +1 -1
- package/dist/components/discover.d.ts.map +1 -1
- package/dist/components/driver.d.ts +16 -2
- package/dist/components/driver.d.ts.map +1 -1
- package/dist/components/gate-plan.d.ts +40 -0
- package/dist/components/gate-plan.d.ts.map +1 -0
- package/dist/components/promote.d.ts +60 -7
- package/dist/components/promote.d.ts.map +1 -1
- package/dist/components/sandbox/driver.d.ts.map +1 -1
- package/dist/components/sandbox/run.d.ts.map +1 -1
- package/dist/components/verbs/build-archive.d.ts +22 -11
- package/dist/components/verbs/build-archive.d.ts.map +1 -1
- package/dist/components/verbs/run-agent.d.ts.map +1 -1
- package/dist/config-static.d.ts +59 -0
- package/dist/config-static.d.ts.map +1 -0
- package/dist/config.d.ts +32 -10
- package/dist/config.d.ts.map +1 -1
- package/dist/content-digest.d.ts +17 -0
- package/dist/content-digest.d.ts.map +1 -0
- package/dist/discovery/files.d.ts +10 -5
- package/dist/discovery/files.d.ts.map +1 -1
- package/dist/discovery/fold-import.d.ts +20 -4
- package/dist/discovery/fold-import.d.ts.map +1 -1
- package/dist/discovery/index.d.ts.map +1 -1
- package/dist/discovery/sandbox/driver.d.ts +20 -0
- package/dist/discovery/sandbox/driver.d.ts.map +1 -1
- package/dist/discovery/sandbox/fork.d.ts +20 -0
- package/dist/discovery/sandbox/fork.d.ts.map +1 -1
- package/dist/discovery/walk.d.ts +119 -0
- package/dist/discovery/walk.d.ts.map +1 -0
- package/dist/graph-ir.d.ts +4 -1
- package/dist/graph-ir.d.ts.map +1 -1
- package/dist/join-key.d.ts +31 -0
- package/dist/join-key.d.ts.map +1 -0
- package/dist/lexicon-module.d.ts +51 -6
- package/dist/lexicon-module.d.ts.map +1 -1
- package/dist/lexicon.d.ts +58 -1
- package/dist/lexicon.d.ts.map +1 -1
- package/dist/lifecycle/build-ledger-store.d.ts.map +1 -1
- package/dist/lifecycle/digest.d.ts +5 -1
- package/dist/lifecycle/digest.d.ts.map +1 -1
- package/dist/lifecycle/gate-ledger.d.ts +20 -5
- package/dist/lifecycle/gate-ledger.d.ts.map +1 -1
- package/dist/lifecycle/git.d.ts +64 -0
- package/dist/lifecycle/git.d.ts.map +1 -1
- package/dist/lifecycle/lease.d.ts +8 -1
- package/dist/lifecycle/lease.d.ts.map +1 -1
- package/dist/lifecycle/legacy-digest.d.ts +41 -8
- package/dist/lifecycle/legacy-digest.d.ts.map +1 -1
- package/dist/lifecycle/member-ledger.d.ts +67 -0
- package/dist/lifecycle/member-ledger.d.ts.map +1 -0
- package/dist/lifecycle/plan-digest.d.ts +4 -3
- package/dist/lifecycle/plan-digest.d.ts.map +1 -1
- package/dist/lifecycle/release-ledger.d.ts +18 -2
- package/dist/lifecycle/release-ledger.d.ts.map +1 -1
- package/dist/lifecycle/status.d.ts +90 -5
- package/dist/lifecycle/status.d.ts.map +1 -1
- package/dist/op/activities/activity-contracts.d.ts +32 -0
- package/dist/op/activities/activity-contracts.d.ts.map +1 -1
- package/dist/op/activities/index.d.ts +2 -0
- package/dist/op/activities/index.d.ts.map +1 -1
- package/dist/op/activities/propose-upgrade.d.ts +75 -0
- package/dist/op/activities/propose-upgrade.d.ts.map +1 -0
- package/dist/op/discover.d.ts.map +1 -1
- package/dist/op/gate-name.d.ts +7 -0
- package/dist/op/gate-name.d.ts.map +1 -1
- package/dist/op/gate-summary.d.ts +16 -0
- package/dist/op/gate-summary.d.ts.map +1 -1
- package/dist/op/gate.d.ts +16 -2
- package/dist/op/gate.d.ts.map +1 -1
- package/dist/project-root.d.ts +26 -0
- package/dist/project-root.d.ts.map +1 -1
- package/dist/workspace/__fixtures__/contract-repo.d.ts +37 -0
- package/dist/workspace/__fixtures__/contract-repo.d.ts.map +1 -0
- package/dist/workspace/__fixtures__/two-member-workspace.d.ts +11 -0
- package/dist/workspace/__fixtures__/two-member-workspace.d.ts.map +1 -0
- package/dist/workspace/checks/generated.d.ts +156 -0
- package/dist/workspace/checks/generated.d.ts.map +1 -0
- package/dist/workspace/checks/ledgers.d.ts +78 -0
- package/dist/workspace/checks/ledgers.d.ts.map +1 -0
- package/dist/workspace/checks/links.d.ts +25 -0
- package/dist/workspace/checks/links.d.ts.map +1 -0
- package/dist/workspace/checks/pipelines.d.ts +76 -0
- package/dist/workspace/checks/pipelines.d.ts.map +1 -0
- package/dist/workspace/checks.d.ts +161 -0
- package/dist/workspace/checks.d.ts.map +1 -0
- package/dist/workspace/compose-graph.d.ts +115 -0
- package/dist/workspace/compose-graph.d.ts.map +1 -0
- package/dist/workspace/compose-reports.d.ts +68 -0
- package/dist/workspace/compose-reports.d.ts.map +1 -0
- package/dist/workspace/declaration.d.ts +220 -0
- package/dist/workspace/declaration.d.ts.map +1 -0
- package/dist/workspace/declaration.schema.json +434 -0
- package/dist/workspace/generated-files.d.ts +65 -0
- package/dist/workspace/generated-files.d.ts.map +1 -0
- package/dist/workspace/glob.d.ts +13 -0
- package/dist/workspace/glob.d.ts.map +1 -0
- package/dist/workspace/graph-cli.d.ts +75 -0
- package/dist/workspace/graph-cli.d.ts.map +1 -0
- package/dist/workspace/init.d.ts +96 -0
- package/dist/workspace/init.d.ts.map +1 -0
- package/dist/workspace/jsonc.d.ts +38 -0
- package/dist/workspace/jsonc.d.ts.map +1 -0
- package/dist/workspace/kinds.d.ts +192 -0
- package/dist/workspace/kinds.d.ts.map +1 -0
- package/dist/workspace/lineage-check.d.ts +84 -0
- package/dist/workspace/lineage-check.d.ts.map +1 -0
- package/dist/workspace/lineage-cli.d.ts +31 -0
- package/dist/workspace/lineage-cli.d.ts.map +1 -0
- package/dist/workspace/lineage-init.d.ts +101 -0
- package/dist/workspace/lineage-init.d.ts.map +1 -0
- package/dist/workspace/lineage-lock.d.ts +258 -0
- package/dist/workspace/lineage-lock.d.ts.map +1 -0
- package/dist/workspace/lineage-merge.d.ts +14 -0
- package/dist/workspace/lineage-merge.d.ts.map +1 -0
- package/dist/workspace/lineage-migrations.d.ts +178 -0
- package/dist/workspace/lineage-migrations.d.ts.map +1 -0
- package/dist/workspace/lineage-update.d.ts +59 -0
- package/dist/workspace/lineage-update.d.ts.map +1 -0
- package/dist/workspace/lineage-upgrade-cli.d.ts +40 -0
- package/dist/workspace/lineage-upgrade-cli.d.ts.map +1 -0
- package/dist/workspace/lineage-upgrade.d.ts +146 -0
- package/dist/workspace/lineage-upgrade.d.ts.map +1 -0
- package/dist/workspace/lineage-vendor.d.ts +63 -0
- package/dist/workspace/lineage-vendor.d.ts.map +1 -0
- package/dist/workspace/lineage-version.d.ts +32 -0
- package/dist/workspace/lineage-version.d.ts.map +1 -0
- package/dist/workspace/links.d.ts +150 -0
- package/dist/workspace/links.d.ts.map +1 -0
- package/dist/workspace/ls.d.ts +99 -0
- package/dist/workspace/ls.d.ts.map +1 -0
- package/dist/workspace/member-commands.d.ts +139 -0
- package/dist/workspace/member-commands.d.ts.map +1 -0
- package/dist/workspace/member-handles.d.ts +54 -0
- package/dist/workspace/member-handles.d.ts.map +1 -0
- package/dist/workspace/member-pipeline.d.ts +143 -0
- package/dist/workspace/member-pipeline.d.ts.map +1 -0
- package/dist/workspace/member-run.d.ts +67 -0
- package/dist/workspace/member-run.d.ts.map +1 -0
- package/dist/workspace/reason-codes.d.ts +65 -0
- package/dist/workspace/reason-codes.d.ts.map +1 -0
- package/dist/workspace/records-cli.d.ts +28 -4
- package/dist/workspace/records-cli.d.ts.map +1 -1
- package/dist/workspace/records.d.ts.map +1 -1
- package/dist/workspace/root-refusal.d.ts +53 -0
- package/dist/workspace/root-refusal.d.ts.map +1 -0
- package/dist/workspace/status.d.ts +150 -0
- package/dist/workspace/status.d.ts.map +1 -0
- package/dist/workspace/template-manifest.d.ts +90 -0
- package/dist/workspace/template-manifest.d.ts.map +1 -0
- package/dist/workspace/tree.d.ts +36 -0
- package/dist/workspace/tree.d.ts.map +1 -0
- package/dist/workspace/trust/attestor.d.ts +67 -0
- package/dist/workspace/trust/attestor.d.ts.map +1 -0
- package/dist/workspace/trust/policy.d.ts +111 -0
- package/dist/workspace/trust/policy.d.ts.map +1 -0
- package/dist/workspace/trust/provenance.d.ts +67 -0
- package/dist/workspace/trust/provenance.d.ts.map +1 -0
- package/dist/workspace/trust/ssh-commit.d.ts +43 -0
- package/dist/workspace/trust/ssh-commit.d.ts.map +1 -0
- package/dist/workspace/trust/test-repo.d.ts +42 -0
- package/dist/workspace/trust/test-repo.d.ts.map +1 -0
- package/dist/workspace/trust/verify-cli.d.ts +9 -0
- package/dist/workspace/trust/verify-cli.d.ts.map +1 -0
- package/dist/workspace/trust/verify.d.ts +66 -0
- package/dist/workspace/trust/verify.d.ts.map +1 -0
- package/dist/workspace/which-chant.d.ts +58 -0
- package/dist/workspace/which-chant.d.ts.map +1 -0
- package/dist/workspace/workspace-kinds.schema.json +133 -0
- package/package.json +2 -1
- package/src/audit/discover.ts +66 -78
- package/src/audit/report-html.ts +3 -1
- package/src/audit/report-model.ts +14 -1
- package/src/audit/terraform-state.test.ts +4 -9
- package/src/audit/terraform-state.ts +4 -5
- package/src/cli/commands/audit-truncation.test.ts +167 -0
- package/src/cli/commands/audit.ts +89 -60
- package/src/cli/commands/check-lexicon.ts +24 -0
- package/src/cli/commands/doctor.test.ts +30 -0
- package/src/cli/commands/doctor.ts +18 -0
- package/src/cli/commands/import-agents.ts +6 -2
- package/src/cli/commands/init.ts +22 -3
- package/src/cli/commands/lint.test.ts +17 -23
- package/src/cli/commands/lint.ts +24 -84
- package/src/cli/commands/update.ts +13 -4
- package/src/cli/commands/vendor.ts +11 -6
- package/src/cli/handlers/__snapshots__/build-member.test.ts.snap +38 -0
- package/src/cli/handlers/build-member.test.ts +127 -0
- package/src/cli/handlers/build.ts +35 -1
- package/src/cli/handlers/components.test.ts +127 -0
- package/src/cli/handlers/components.ts +70 -8
- package/src/cli/handlers/dev.ts +5 -3
- package/src/cli/handlers/fan-out.ts +9 -2
- package/src/cli/handlers/init.ts +54 -1
- package/src/cli/handlers/operator.test.ts +68 -0
- package/src/cli/handlers/operator.ts +37 -9
- package/src/cli/handlers/promote.test.ts +118 -2
- package/src/cli/handlers/promote.ts +82 -16
- package/src/cli/handlers/run-generate-member.test.ts +83 -0
- package/src/cli/handlers/run-generate.test.ts +6 -8
- package/src/cli/handlers/run-generate.ts +36 -1
- package/src/cli/handlers/run.test.ts +47 -0
- package/src/cli/handlers/run.ts +28 -5
- package/src/cli/handlers/vendor.ts +147 -43
- package/src/cli/main.test.ts +105 -1
- package/src/cli/main.ts +230 -17
- package/src/cli/plugins.ts +17 -17
- package/src/cli/registry.ts +32 -0
- package/src/cli/static-config-read.test.ts +208 -0
- package/src/codegen/fetch.test.ts +63 -1
- package/src/codegen/fetch.ts +30 -1
- package/src/components/cli-support.ts +6 -1
- package/src/components/discover.test.ts +27 -57
- package/src/components/discover.ts +13 -68
- package/src/components/driver.test.ts +78 -1
- package/src/components/driver.ts +43 -7
- package/src/components/gate-plan.ts +53 -0
- package/src/components/promote.test.ts +276 -2
- package/src/components/promote.ts +119 -15
- package/src/components/sandbox/driver.ts +8 -8
- package/src/components/sandbox/run.ts +4 -1
- package/src/components/verbs/build-archive.ts +27 -21
- package/src/components/verbs/run-agent.ts +3 -5
- package/src/config-static.test.ts +136 -0
- package/src/config-static.ts +232 -0
- package/src/config.test.ts +2 -2
- package/src/config.ts +48 -15
- package/src/content-digest.test.ts +120 -0
- package/src/content-digest.ts +21 -0
- package/src/discovery/files.test.ts +10 -10
- package/src/discovery/files.ts +25 -81
- package/src/discovery/fold-import.ts +49 -5
- package/src/discovery/index.ts +1 -0
- package/src/discovery/sandbox/driver.ts +73 -35
- package/src/discovery/sandbox/fork-diagnostic.test.ts +136 -13
- package/src/discovery/sandbox/fork.ts +83 -43
- package/src/discovery/sandbox/path-lexicon-multifile.test.ts +285 -0
- package/src/discovery/walk.test.ts +263 -0
- package/src/discovery/walk.ts +334 -0
- package/src/effect-receipt.ts +2 -2
- package/src/graph-ir.ts +4 -1
- package/src/join-key.test.ts +25 -0
- package/src/join-key.ts +39 -0
- package/src/lexicon-module.ts +108 -10
- package/src/lexicon.ts +79 -1
- package/src/lifecycle/build-ledger-store.ts +11 -10
- package/src/lifecycle/digest.ts +9 -5
- package/src/lifecycle/gate-ledger.test.ts +20 -0
- package/src/lifecycle/gate-ledger.ts +28 -6
- package/src/lifecycle/git.test.ts +13 -1
- package/src/lifecycle/git.ts +221 -176
- package/src/lifecycle/lease.ts +25 -10
- package/src/lifecycle/legacy-digest.test.ts +115 -23
- package/src/lifecycle/legacy-digest.ts +60 -19
- package/src/lifecycle/member-ledger.test.ts +319 -0
- package/src/lifecycle/member-ledger.ts +145 -0
- package/src/lifecycle/plan-digest.ts +7 -7
- package/src/lifecycle/release-ledger.test.ts +20 -0
- package/src/lifecycle/release-ledger.ts +23 -7
- package/src/lifecycle/status.test.ts +151 -1
- package/src/lifecycle/status.ts +228 -7
- package/src/op/activities/activity-contracts.ts +37 -0
- package/src/op/activities/index.ts +10 -0
- package/src/op/activities/propose-upgrade.ts +206 -0
- package/src/op/discover.test.ts +14 -4
- package/src/op/discover.ts +19 -27
- package/src/op/gate-name.ts +8 -0
- package/src/op/gate-summary.test.ts +14 -0
- package/src/op/gate-summary.ts +25 -2
- package/src/op/gate.ts +36 -7
- package/src/op/generate-pipeline.test.ts +14 -5
- package/src/project-root.test.ts +66 -1
- package/src/project-root.ts +48 -0
- package/src/workspace/__fixtures__/contract-repo.ts +100 -0
- package/src/workspace/__fixtures__/two-member-workspace.ts +35 -0
- package/src/workspace/behold-kinds.test.ts +162 -0
- package/src/workspace/check-contract.test.ts +127 -0
- package/src/workspace/check.schema.json +347 -0
- package/src/workspace/checks/generated.test.ts +287 -0
- package/src/workspace/checks/generated.ts +475 -0
- package/src/workspace/checks/ledgers.test.ts +139 -0
- package/src/workspace/checks/ledgers.ts +246 -0
- package/src/workspace/checks/links.ts +183 -0
- package/src/workspace/checks/pipelines.test.ts +133 -0
- package/src/workspace/checks/pipelines.ts +225 -0
- package/src/workspace/checks-repo.test.ts +71 -0
- package/src/workspace/checks.test.ts +450 -0
- package/src/workspace/checks.ts +461 -0
- package/src/workspace/compose-graph.test.ts +146 -0
- package/src/workspace/compose-graph.ts +228 -0
- package/src/workspace/compose-reports.test.ts +109 -0
- package/src/workspace/compose-reports.ts +186 -0
- package/src/workspace/declaration.schema.json +434 -0
- package/src/workspace/declaration.test.ts +347 -0
- package/src/workspace/declaration.ts +623 -0
- package/src/workspace/generated-files.test.ts +114 -0
- package/src/workspace/generated-files.ts +94 -0
- package/src/workspace/glob.ts +49 -0
- package/src/workspace/graph-cli.ts +220 -0
- package/src/workspace/graph-contract.test.ts +204 -0
- package/src/workspace/graph.schema.json +662 -0
- package/src/workspace/init.test.ts +223 -0
- package/src/workspace/init.ts +585 -0
- package/src/workspace/jsonc.test.ts +65 -0
- package/src/workspace/jsonc.ts +231 -0
- package/src/workspace/kinds.test.ts +288 -0
- package/src/workspace/kinds.ts +506 -0
- package/src/workspace/lineage-check.ts +264 -0
- package/src/workspace/lineage-cli.ts +114 -0
- package/src/workspace/lineage-init.test.ts +231 -0
- package/src/workspace/lineage-init.ts +365 -0
- package/src/workspace/lineage-lock.test.ts +224 -0
- package/src/workspace/lineage-lock.ts +357 -0
- package/src/workspace/lineage-merge.ts +44 -0
- package/src/workspace/lineage-migrations.test.ts +178 -0
- package/src/workspace/lineage-migrations.ts +343 -0
- package/src/workspace/lineage-update.ts +160 -0
- package/src/workspace/lineage-upgrade-cli.ts +151 -0
- package/src/workspace/lineage-upgrade.test.ts +474 -0
- package/src/workspace/lineage-upgrade.ts +624 -0
- package/src/workspace/lineage-vendor.test.ts +126 -0
- package/src/workspace/lineage-vendor.ts +215 -0
- package/src/workspace/lineage-version.ts +127 -0
- package/src/workspace/links.test.ts +344 -0
- package/src/workspace/links.ts +331 -0
- package/src/workspace/ls-contract.test.ts +200 -0
- package/src/workspace/ls.schema.json +190 -0
- package/src/workspace/ls.ts +254 -0
- package/src/workspace/member-commands.e2e.test.ts +244 -0
- package/src/workspace/member-commands.test.ts +217 -0
- package/src/workspace/member-commands.ts +540 -0
- package/src/workspace/member-handles.test.ts +76 -0
- package/src/workspace/member-handles.ts +228 -0
- package/src/workspace/member-pipeline.test.ts +135 -0
- package/src/workspace/member-pipeline.ts +317 -0
- package/src/workspace/member-run.ts +146 -0
- package/src/workspace/read-contract.test.ts +129 -0
- package/src/workspace/reason-codes.test.ts +113 -0
- package/src/workspace/reason-codes.ts +81 -0
- package/src/workspace/records-cli.ts +86 -14
- package/src/workspace/records-contract.test.ts +5 -0
- package/src/workspace/records.schema.json +32 -2
- package/src/workspace/records.ts +3 -2
- package/src/workspace/root-refusal.ts +97 -0
- package/src/workspace/status-contract.test.ts +308 -0
- package/src/workspace/status.schema.json +210 -0
- package/src/workspace/status.ts +412 -0
- package/src/workspace/template-manifest.test.ts +79 -0
- package/src/workspace/template-manifest.ts +221 -0
- package/src/workspace/tree.ts +132 -0
- package/src/workspace/trust/attestor.ts +93 -0
- package/src/workspace/trust/policy.ts +306 -0
- package/src/workspace/trust/provenance.ts +181 -0
- package/src/workspace/trust/ssh-commit.ts +138 -0
- package/src/workspace/trust/test-repo.ts +138 -0
- package/src/workspace/trust/trust.test.ts +397 -0
- package/src/workspace/trust/verify-cli.ts +56 -0
- package/src/workspace/trust/verify.ts +194 -0
- package/src/workspace/which-chant.test.ts +117 -0
- package/src/workspace/which-chant.ts +151 -0
- package/src/workspace/workspace-kinds.schema.json +133 -0
- package/dist/components/unbound-gate-approval.d.ts +0 -24
- package/dist/components/unbound-gate-approval.d.ts.map +0 -1
- package/dist/discovery/convergence.d.ts +0 -97
- package/dist/discovery/convergence.d.ts.map +0 -1
- package/src/cli/commands/audit-walk-warnings.test.ts +0 -156
- package/src/components/unbound-gate-approval.test.ts +0 -82
- package/src/components/unbound-gate-approval.ts +0 -51
- package/src/discovery/convergence.test.ts +0 -231
- package/src/discovery/convergence.ts +0 -399
- package/src/op/discover-convergence.test.ts +0 -70
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* chant #2514 —
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
* chant #2514 — digests recorded before real SHA-256. A project whose release
|
|
3
|
+
* ledger or build manifests hold values from the old 32-bit `contentDigest`
|
|
4
|
+
* is told once; those records read back flagged `legacy-digest`, and nothing
|
|
5
|
+
* is re-keyed or rewritten.
|
|
5
6
|
*/
|
|
6
7
|
import { describe, test, expect, beforeEach, afterEach, vi } from "vitest";
|
|
7
8
|
import { withTestDir } from "@intentius/chant-test-utils";
|
|
@@ -9,11 +10,35 @@ import { spawnSync } from "node:child_process";
|
|
|
9
10
|
import { createHash } from "node:crypto";
|
|
10
11
|
import { writeFileSync } from "node:fs";
|
|
11
12
|
import { join } from "node:path";
|
|
12
|
-
import {
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
import {
|
|
14
|
+
LEGACY_DIGEST_ACCEPTED_THROUGH,
|
|
15
|
+
LEGACY_DIGEST_FLAG,
|
|
16
|
+
flagLegacyDigest,
|
|
17
|
+
isLegacyContentDigest,
|
|
18
|
+
resetLegacyDigestWarnings,
|
|
19
|
+
warnOnLegacyDigests,
|
|
20
|
+
} from "./legacy-digest";
|
|
21
|
+
import { appendReleaseRecord, readReleaseLedger, type ReleaseRecord, type ReleaseRecordInput } from "./release-ledger";
|
|
22
|
+
import {
|
|
23
|
+
findBuildManifestByArtifactDigest,
|
|
24
|
+
persistBuildManifest,
|
|
25
|
+
readAllBuildManifests,
|
|
26
|
+
readBuildManifest,
|
|
27
|
+
} from "./build-ledger-store";
|
|
28
|
+
import { readBlobFromPath } from "./git";
|
|
29
|
+
import { compareAcrossEnvironments } from "./status";
|
|
15
30
|
import { addArchiveEntry, contentDigest, createBuildArchiveManifest } from "../components/verbs/build-archive";
|
|
16
31
|
|
|
32
|
+
/** The 32-bit hash chant labelled `sha256:` up to 0.80.0, kept here to make old values. */
|
|
33
|
+
function legacyContentDigest(input: string): string {
|
|
34
|
+
let hash = 0;
|
|
35
|
+
for (let i = 0; i < input.length; i++) {
|
|
36
|
+
hash = (Math.imul(hash, 31) + input.charCodeAt(i)) | 0;
|
|
37
|
+
}
|
|
38
|
+
const hex = Math.abs(hash).toString(16).padStart(8, "0");
|
|
39
|
+
return `sha256:${hex.repeat(8).slice(0, 64)}`;
|
|
40
|
+
}
|
|
41
|
+
|
|
17
42
|
function git(args: string[], cwd: string): void {
|
|
18
43
|
spawnSync("git", args, { cwd, encoding: "utf-8" });
|
|
19
44
|
}
|
|
@@ -56,9 +81,10 @@ describe("legacy content digests (#2514)", () => {
|
|
|
56
81
|
|
|
57
82
|
const legacyWarnings = () => warn.mock.calls.filter((c: unknown[]) => String(c[0]).includes("issues/2514"));
|
|
58
83
|
|
|
59
|
-
test("
|
|
60
|
-
expect(isLegacyContentDigest(
|
|
61
|
-
expect(isLegacyContentDigest(
|
|
84
|
+
test("the old contentDigest output is recognised, and today's is not", () => {
|
|
85
|
+
expect(isLegacyContentDigest(legacyContentDigest("hello"))).toBe(true);
|
|
86
|
+
expect(isLegacyContentDigest(legacyContentDigest(""))).toBe(true);
|
|
87
|
+
expect(isLegacyContentDigest(contentDigest("hello"))).toBe(false);
|
|
62
88
|
});
|
|
63
89
|
|
|
64
90
|
test("a real SHA-256 value, a short value and a non-string are not", () => {
|
|
@@ -69,23 +95,48 @@ describe("legacy content digests (#2514)", () => {
|
|
|
69
95
|
|
|
70
96
|
test("warns once per project, and not at all without an old value", () => {
|
|
71
97
|
expect(warnOnLegacyDigests([realSha256], "release ledger", "/p/a")).toBe(false);
|
|
72
|
-
expect(warnOnLegacyDigests([
|
|
73
|
-
expect(warnOnLegacyDigests([
|
|
74
|
-
expect(warnOnLegacyDigests([
|
|
98
|
+
expect(warnOnLegacyDigests([legacyContentDigest("x")], "release ledger", "/p/a")).toBe(true);
|
|
99
|
+
expect(warnOnLegacyDigests([legacyContentDigest("y")], "build manifests", "/p/a")).toBe(false);
|
|
100
|
+
expect(warnOnLegacyDigests([legacyContentDigest("y")], "build manifests", "/p/b")).toBe(true);
|
|
75
101
|
expect(legacyWarnings()).toHaveLength(2);
|
|
76
|
-
expect(String(legacyWarnings()[0][0])).toContain("
|
|
102
|
+
expect(String(legacyWarnings()[0][0])).toContain("chant now computes real SHA-256");
|
|
103
|
+
expect(String(legacyWarnings()[0][0])).toContain(`accepts them through ${LEGACY_DIGEST_ACCEPTED_THROUGH}`);
|
|
77
104
|
});
|
|
78
105
|
|
|
79
|
-
test("
|
|
106
|
+
test("flagLegacyDigest adds the flag once, keeps other flags, and leaves real values alone", () => {
|
|
107
|
+
const old = legacyContentDigest("x");
|
|
108
|
+
const record: { id: number; flags?: string[] } = { id: 1 };
|
|
109
|
+
expect(flagLegacyDigest(record, [realSha256])).toEqual({ id: 1 });
|
|
110
|
+
expect(flagLegacyDigest(record, [undefined, old])).toEqual({ id: 1, flags: [LEGACY_DIGEST_FLAG] });
|
|
111
|
+
expect(flagLegacyDigest({ flags: ["legacy-digest"] }, [old])).toEqual({ flags: ["legacy-digest"] });
|
|
112
|
+
expect(flagLegacyDigest({ flags: ["other"] }, [old])).toEqual({ flags: ["other", "legacy-digest"] });
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("a release ledger with old and new values: old entries read back flagged, warned once, never re-keyed", async () => {
|
|
80
116
|
await withTestDir(async (dir) => {
|
|
81
117
|
initRepo(dir);
|
|
82
|
-
|
|
83
|
-
await appendReleaseRecord(release(
|
|
118
|
+
const old = legacyContentDigest("rendered");
|
|
119
|
+
await appendReleaseRecord(release(old, "run-1"), { cwd: dir });
|
|
120
|
+
await appendReleaseRecord(release(contentDigest("rendered"), "run-2"), { cwd: dir });
|
|
84
121
|
|
|
85
|
-
await readReleaseLedger("prod", { cwd: dir });
|
|
122
|
+
const { records } = await readReleaseLedger("prod", { cwd: dir });
|
|
86
123
|
await readReleaseLedger("prod", { cwd: dir });
|
|
87
124
|
expect(legacyWarnings()).toHaveLength(1);
|
|
88
125
|
expect(String(legacyWarnings()[0][0])).toContain("release ledger holds");
|
|
126
|
+
|
|
127
|
+
expect(records.map((r) => [r.runId, r.digest, r.flags])).toEqual([
|
|
128
|
+
["run-1", old, [LEGACY_DIGEST_FLAG]],
|
|
129
|
+
["run-2", contentDigest("rendered"), undefined],
|
|
130
|
+
]);
|
|
131
|
+
|
|
132
|
+
// Appending a record read back with its flag does not persist the flag,
|
|
133
|
+
// and the old digest is written exactly as it was.
|
|
134
|
+
const flagged = { ...records[0]!, runId: "run-3" } as ReleaseRecord;
|
|
135
|
+
const { record: written } = await appendReleaseRecord(flagged, { cwd: dir });
|
|
136
|
+
expect(written.flags).toBeUndefined();
|
|
137
|
+
expect(written.digest).toBe(old);
|
|
138
|
+
const raw = await readBlobFromPath("prod", "releases.jsonl", { cwd: dir });
|
|
139
|
+
expect(raw ?? "").not.toContain("legacy-digest");
|
|
89
140
|
});
|
|
90
141
|
});
|
|
91
142
|
|
|
@@ -98,17 +149,58 @@ describe("legacy content digests (#2514)", () => {
|
|
|
98
149
|
});
|
|
99
150
|
});
|
|
100
151
|
|
|
101
|
-
test("
|
|
152
|
+
test("an old persisted build manifest reads back under its old key, flagged, and is found by its old artifact digest", async () => {
|
|
102
153
|
await withTestDir(async (dir) => {
|
|
103
154
|
initRepo(dir);
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
155
|
+
const oldEntry = legacyContentDigest("{}");
|
|
156
|
+
const oldManifestDigest = legacyContentDigest(`template:web.json:${oldEntry}`);
|
|
157
|
+
const oldManifest = {
|
|
158
|
+
version: 1 as const,
|
|
159
|
+
component: "web",
|
|
160
|
+
createdAt: new Date(0).toISOString(),
|
|
161
|
+
contents: [{ kind: "template" as const, path: "web.json", digest: oldEntry }],
|
|
162
|
+
manifestDigest: oldManifestDigest,
|
|
163
|
+
};
|
|
164
|
+
await persistBuildManifest(oldManifest, { cwd: dir });
|
|
165
|
+
|
|
166
|
+
let fresh = createBuildArchiveManifest("web", { now: () => new Date(0) });
|
|
167
|
+
fresh = addArchiveEntry(fresh, { kind: "template", path: "web.json", digest: contentDigest("{}") });
|
|
168
|
+
await persistBuildManifest(fresh, { cwd: dir });
|
|
169
|
+
|
|
170
|
+
const all = await readAllBuildManifests({ cwd: dir });
|
|
109
171
|
await readAllBuildManifests({ cwd: dir });
|
|
110
172
|
expect(legacyWarnings()).toHaveLength(1);
|
|
111
173
|
expect(String(legacyWarnings()[0][0])).toContain("build manifests hold");
|
|
174
|
+
expect(all).toHaveLength(2);
|
|
175
|
+
|
|
176
|
+
const old = await readBuildManifest(oldManifestDigest, { cwd: dir });
|
|
177
|
+
expect(old?.manifestDigest).toBe(oldManifestDigest);
|
|
178
|
+
expect(old?.flags).toEqual([LEGACY_DIGEST_FLAG]);
|
|
179
|
+
expect((await findBuildManifestByArtifactDigest(oldEntry, { cwd: dir }))?.manifestDigest).toBe(oldManifestDigest);
|
|
180
|
+
expect((await readBuildManifest(fresh.manifestDigest, { cwd: dir }))?.flags).toBeUndefined();
|
|
181
|
+
|
|
182
|
+
// Persisting a flagged manifest again writes no flag.
|
|
183
|
+
await persistBuildManifest(old!, { cwd: dir });
|
|
184
|
+
const raw = await readBlobFromPath("_builds", `${oldManifestDigest.replace(/:/g, "_")}.json`, { cwd: dir });
|
|
185
|
+
expect(raw ?? "").not.toContain("legacy-digest");
|
|
112
186
|
});
|
|
113
187
|
});
|
|
188
|
+
|
|
189
|
+
test("a cross-environment comparison of an old and a new digest says it cannot tell", () => {
|
|
190
|
+
const rec = (env: string, digest: string): ReleaseRecord => ({ version: 1, ...release(digest, `${env}-1`), env });
|
|
191
|
+
const mixed = compareAcrossEnvironments(
|
|
192
|
+
"web",
|
|
193
|
+
{ name: "staging", records: [rec("staging", contentDigest("x"))] },
|
|
194
|
+
{ name: "prod", records: [rec("prod", legacyContentDigest("x"))] },
|
|
195
|
+
);
|
|
196
|
+
expect(mixed.same).toBe(false);
|
|
197
|
+
expect(mixed.mixedDigestForms).toBe(true);
|
|
198
|
+
|
|
199
|
+
const differentNew = compareAcrossEnvironments(
|
|
200
|
+
"web",
|
|
201
|
+
{ name: "staging", records: [rec("staging", contentDigest("x"))] },
|
|
202
|
+
{ name: "prod", records: [rec("prod", contentDigest("y"))] },
|
|
203
|
+
);
|
|
204
|
+
expect(differentNew.mixedDigestForms).toBeUndefined();
|
|
205
|
+
});
|
|
114
206
|
});
|
|
@@ -1,13 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Digests recorded before real SHA-256 (chant #2514).
|
|
3
3
|
*
|
|
4
|
-
* `contentDigest`
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* produces
|
|
8
|
-
* ledger or persisted build manifests keep the old form
|
|
9
|
-
*
|
|
10
|
-
*
|
|
4
|
+
* Up to chant 0.80.0, `contentDigest` labelled its output `sha256:` but
|
|
5
|
+
* computed a 32-bit string hash and repeated its 8 hex digits eight times.
|
|
6
|
+
* It now computes real SHA-256 (../content-digest.ts), so every value it
|
|
7
|
+
* produces changed once. Values already recorded in a project's release
|
|
8
|
+
* ledger or persisted build manifests keep the old form: they are never
|
|
9
|
+
* re-keyed or rewritten, because a digest is an identity other records point
|
|
10
|
+
* at.
|
|
11
|
+
*
|
|
12
|
+
* Readers accept both forms. A record read back with an old value carries
|
|
13
|
+
* `flags: ["legacy-digest"]`, computed on read and never written, and chant
|
|
14
|
+
* warns once per project, per process, that the ledger holds such values.
|
|
15
|
+
* Both forms stay accepted through {@link LEGACY_DIGEST_ACCEPTED_THROUGH}.
|
|
16
|
+
* A later release may stop accepting old values, and it will warn a release
|
|
17
|
+
* ahead, as #2525 requires of any level-0 change.
|
|
11
18
|
*
|
|
12
19
|
* An old value is recognisable by its shape alone. A real SHA-256 hex digest
|
|
13
20
|
* whose 64 characters are one 8-character block repeated eight times has
|
|
@@ -18,11 +25,52 @@ import { resolve } from "node:path";
|
|
|
18
25
|
|
|
19
26
|
const LEGACY_DIGEST = /^sha256:([0-9a-f]{8})\1{7}$/;
|
|
20
27
|
|
|
28
|
+
/** The flag a record read back with an old-form digest carries. */
|
|
29
|
+
export const LEGACY_DIGEST_FLAG = "legacy-digest";
|
|
30
|
+
export type LegacyDigestFlag = typeof LEGACY_DIGEST_FLAG;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* The last release series in which readers are guaranteed to accept old-form
|
|
34
|
+
* digests: every 0.8x release. The first release allowed to refuse them is
|
|
35
|
+
* 0.90.0, and only after a release that warns about it.
|
|
36
|
+
*/
|
|
37
|
+
export const LEGACY_DIGEST_ACCEPTED_THROUGH = "0.89.x";
|
|
38
|
+
|
|
21
39
|
/** Whether `value` is a `sha256:` string made by the old 32-bit `contentDigest`. */
|
|
22
40
|
export function isLegacyContentDigest(value: unknown): boolean {
|
|
23
41
|
return typeof value === "string" && LEGACY_DIGEST.test(value);
|
|
24
42
|
}
|
|
25
43
|
|
|
44
|
+
/** Whether any of `values` is an old-form digest. */
|
|
45
|
+
export function hasLegacyContentDigest(values: Iterable<unknown>): boolean {
|
|
46
|
+
for (const value of values) {
|
|
47
|
+
if (isLegacyContentDigest(value)) return true;
|
|
48
|
+
}
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* `record` with `flags: ["legacy-digest"]` added when any of `values` (the
|
|
54
|
+
* record's own digests) is in the old form, and otherwise unchanged. Other
|
|
55
|
+
* flags a record carries are kept.
|
|
56
|
+
*/
|
|
57
|
+
export function flagLegacyDigest<T extends { flags?: string[] }>(record: T, values: Iterable<unknown>): T {
|
|
58
|
+
if (!hasLegacyContentDigest(values)) return record;
|
|
59
|
+
const flags = record.flags ?? [];
|
|
60
|
+
if (flags.includes(LEGACY_DIGEST_FLAG)) return record;
|
|
61
|
+
return { ...record, flags: [...flags, LEGACY_DIGEST_FLAG] };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* `record` without its read-side `flags`, for writers. Flags describe how a
|
|
66
|
+
* record reads now, so they are never persisted.
|
|
67
|
+
*/
|
|
68
|
+
export function withoutReadFlags<T extends { flags?: unknown }>(record: T): Omit<T, "flags"> {
|
|
69
|
+
if (!("flags" in record)) return record;
|
|
70
|
+
const { flags: _flags, ...rest } = record;
|
|
71
|
+
return rest;
|
|
72
|
+
}
|
|
73
|
+
|
|
26
74
|
/** Project roots already warned about in this process. */
|
|
27
75
|
const warnedProjects = new Set<string>();
|
|
28
76
|
|
|
@@ -43,20 +91,13 @@ export function warnOnLegacyDigests(
|
|
|
43
91
|
): boolean {
|
|
44
92
|
const project = resolve(cwd ?? process.cwd());
|
|
45
93
|
if (warnedProjects.has(project)) return false;
|
|
46
|
-
|
|
47
|
-
for (const value of values) {
|
|
48
|
-
if (isLegacyContentDigest(value)) {
|
|
49
|
-
found = true;
|
|
50
|
-
break;
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
if (!found) return false;
|
|
94
|
+
if (!hasLegacyContentDigest(values)) return false;
|
|
54
95
|
warnedProjects.add(project);
|
|
55
96
|
console.warn(
|
|
56
97
|
`[chant] warning: this project's ${where} ${where === "release ledger" ? "holds" : "hold"} sha256: values made by chant's old 32-bit content hash, ` +
|
|
57
|
-
"not real SHA-256.
|
|
58
|
-
"
|
|
59
|
-
|
|
98
|
+
"not real SHA-256. chant now computes real SHA-256, so a digest recorded from now on will not match " +
|
|
99
|
+
"an old one for the same content. Old entries are not rewritten: they read back flagged legacy-digest, " +
|
|
100
|
+
`and chant accepts them through ${LEGACY_DIGEST_ACCEPTED_THROUGH}. See https://github.com/INTENTIUS/chant/issues/2514`,
|
|
60
101
|
);
|
|
61
102
|
return true;
|
|
62
103
|
}
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* #2538 — member ledgers under `_members/<member>/` on `chant/lifecycle`.
|
|
3
|
+
*
|
|
4
|
+
* One fixture workspace holds a root member `.`, two members `api` and `web`
|
|
5
|
+
* that use the same environment name, an example group, and a nested
|
|
6
|
+
* workspace `platform` with a member `core`. Each store is written from a
|
|
7
|
+
* member's directory and the exact branch path is asserted, then read back
|
|
8
|
+
* from each member to show neither sees the other's records.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { afterAll, beforeEach, describe, expect, test } from "vitest";
|
|
12
|
+
import { spawnSync } from "node:child_process";
|
|
13
|
+
import { mkdirSync, mkdtempSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
14
|
+
import { tmpdir } from "node:os";
|
|
15
|
+
import { dirname, join } from "node:path";
|
|
16
|
+
import { clearMemberLedgerCache, memberLedgerPrefix, MemberLedgerError, resolveMemberLedger } from "./member-ledger";
|
|
17
|
+
import { ledgerDir, listLedgerEnvironments, listSnapshots, readEnvironmentSnapshots, writeLedgerFiles, writeSnapshot } from "./git";
|
|
18
|
+
import { appendReleaseRecord, readReleaseLedger, type ReleaseRecordInput } from "./release-ledger";
|
|
19
|
+
import { appendRunRecord, readRunLedger } from "./run-ledger";
|
|
20
|
+
import type { OpRunRecordInput } from "../op/runtime";
|
|
21
|
+
import { appendConvergeRecord, readConvergeLedger, type ConvergeTickRecordInput } from "./converge-ledger";
|
|
22
|
+
import { appendGateResolution, appendPendingGate, gateLedgerPath, readGateLedger } from "./gate-ledger";
|
|
23
|
+
import { persistBuildManifest, listBuildManifestDigests } from "./build-ledger-store";
|
|
24
|
+
import { emptyBaseline, readObservationBaseline, writeObservationBaseline } from "./observation-baseline";
|
|
25
|
+
import { acquireLease, leaseRef, readLease } from "./lease";
|
|
26
|
+
import { createBuildArchiveManifest } from "../components/verbs/build-archive";
|
|
27
|
+
|
|
28
|
+
const scratch: string[] = [];
|
|
29
|
+
afterAll(() => {
|
|
30
|
+
for (const d of scratch) rmSync(d, { recursive: true, force: true });
|
|
31
|
+
});
|
|
32
|
+
beforeEach(() => clearMemberLedgerCache());
|
|
33
|
+
|
|
34
|
+
function git(args: string[], cwd: string): string {
|
|
35
|
+
const r = spawnSync("git", args, { cwd, encoding: "utf-8" });
|
|
36
|
+
if (r.status !== 0) throw new Error(`git ${args.join(" ")}: ${r.stderr}`);
|
|
37
|
+
return r.stdout;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function repo(files: Record<string, string>): string {
|
|
41
|
+
const root = realpathSync(mkdtempSync(join(tmpdir(), "chant-member-ledger-")));
|
|
42
|
+
scratch.push(root);
|
|
43
|
+
for (const [path, text] of Object.entries(files)) {
|
|
44
|
+
mkdirSync(dirname(join(root, path)), { recursive: true });
|
|
45
|
+
writeFileSync(join(root, path), text);
|
|
46
|
+
}
|
|
47
|
+
git(["init", "-q", "-b", "main"], root);
|
|
48
|
+
git(["config", "user.email", "test@chant.dev"], root);
|
|
49
|
+
git(["config", "user.name", "Test"], root);
|
|
50
|
+
git(["add", "-A"], root);
|
|
51
|
+
git(["commit", "-q", "-m", "init"], root);
|
|
52
|
+
return root;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const CONFIG = JSON.stringify({ lexicons: ["k8s"] });
|
|
56
|
+
|
|
57
|
+
function workspace(): string {
|
|
58
|
+
return repo({
|
|
59
|
+
"chant.workspace.json": JSON.stringify({
|
|
60
|
+
name: "acme",
|
|
61
|
+
schema: 1,
|
|
62
|
+
members: [
|
|
63
|
+
{ name: "root", dir: ".", kind: "chant" },
|
|
64
|
+
{ name: "api", dir: "services/api", kind: "chant" },
|
|
65
|
+
{ name: "web", dir: "services/web", kind: "chant" },
|
|
66
|
+
{ name: "platform", dir: "platform", kind: "workspace" },
|
|
67
|
+
{ name: "examples", kind: "examples", glob: "examples/*" },
|
|
68
|
+
],
|
|
69
|
+
}),
|
|
70
|
+
"chant.config.json": CONFIG,
|
|
71
|
+
"services/api/chant.config.json": CONFIG,
|
|
72
|
+
"services/api/src/main.ts": "",
|
|
73
|
+
"services/web/chant.config.json": CONFIG,
|
|
74
|
+
"examples/demo/chant.config.json": CONFIG,
|
|
75
|
+
"docs/README.md": "",
|
|
76
|
+
"platform/chant.workspace.json": JSON.stringify({
|
|
77
|
+
name: "platform",
|
|
78
|
+
schema: 1,
|
|
79
|
+
members: [{ name: "core", dir: "core", kind: "chant" }],
|
|
80
|
+
}),
|
|
81
|
+
"platform/core/chant.config.json": CONFIG,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Every file on chant/lifecycle, sorted. */
|
|
86
|
+
function branchFiles(root: string): string[] {
|
|
87
|
+
return git(["ls-tree", "-r", "--name-only", "chant/lifecycle"], root).split("\n").filter(Boolean).sort();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function release(overrides: Partial<ReleaseRecordInput> = {}): ReleaseRecordInput {
|
|
91
|
+
return {
|
|
92
|
+
component: "svc",
|
|
93
|
+
env: "prod",
|
|
94
|
+
digest: "sha256:abc",
|
|
95
|
+
gitSha: "deadbeef",
|
|
96
|
+
runId: "run-1",
|
|
97
|
+
timestamp: "2026-01-01T00:00:00.000Z",
|
|
98
|
+
actor: "ci",
|
|
99
|
+
...overrides,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function run(overrides: Partial<OpRunRecordInput> = {}): OpRunRecordInput {
|
|
104
|
+
return {
|
|
105
|
+
op: "deploy",
|
|
106
|
+
env: "prod",
|
|
107
|
+
started: "2026-01-01T00:00:00.000Z",
|
|
108
|
+
ended: "2026-01-01T00:00:01.000Z",
|
|
109
|
+
status: "ok",
|
|
110
|
+
labels: { Env: "prod" },
|
|
111
|
+
outcomes: {},
|
|
112
|
+
phases: [],
|
|
113
|
+
...overrides,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function tick(overrides: Partial<ConvergeTickRecordInput> = {}): ConvergeTickRecordInput {
|
|
118
|
+
return {
|
|
119
|
+
op: "converge",
|
|
120
|
+
env: "prod",
|
|
121
|
+
timestamp: "2026-01-01T00:00:00.000Z",
|
|
122
|
+
firedRuleIds: [],
|
|
123
|
+
outcomes: [],
|
|
124
|
+
summary: { drifted: 0, remediated: 0, reported: 0, skippedBudget: 0, skippedFlap: 0, unobserved: 0, adopted: 0 },
|
|
125
|
+
log: "converge(prod)",
|
|
126
|
+
...overrides,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
describe("resolveMemberLedger (#2538)", () => {
|
|
131
|
+
test("a project with no declaration keeps the flat layout", async () => {
|
|
132
|
+
const root = repo({ "chant.config.json": CONFIG });
|
|
133
|
+
expect(await resolveMemberLedger(root)).toEqual({ members: [], prefix: "" });
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test("the root member, unowned directories and example matches keep the flat layout", async () => {
|
|
137
|
+
const root = workspace();
|
|
138
|
+
for (const dir of [".", "docs", "examples/demo"]) {
|
|
139
|
+
expect((await resolveMemberLedger(join(root, dir))).prefix, dir).toBe("");
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
test("a member, and any directory inside it, writes under _members/<member>/", async () => {
|
|
144
|
+
const root = workspace();
|
|
145
|
+
expect(await resolveMemberLedger(join(root, "services/api"))).toEqual({ members: ["api"], prefix: "_members/api/" });
|
|
146
|
+
expect((await resolveMemberLedger(join(root, "services/api/src"))).prefix).toBe("_members/api/");
|
|
147
|
+
expect((await resolveMemberLedger(join(root, "services/web"))).prefix).toBe("_members/web/");
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test("a nested workspace's ledgers sit inside the outer member that holds it", async () => {
|
|
151
|
+
const root = workspace();
|
|
152
|
+
expect((await resolveMemberLedger(join(root, "platform"))).prefix).toBe("_members/platform/");
|
|
153
|
+
expect(await resolveMemberLedger(join(root, "platform/core"))).toEqual({
|
|
154
|
+
members: ["platform", "core"],
|
|
155
|
+
prefix: "_members/platform/_members/core/",
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test("an unreadable declaration refuses to place the ledger rather than guess", async () => {
|
|
160
|
+
const root = repo({ "chant.workspace.json": "{ not json", "api/chant.config.json": CONFIG });
|
|
161
|
+
await expect(resolveMemberLedger(join(root, "api"))).rejects.toBeInstanceOf(MemberLedgerError);
|
|
162
|
+
await expect(ledgerDir("prod", { cwd: join(root, "api") })).rejects.toThrow(/cannot place this project's lifecycle ledger/);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test("memberLedgerPrefix joins one _members/<name>/ per level", () => {
|
|
166
|
+
expect(memberLedgerPrefix([])).toBe("");
|
|
167
|
+
expect(memberLedgerPrefix(["a", "b"])).toBe("_members/a/_members/b/");
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
describe("every lifecycle store writes under the member path (#2538)", () => {
|
|
172
|
+
test("releases: two members with one environment name keep separate ledgers", async () => {
|
|
173
|
+
const root = workspace();
|
|
174
|
+
const api = join(root, "services/api");
|
|
175
|
+
const web = join(root, "services/web");
|
|
176
|
+
await appendReleaseRecord(release({ component: "api" }), { cwd: api });
|
|
177
|
+
await appendReleaseRecord(release({ component: "web" }), { cwd: web });
|
|
178
|
+
await appendReleaseRecord(release({ component: "root" }), { cwd: root });
|
|
179
|
+
|
|
180
|
+
expect(branchFiles(root)).toEqual(["_members/api/prod/releases.jsonl", "_members/web/prod/releases.jsonl", "prod/releases.jsonl"]);
|
|
181
|
+
expect((await readReleaseLedger("prod", { cwd: api })).records.map((r) => r.component)).toEqual(["api"]);
|
|
182
|
+
expect((await readReleaseLedger("prod", { cwd: web })).records.map((r) => r.component)).toEqual(["web"]);
|
|
183
|
+
expect((await readReleaseLedger("prod", { cwd: root })).records.map((r) => r.component)).toEqual(["root"]);
|
|
184
|
+
expect(await listLedgerEnvironments({ cwd: api })).toEqual(["prod"]);
|
|
185
|
+
// The root member's listing never reads `_members` as an environment.
|
|
186
|
+
expect(await listLedgerEnvironments({ cwd: root })).toEqual(["prod"]);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test("snapshots and observation baselines", async () => {
|
|
190
|
+
const root = workspace();
|
|
191
|
+
const api = join(root, "services/api");
|
|
192
|
+
const web = join(root, "services/web");
|
|
193
|
+
await writeSnapshot("prod", "k8s", '{"from":"api"}', { cwd: api });
|
|
194
|
+
await writeSnapshot("prod", "k8s", '{"from":"web"}', { cwd: web });
|
|
195
|
+
await writeObservationBaseline(emptyBaseline("prod"), { cwd: api });
|
|
196
|
+
|
|
197
|
+
const files = branchFiles(root);
|
|
198
|
+
expect(files).toContain("_members/api/prod/k8s.json");
|
|
199
|
+
expect(files).toContain("_members/web/prod/k8s.json");
|
|
200
|
+
expect(files.filter((f) => f.startsWith("_members/api/prod/") && f !== "_members/api/prod/k8s.json")).toHaveLength(1);
|
|
201
|
+
expect(files.every((f) => f.startsWith("_members/"))).toBe(true);
|
|
202
|
+
expect([...(await readEnvironmentSnapshots("prod", { cwd: web })).values()]).toEqual(['{"from":"web"}']);
|
|
203
|
+
expect(await readObservationBaseline("prod", { cwd: api })).toMatchObject({ environment: "prod" });
|
|
204
|
+
expect(await readObservationBaseline("prod", { cwd: web })).toBeNull();
|
|
205
|
+
// A member's history lists only the commits that touched its ledger.
|
|
206
|
+
expect(await listSnapshots({ cwd: web })).toHaveLength(1);
|
|
207
|
+
expect(await listSnapshots({ cwd: root })).toHaveLength(3);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
test("runs and converge records", async () => {
|
|
211
|
+
const root = workspace();
|
|
212
|
+
const api = join(root, "services/api");
|
|
213
|
+
const web = join(root, "services/web");
|
|
214
|
+
await appendRunRecord(run(), { cwd: api });
|
|
215
|
+
await appendConvergeRecord(tick(), { cwd: web });
|
|
216
|
+
|
|
217
|
+
expect(branchFiles(root)).toEqual(["_members/api/prod/runs__deploy.jsonl", "_members/web/prod/converge.jsonl"]);
|
|
218
|
+
expect((await readRunLedger("prod", "deploy", { cwd: api })).records).toHaveLength(1);
|
|
219
|
+
expect((await readRunLedger("prod", "deploy", { cwd: web })).records).toHaveLength(0);
|
|
220
|
+
expect((await readConvergeLedger("prod", { cwd: web })).records).toHaveLength(1);
|
|
221
|
+
expect((await readConvergeLedger("prod", { cwd: api })).records).toHaveLength(0);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
test("gates: _gates moves under the member, and an Op name can repeat across members", async () => {
|
|
225
|
+
const root = workspace();
|
|
226
|
+
const api = join(root, "services/api");
|
|
227
|
+
const web = join(root, "services/web");
|
|
228
|
+
await appendPendingGate({ op: "deploy", gate: "approve", timestamp: "2026-01-01T00:00:00.000Z", expiresAt: "2026-01-02T00:00:00.000Z" }, { cwd: api });
|
|
229
|
+
await appendGateResolution({ op: "deploy", gate: "approve", resolvedBy: "alex", timestamp: "2026-01-01T00:00:01.000Z" }, { cwd: web });
|
|
230
|
+
|
|
231
|
+
expect(branchFiles(root)).toEqual(["_members/api/_gates/deploy.jsonl", "_members/web/_gates/deploy.jsonl"]);
|
|
232
|
+
const apiGates = await readGateLedger("deploy", { cwd: api });
|
|
233
|
+
expect(apiGates.pending).toHaveLength(1);
|
|
234
|
+
expect(apiGates.resolutions).toHaveLength(0);
|
|
235
|
+
const webGates = await readGateLedger("deploy", { cwd: web });
|
|
236
|
+
expect(webGates.pending).toHaveLength(0);
|
|
237
|
+
expect(webGates.resolutions).toHaveLength(1);
|
|
238
|
+
expect(gateLedgerPath("deploy", "_members/api/")).toBe("_members/api/_gates/deploy.jsonl");
|
|
239
|
+
expect(gateLedgerPath("deploy")).toBe("_gates/deploy.jsonl");
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
test("build records: _builds moves under the member", async () => {
|
|
243
|
+
const root = workspace();
|
|
244
|
+
const api = join(root, "services/api");
|
|
245
|
+
const manifest = createBuildArchiveManifest("api", { now: () => new Date("2026-01-01T00:00:00.000Z") });
|
|
246
|
+
await persistBuildManifest(manifest, { cwd: api });
|
|
247
|
+
|
|
248
|
+
const files = branchFiles(root);
|
|
249
|
+
expect(files).toHaveLength(1);
|
|
250
|
+
expect(files[0]).toMatch(/^_members\/api\/_builds\/sha256_[0-9a-f]+\.json$/);
|
|
251
|
+
expect(await listBuildManifestDigests({ cwd: api })).toEqual([manifest.manifestDigest]);
|
|
252
|
+
expect(await listBuildManifestDigests({ cwd: join(root, "services/web") })).toEqual([]);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
test("leases: refs/chant/lease/_members/<member>/<op>, so two members' Ops of one name hold separate leases", async () => {
|
|
256
|
+
const root = workspace();
|
|
257
|
+
const api = join(root, "services/api");
|
|
258
|
+
const web = join(root, "services/web");
|
|
259
|
+
expect((await acquireLease("converge", "holder-api", { cwd: api })).acquired).toBe(true);
|
|
260
|
+
expect((await acquireLease("converge", "holder-web", { cwd: web })).acquired).toBe(true);
|
|
261
|
+
expect((await acquireLease("converge", "holder-root", { cwd: root })).acquired).toBe(true);
|
|
262
|
+
|
|
263
|
+
const refs = git(["for-each-ref", "--format=%(refname)", "refs/chant/lease/"], root).split("\n").filter(Boolean).sort();
|
|
264
|
+
expect(refs).toEqual([
|
|
265
|
+
"refs/chant/lease/_members/api/converge",
|
|
266
|
+
"refs/chant/lease/_members/web/converge",
|
|
267
|
+
"refs/chant/lease/converge",
|
|
268
|
+
]);
|
|
269
|
+
expect((await readLease("converge", { cwd: api })).record?.holder).toBe("holder-api");
|
|
270
|
+
expect((await readLease("converge", { cwd: web })).record?.holder).toBe("holder-web");
|
|
271
|
+
expect(leaseRef("converge", "_members/api/")).toBe("refs/chant/lease/_members/api/converge");
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
test("a nested workspace member writes under both levels", async () => {
|
|
275
|
+
const root = workspace();
|
|
276
|
+
await appendReleaseRecord(release({ component: "core" }), { cwd: join(root, "platform/core") });
|
|
277
|
+
expect(branchFiles(root)).toEqual(["_members/platform/_members/core/prod/releases.jsonl"]);
|
|
278
|
+
});
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
describe("one commit can write several members (#2538)", () => {
|
|
282
|
+
test("writeLedgerFiles lands two members' records in a single commit, each read back by its own member", async () => {
|
|
283
|
+
const root = workspace();
|
|
284
|
+
const api = join(root, "services/api");
|
|
285
|
+
const web = join(root, "services/web");
|
|
286
|
+
// An earlier write, so the multi-member commit extends a branch that has other entries.
|
|
287
|
+
await appendReleaseRecord(release({ component: "root" }), { cwd: root });
|
|
288
|
+
const before = git(["rev-parse", "chant/lifecycle"], root).trim();
|
|
289
|
+
|
|
290
|
+
const line = (component: string) => JSON.stringify({ version: 1, ...release({ component }) });
|
|
291
|
+
const commit = await writeLedgerFiles(
|
|
292
|
+
[
|
|
293
|
+
{ path: `${await ledgerDir("prod", { cwd: api })}/releases.jsonl`, content: line("api") },
|
|
294
|
+
{ path: `${await ledgerDir("prod", { cwd: web })}/releases.jsonl`, content: line("web") },
|
|
295
|
+
],
|
|
296
|
+
"Release records: api, web",
|
|
297
|
+
{ cwd: root },
|
|
298
|
+
);
|
|
299
|
+
|
|
300
|
+
expect(git(["rev-parse", "chant/lifecycle"], root).trim()).toBe(commit);
|
|
301
|
+
expect(git(["rev-parse", `${commit}^`], root).trim()).toBe(before);
|
|
302
|
+
expect(git(["diff-tree", "-r", "--name-only", "--no-commit-id", commit], root).split("\n").filter(Boolean).sort()).toEqual([
|
|
303
|
+
"_members/api/prod/releases.jsonl",
|
|
304
|
+
"_members/web/prod/releases.jsonl",
|
|
305
|
+
]);
|
|
306
|
+
expect((await readReleaseLedger("prod", { cwd: api })).records.map((r) => r.component)).toEqual(["api"]);
|
|
307
|
+
expect((await readReleaseLedger("prod", { cwd: web })).records.map((r) => r.component)).toEqual(["web"]);
|
|
308
|
+
expect((await readReleaseLedger("prod", { cwd: root })).records.map((r) => r.component)).toEqual(["root"]);
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
test("an expected prior sha that moved is a conflict, not a blind overwrite", async () => {
|
|
312
|
+
const root = workspace();
|
|
313
|
+
const api = join(root, "services/api");
|
|
314
|
+
await appendReleaseRecord(release({ component: "api" }), { cwd: api });
|
|
315
|
+
await expect(
|
|
316
|
+
writeLedgerFiles([{ path: "_members/api/prod/releases.jsonl", content: "{}", expectPriorSha: null }], "stale", { cwd: root }),
|
|
317
|
+
).rejects.toThrow(/target path changed concurrently/);
|
|
318
|
+
});
|
|
319
|
+
});
|