@intentius/chant 0.80.0 → 0.82.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 +35 -0
- package/dist/workspace/lineage-cli.d.ts.map +1 -0
- package/dist/workspace/lineage-init.d.ts +144 -0
- package/dist/workspace/lineage-init.d.ts.map +1 -0
- package/dist/workspace/lineage-lock.d.ts +271 -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 +152 -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 +58 -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 +232 -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 +158 -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 +134 -0
- package/src/workspace/lineage-init-dir.test.ts +243 -0
- package/src/workspace/lineage-init.test.ts +231 -0
- package/src/workspace/lineage-init.ts +560 -0
- package/src/workspace/lineage-lock.test.ts +224 -0
- package/src/workspace/lineage-lock.ts +365 -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 +723 -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
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from "vitest";
|
|
2
|
+
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import type { Lineage } from "./lineage-lock";
|
|
6
|
+
import { mergeFile } from "./lineage-merge";
|
|
7
|
+
import { planMigrations, runMigration, splitMigrations, type LoadedMigration } from "./lineage-migrations";
|
|
8
|
+
import { applyUpstream } from "./lineage-update";
|
|
9
|
+
import { compareVersions, parseVersion, satisfies } from "./lineage-version";
|
|
10
|
+
|
|
11
|
+
function lineage(ref: string | undefined, migrations: string[] = []): Lineage {
|
|
12
|
+
return {
|
|
13
|
+
kind: "template",
|
|
14
|
+
template: "github.com/acme/starter",
|
|
15
|
+
source: { type: "git", repo: "acme/starter", url: "https://github.com/acme/starter.git" },
|
|
16
|
+
...(ref ? { ref } : {}),
|
|
17
|
+
address: null,
|
|
18
|
+
parameters: {},
|
|
19
|
+
migrations,
|
|
20
|
+
files: {},
|
|
21
|
+
manualSteps: [],
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function migration(id: string, versions: string, to: string, template?: string): LoadedMigration {
|
|
26
|
+
return {
|
|
27
|
+
file: `.chant/migrations/${id}.json`,
|
|
28
|
+
migration: { id, from: { versions, ...(template ? { template } : {}) }, to, body: { type: "declarative", steps: [{ op: "delete", path: "x" }] }, post: [] },
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
describe("versions", () => {
|
|
33
|
+
test("a version is read from a tag, with or without a prefix", () => {
|
|
34
|
+
expect(parseVersion("v1.4.0")).toEqual({ major: 1, minor: 4, patch: 0, pre: "" });
|
|
35
|
+
expect(parseVersion("2")).toEqual({ major: 2, minor: 0, patch: 0, pre: "" });
|
|
36
|
+
expect(parseVersion("chant-v0.80.0")).toEqual({ major: 0, minor: 80, patch: 0, pre: "" });
|
|
37
|
+
expect(parseVersion("v2.0.0-rc.1")?.pre).toBe("rc.1");
|
|
38
|
+
expect(parseVersion("main")).toBeNull();
|
|
39
|
+
expect(parseVersion("4624a0b6ca83")).toBeNull();
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
test("ranges", () => {
|
|
43
|
+
const v = (s: string) => parseVersion(s)!;
|
|
44
|
+
expect(satisfies(v("1.5.0"), ">=1.0.0 <2.0.0")).toBe(true);
|
|
45
|
+
expect(satisfies(v("2.0.0"), ">=1.0.0 <2.0.0")).toBe(false);
|
|
46
|
+
expect(satisfies(v("1.9.3"), "^1.2")).toBe(true);
|
|
47
|
+
expect(satisfies(v("0.3.1"), "^0.3.0")).toBe(true);
|
|
48
|
+
expect(satisfies(v("0.4.0"), "^0.3.0")).toBe(false);
|
|
49
|
+
expect(satisfies(v("1.2.9"), "~1.2.0")).toBe(true);
|
|
50
|
+
expect(satisfies(v("1.3.0"), "~1.2.0")).toBe(false);
|
|
51
|
+
expect(satisfies(v("1.7.0"), "1.x")).toBe(true);
|
|
52
|
+
expect(satisfies(v("3.0.0"), "1.x || >=3")).toBe(true);
|
|
53
|
+
expect(satisfies(v("1.2.3"), "1.2.3")).toBe(true);
|
|
54
|
+
expect(satisfies(v("1.2.9"), "<=1.2")).toBe(true);
|
|
55
|
+
expect(satisfies(v("2.0.0-rc.1"), ">=1.0.0")).toBe(false);
|
|
56
|
+
expect(satisfies(v("7.0.0"), "*")).toBe(true);
|
|
57
|
+
expect(compareVersions(v("2.0.0-rc.1"), v("2.0.0"))).toBe(-1);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe("planMigrations", () => {
|
|
62
|
+
test("orders the chain by the version each migration brings the scope to", () => {
|
|
63
|
+
const plan = planMigrations(lineage("v1.0.0"), "v3.1.0", [
|
|
64
|
+
migration("b", ">=2.0.0 <3.0.0", "3.0.0"),
|
|
65
|
+
migration("a", "1.x", "2.0.0"),
|
|
66
|
+
migration("future", ">=3.0.0", "4.0.0"),
|
|
67
|
+
migration("past", "0.x", "1.0.0"),
|
|
68
|
+
]);
|
|
69
|
+
expect(plan.chain.map((m) => m.migration.id)).toEqual(["a", "b"]);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("skips migrations already applied and those for another template", () => {
|
|
73
|
+
const plan = planMigrations(lineage("v1.0.0", ["a"]), "v2.0.0", [
|
|
74
|
+
migration("a", "1.x", "2.0.0"),
|
|
75
|
+
migration("other", "1.x", "2.0.0", "github.com/someone/else"),
|
|
76
|
+
migration("mine", "1.x", "2.0.0", "github.com/acme/starter"),
|
|
77
|
+
]);
|
|
78
|
+
expect(plan.chain.map((m) => m.migration.id)).toEqual(["mine"]);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("refuses a gap, a downgrade, and a chain it cannot compute", () => {
|
|
82
|
+
expect(() => planMigrations(lineage("v1.0.0"), "v3.0.0", [migration("b", ">=2.0.0 <3.0.0", "3.0.0")])).toThrow(/gap in the migration chain/);
|
|
83
|
+
expect(() => planMigrations(lineage("v2.0.0"), "v1.0.0", [])).toThrow(/only goes forward/);
|
|
84
|
+
expect(() => planMigrations(lineage("main"), "v2.0.0", [migration("a", "1.x", "2.0.0")])).toThrow(/not a version/);
|
|
85
|
+
// With nothing to run, no version is needed.
|
|
86
|
+
expect(planMigrations(lineage("main"), "main", []).chain).toEqual([]);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
describe("migration files", () => {
|
|
91
|
+
const enc = (o: unknown) => Buffer.from(JSON.stringify(o));
|
|
92
|
+
test("are split out of the template's files and validated", () => {
|
|
93
|
+
const files = new Map<string, Buffer>([
|
|
94
|
+
["src/a.ts", Buffer.from("a")],
|
|
95
|
+
[".chant/migrations/one.json", enc({ id: "one", from: { versions: "1.x" }, to: "2.0.0", body: { type: "declarative", steps: [{ op: "delete", path: "a" }] } })],
|
|
96
|
+
[".chant/migrations/two.mjs", Buffer.from("export default () => {}")],
|
|
97
|
+
]);
|
|
98
|
+
const split = splitMigrations(files);
|
|
99
|
+
expect([...split.files.keys()]).toEqual(["src/a.ts"]);
|
|
100
|
+
expect(split.migrations.map((m) => m.migration.id)).toEqual(["one"]);
|
|
101
|
+
expect([...split.modules.keys()]).toEqual(["two.mjs"]);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("refuse an unknown step, a bad range, an escaping path and a missing code module", () => {
|
|
105
|
+
const one = (m: unknown) => splitMigrations(new Map([[".chant/migrations/m.json", enc(m)]]));
|
|
106
|
+
const base = { id: "m", from: { versions: "1.x" }, to: "2.0.0" };
|
|
107
|
+
expect(() => one({ ...base, body: { type: "declarative", steps: [{ op: "chmod", path: "a" }] } })).toThrow(/invalid migration/);
|
|
108
|
+
expect(() => one({ ...base, from: { versions: ">>1" }, body: { type: "declarative", steps: [{ op: "delete", path: "a" }] } })).toThrow(/not a version range/);
|
|
109
|
+
expect(() => one({ ...base, body: { type: "declarative", steps: [{ op: "delete", path: "../a" }] } })).toThrow(/inside the scope/);
|
|
110
|
+
expect(() => one({ ...base, body: { type: "code", module: "gone.mjs" } })).toThrow(/is not in the template/);
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
describe("declarative steps", () => {
|
|
115
|
+
let dir: string;
|
|
116
|
+
beforeEach(() => {
|
|
117
|
+
dir = mkdtempSync(join(tmpdir(), "chant-migration-"));
|
|
118
|
+
});
|
|
119
|
+
afterEach(() => rmSync(dir, { recursive: true, force: true }));
|
|
120
|
+
|
|
121
|
+
test("json-set and json-delete edit a JSON file at a pointer", async () => {
|
|
122
|
+
writeFileSync(join(dir, "c.json"), JSON.stringify({ a: { b: 1, gone: true } }));
|
|
123
|
+
const l = lineage("v1");
|
|
124
|
+
await runMigration(
|
|
125
|
+
{
|
|
126
|
+
file: "m.json",
|
|
127
|
+
migration: {
|
|
128
|
+
id: "json",
|
|
129
|
+
from: { versions: "*" },
|
|
130
|
+
to: "2.0.0",
|
|
131
|
+
body: {
|
|
132
|
+
type: "declarative",
|
|
133
|
+
steps: [
|
|
134
|
+
{ op: "json-set", path: "c.json", pointer: "/a/c~1d", value: [1] },
|
|
135
|
+
{ op: "json-delete", path: "c.json", pointer: "/a/gone" },
|
|
136
|
+
],
|
|
137
|
+
},
|
|
138
|
+
post: [{ check: "contains", path: "c.json", text: '"c/d"' }],
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
{ dir, lineage: l, modules: new Map(), allowCode: false, scratch: dir },
|
|
142
|
+
);
|
|
143
|
+
expect(JSON.parse(readFileSync(join(dir, "c.json"), "utf-8"))).toEqual({ a: { b: 1, "c/d": [1] } });
|
|
144
|
+
expect(l.migrations).toEqual(["json"]);
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
describe("applyUpstream class rules", () => {
|
|
149
|
+
test("seed and generated files are never written or merged", () => {
|
|
150
|
+
const dir = mkdtempSync(join(tmpdir(), "chant-classes-"));
|
|
151
|
+
try {
|
|
152
|
+
mkdirSync(join(dir, "skills/x"), { recursive: true });
|
|
153
|
+
writeFileSync(join(dir, ".mcp.json"), "{}");
|
|
154
|
+
writeFileSync(join(dir, "skills/x/SKILL.md"), "old");
|
|
155
|
+
const l = lineage("v1");
|
|
156
|
+
l.files = {
|
|
157
|
+
".mcp.json": { class: "seed", sha256: `sha256:${"1".repeat(64)}` },
|
|
158
|
+
"skills/x/SKILL.md": { class: "generated", sha256: `sha256:${"2".repeat(64)}`, command: "chant update" },
|
|
159
|
+
};
|
|
160
|
+
const result = applyUpstream(dir, l, new Map([[".mcp.json", Buffer.from("{ new }")], ["skills/x/SKILL.md", Buffer.from("new")]]), { base: new Map(), merge: mergeFile });
|
|
161
|
+
expect(result.written).toEqual([]);
|
|
162
|
+
expect(result.skipped).toEqual([
|
|
163
|
+
{ path: ".mcp.json", class: "seed" },
|
|
164
|
+
{ path: "skills/x/SKILL.md", class: "generated", command: "chant update" },
|
|
165
|
+
]);
|
|
166
|
+
expect(readFileSync(join(dir, ".mcp.json"), "utf-8")).toBe("{}");
|
|
167
|
+
} finally {
|
|
168
|
+
rmSync(dir, { recursive: true, force: true });
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test("mergeFile refuses a conflicting hunk and a binary file", () => {
|
|
173
|
+
const b = Buffer.from("a\nb\nc\n");
|
|
174
|
+
expect(mergeFile(b, Buffer.from("A\nb\nc\n"), Buffer.from("a\nb\nC\n"))?.toString()).toBe("A\nb\nC\n");
|
|
175
|
+
expect(mergeFile(b, Buffer.from("a\nX\nc\n"), Buffer.from("a\nY\nc\n"))).toBeNull();
|
|
176
|
+
expect(mergeFile(Buffer.from([0, 1]), Buffer.from([0, 2]), Buffer.from([0, 3]))).toBeNull();
|
|
177
|
+
});
|
|
178
|
+
});
|
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Template migrations (#2550, D9).
|
|
3
|
+
*
|
|
4
|
+
* A template ships its migrations beside its files, under
|
|
5
|
+
* `.chant/migrations/`, one JSON file each. The metadata is data: an id, the
|
|
6
|
+
* template versions it applies `from` (a template id and a version range), the
|
|
7
|
+
* version it brings the scope `to`, a body, and a post-condition. The body is
|
|
8
|
+
* either a closed list of declarative steps or a module, marked
|
|
9
|
+
* `"type": "code"`, which runs only when the caller allows code.
|
|
10
|
+
*
|
|
11
|
+
* `chant workspace upgrade` plans a chain from the scope's version to the
|
|
12
|
+
* target: every migration whose `to` lies in between, ordered by `to`. Each
|
|
13
|
+
* must accept the version the scope has reached when its turn comes. When one
|
|
14
|
+
* does not, a migration is missing from the chain, and the upgrade is refused
|
|
15
|
+
* rather than applied in part.
|
|
16
|
+
*
|
|
17
|
+
* Migrations run inside the upgrade's staging worktree, before the per-file
|
|
18
|
+
* merge. A `move` or `delete` updates the lineage too, so the merge that
|
|
19
|
+
* follows compares the right merge base with the new version.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
23
|
+
import { dirname, join, posix } from "node:path";
|
|
24
|
+
import { pathToFileURL } from "node:url";
|
|
25
|
+
import { z } from "zod";
|
|
26
|
+
import { LockError, type Lineage } from "./lineage-lock";
|
|
27
|
+
import { compareVersions, formatVersion, parseVersion, satisfies, validateRange, VersionRangeError, type Version } from "./lineage-version";
|
|
28
|
+
|
|
29
|
+
/** Where a template keeps its migrations, relative to the template's directory. */
|
|
30
|
+
export const MIGRATIONS_DIR = ".chant/migrations";
|
|
31
|
+
|
|
32
|
+
const RelPath = z
|
|
33
|
+
.string()
|
|
34
|
+
.min(1)
|
|
35
|
+
.refine((p) => {
|
|
36
|
+
const n = posix.normalize(p);
|
|
37
|
+
return !n.startsWith("/") && n !== ".." && !n.startsWith("../") && !p.includes("\\");
|
|
38
|
+
}, "must be a relative path inside the scope");
|
|
39
|
+
|
|
40
|
+
/** The declarative steps. Closed: a step chant does not know is refused. */
|
|
41
|
+
const StepSchema = z.discriminatedUnion("op", [
|
|
42
|
+
/** Move a file. The lineage entry moves with it, merge base included. */
|
|
43
|
+
z.strictObject({ op: z.literal("move"), from: RelPath, to: RelPath }),
|
|
44
|
+
/** Delete a file and drop it from the lineage. */
|
|
45
|
+
z.strictObject({ op: z.literal("delete"), path: RelPath }),
|
|
46
|
+
/** Replace every occurrence of a literal string in a file. */
|
|
47
|
+
z.strictObject({ op: z.literal("replace"), path: RelPath, find: z.string().min(1), with: z.string() }),
|
|
48
|
+
/** Set a value in a JSON file at a JSON Pointer, creating objects on the way. */
|
|
49
|
+
z.strictObject({ op: z.literal("json-set"), path: RelPath, pointer: z.string().startsWith("/"), value: z.unknown() }),
|
|
50
|
+
/** Remove a key from a JSON file at a JSON Pointer. */
|
|
51
|
+
z.strictObject({ op: z.literal("json-delete"), path: RelPath, pointer: z.string().startsWith("/") }),
|
|
52
|
+
]);
|
|
53
|
+
export type MigrationStep = z.infer<typeof StepSchema>;
|
|
54
|
+
|
|
55
|
+
/** What must hold after a migration ran. Every check must pass. */
|
|
56
|
+
const PostSchema = z.discriminatedUnion("check", [
|
|
57
|
+
z.strictObject({ check: z.literal("exists"), path: RelPath }),
|
|
58
|
+
z.strictObject({ check: z.literal("absent"), path: RelPath }),
|
|
59
|
+
z.strictObject({ check: z.literal("contains"), path: RelPath, text: z.string().min(1) }),
|
|
60
|
+
z.strictObject({ check: z.literal("not-contains"), path: RelPath, text: z.string().min(1) }),
|
|
61
|
+
]);
|
|
62
|
+
export type MigrationCheck = z.infer<typeof PostSchema>;
|
|
63
|
+
|
|
64
|
+
const Range = z.string().min(1).superRefine((r, ctx) => {
|
|
65
|
+
try {
|
|
66
|
+
validateRange(r);
|
|
67
|
+
} catch (err) {
|
|
68
|
+
ctx.addIssue({ code: "custom", message: err instanceof VersionRangeError ? err.message : String(err) });
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const MigrationSchema = z.strictObject({
|
|
73
|
+
$schema: z.string().optional(),
|
|
74
|
+
/** Unique within the template. Recorded in the lock once applied. */
|
|
75
|
+
id: z.string().regex(/^[A-Za-z0-9][A-Za-z0-9._-]*$/, "letters, digits, '.', '_' and '-'"),
|
|
76
|
+
description: z.string().optional(),
|
|
77
|
+
from: z.strictObject({
|
|
78
|
+
/** The template id the migration applies to. Omitted, it applies to the template it ships in. */
|
|
79
|
+
template: z.string().min(1).optional(),
|
|
80
|
+
/** The versions it can be applied to. */
|
|
81
|
+
versions: Range,
|
|
82
|
+
}),
|
|
83
|
+
/** The version the scope is at once the migration ran. */
|
|
84
|
+
to: z.string().refine((v) => parseVersion(v) !== null, "must be a version, such as 2.0.0"),
|
|
85
|
+
body: z.discriminatedUnion("type", [
|
|
86
|
+
z.strictObject({ type: z.literal("declarative"), steps: z.array(StepSchema).min(1) }),
|
|
87
|
+
/**
|
|
88
|
+
* Code: a module in the migrations directory whose default export is
|
|
89
|
+
* `async ({ dir }) => void`, run with the scope directory as `dir`.
|
|
90
|
+
*/
|
|
91
|
+
z.strictObject({ type: z.literal("code"), module: RelPath }),
|
|
92
|
+
]),
|
|
93
|
+
post: z.array(PostSchema).default([]),
|
|
94
|
+
});
|
|
95
|
+
export type Migration = z.infer<typeof MigrationSchema>;
|
|
96
|
+
|
|
97
|
+
export class MigrationError extends LockError {
|
|
98
|
+
override name = "MigrationError";
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export interface LoadedMigration {
|
|
102
|
+
migration: Migration;
|
|
103
|
+
/** The file it was read from, relative to the template directory. */
|
|
104
|
+
file: string;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Read the migrations out of a template's files (relative path to bytes).
|
|
109
|
+
* Returns them and the files that remain once `.chant/migrations/` is taken
|
|
110
|
+
* out: migrations are the template's, never copied into a project.
|
|
111
|
+
*/
|
|
112
|
+
export function splitMigrations(files: Map<string, Buffer>): { migrations: LoadedMigration[]; modules: Map<string, Buffer>; files: Map<string, Buffer> } {
|
|
113
|
+
const rest = new Map<string, Buffer>();
|
|
114
|
+
const migrations: LoadedMigration[] = [];
|
|
115
|
+
const modules = new Map<string, Buffer>();
|
|
116
|
+
const prefix = `${MIGRATIONS_DIR}/`;
|
|
117
|
+
for (const [path, data] of files) {
|
|
118
|
+
if (!path.startsWith(prefix)) {
|
|
119
|
+
rest.set(path, data);
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
const rel = path.slice(prefix.length);
|
|
123
|
+
if (!rel.endsWith(".json")) {
|
|
124
|
+
modules.set(rel, data);
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
let raw: unknown;
|
|
128
|
+
try {
|
|
129
|
+
raw = JSON.parse(data.toString("utf-8"));
|
|
130
|
+
} catch (err) {
|
|
131
|
+
throw new MigrationError(`${path} is not valid JSON: ${err instanceof Error ? err.message : String(err)}`);
|
|
132
|
+
}
|
|
133
|
+
const parsed = MigrationSchema.safeParse(raw);
|
|
134
|
+
if (!parsed.success) {
|
|
135
|
+
throw new MigrationError(
|
|
136
|
+
`invalid migration ${path}: ${parsed.error.issues.map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`).join("; ")}`,
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
migrations.push({ migration: parsed.data, file: path });
|
|
140
|
+
}
|
|
141
|
+
const ids = new Set<string>();
|
|
142
|
+
for (const { migration, file } of migrations) {
|
|
143
|
+
if (ids.has(migration.id)) throw new MigrationError(`migration id "${migration.id}" is used twice (${file})`);
|
|
144
|
+
ids.add(migration.id);
|
|
145
|
+
if (migration.body.type === "code" && !modules.has(migration.body.module)) {
|
|
146
|
+
throw new MigrationError(`${file}: its code module ${MIGRATIONS_DIR}/${migration.body.module} is not in the template`);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return { migrations, modules, files: rest };
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// ── Planning ─────────────────────────────────────────────────────────────────
|
|
153
|
+
|
|
154
|
+
export interface MigrationPlan {
|
|
155
|
+
/** In the order they run. */
|
|
156
|
+
chain: LoadedMigration[];
|
|
157
|
+
from: Version | null;
|
|
158
|
+
to: Version | null;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* The migrations that take `lineage` from its version to `targetRef`, in order.
|
|
163
|
+
*
|
|
164
|
+
* Refused, with a {@link MigrationError}:
|
|
165
|
+
* - a target below the scope's version;
|
|
166
|
+
* - a pending migration when either end of the upgrade is not a version, so
|
|
167
|
+
* no chain can be computed;
|
|
168
|
+
* - a gap: a migration in range whose `from` does not accept the version the
|
|
169
|
+
* scope has reached by then.
|
|
170
|
+
*/
|
|
171
|
+
export function planMigrations(lineage: Lineage, targetRef: string | undefined, available: LoadedMigration[]): MigrationPlan {
|
|
172
|
+
const from = parseVersion(lineage.ref);
|
|
173
|
+
const to = parseVersion(targetRef);
|
|
174
|
+
if (from && to && compareVersions(to, from) < 0) {
|
|
175
|
+
throw new MigrationError(`the target ${targetRef} (${formatVersion(to)}) is older than the scope's ${lineage.ref} (${formatVersion(from)}); an upgrade only goes forward`);
|
|
176
|
+
}
|
|
177
|
+
const applied = new Set(lineage.migrations);
|
|
178
|
+
const mine = available.filter(
|
|
179
|
+
({ migration: m }) => (m.from.template === undefined || m.from.template === lineage.template) && !applied.has(m.id),
|
|
180
|
+
);
|
|
181
|
+
if (mine.length === 0) return { chain: [], from, to };
|
|
182
|
+
|
|
183
|
+
if (!from || !to) {
|
|
184
|
+
const which = !from ? `the scope's ref ${lineage.ref ?? "(none)"}` : `the target ${targetRef ?? "(none)"}`;
|
|
185
|
+
throw new MigrationError(
|
|
186
|
+
`the template has ${mine.length} migration(s) not yet applied (${mine.map((m) => m.migration.id).join(", ")}), but ${which} is not a version, so no chain can be planned. Pin tagged versions, such as v1.2.0.`,
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const inRange = mine
|
|
191
|
+
.map((m) => ({ ...m, target: parseVersion(m.migration.to)! }))
|
|
192
|
+
.filter((m) => compareVersions(m.target, from) > 0 && compareVersions(m.target, to) <= 0)
|
|
193
|
+
.sort((a, b) => compareVersions(a.target, b.target) || a.migration.id.localeCompare(b.migration.id));
|
|
194
|
+
|
|
195
|
+
const chain: LoadedMigration[] = [];
|
|
196
|
+
let at = from;
|
|
197
|
+
let i = 0;
|
|
198
|
+
while (i < inRange.length) {
|
|
199
|
+
const step = inRange[i].target;
|
|
200
|
+
const group = inRange.filter((m) => compareVersions(m.target, step) === 0);
|
|
201
|
+
for (const m of group) {
|
|
202
|
+
if (!satisfies(at, m.migration.from.versions)) {
|
|
203
|
+
throw new MigrationError(
|
|
204
|
+
`gap in the migration chain: ${m.migration.id} (${m.file}) upgrades ${m.migration.from.versions} to ${m.migration.to}, but the scope is at ${formatVersion(at)} when it would run. A migration that brings ${formatVersion(at)} into ${m.migration.from.versions} is missing, so the upgrade is refused.`,
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
chain.push({ migration: m.migration, file: m.file });
|
|
208
|
+
}
|
|
209
|
+
at = step;
|
|
210
|
+
i += group.length;
|
|
211
|
+
}
|
|
212
|
+
return { chain, from, to };
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// ── Running ──────────────────────────────────────────────────────────────────
|
|
216
|
+
|
|
217
|
+
export interface RunMigrationOptions {
|
|
218
|
+
/** The scope directory in the staging worktree. */
|
|
219
|
+
dir: string;
|
|
220
|
+
lineage: Lineage;
|
|
221
|
+
/** Code modules from the template, relative to its migrations directory. */
|
|
222
|
+
modules: Map<string, Buffer>;
|
|
223
|
+
/** Run `"type": "code"` bodies. Without it, a chain holding one is refused. */
|
|
224
|
+
allowCode: boolean;
|
|
225
|
+
/** Where a code module is written before it is imported. */
|
|
226
|
+
scratch: string;
|
|
227
|
+
/** Told of each file a declarative step moves, so a caller keyed by path (the merge base) can follow. */
|
|
228
|
+
onMove?: (from: string, to: string) => void;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** Refuse a chain holding code when code is not allowed, before anything runs. */
|
|
232
|
+
export function assertCodeAllowed(chain: LoadedMigration[], allowCode: boolean): void {
|
|
233
|
+
const code = chain.filter((m) => m.migration.body.type === "code");
|
|
234
|
+
if (code.length > 0 && !allowCode) {
|
|
235
|
+
throw new MigrationError(
|
|
236
|
+
`the chain includes ${code.length} code migration(s) (${code.map((m) => m.migration.id).join(", ")}), which run the template's own code. Read them, then pass --allow-code to run them.`,
|
|
237
|
+
);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/** Run one migration in the worktree, then its post-condition. Updates `lineage`. */
|
|
242
|
+
export async function runMigration(m: LoadedMigration, opts: RunMigrationOptions): Promise<void> {
|
|
243
|
+
const { migration } = m;
|
|
244
|
+
const body = migration.body;
|
|
245
|
+
if (body.type === "code") {
|
|
246
|
+
if (!opts.allowCode) throw new MigrationError(`${migration.id} is a code migration; pass --allow-code to run it`);
|
|
247
|
+
const file = join(opts.scratch, "migrations", body.module);
|
|
248
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
249
|
+
for (const [rel, data] of opts.modules) {
|
|
250
|
+
const abs = join(opts.scratch, "migrations", rel);
|
|
251
|
+
mkdirSync(dirname(abs), { recursive: true });
|
|
252
|
+
writeFileSync(abs, data);
|
|
253
|
+
}
|
|
254
|
+
const mod = (await import(pathToFileURL(file).href)) as { default?: unknown };
|
|
255
|
+
if (typeof mod.default !== "function") throw new MigrationError(`${migration.id}: ${body.module} has no default export function`);
|
|
256
|
+
await (mod.default as (ctx: { dir: string }) => unknown)({ dir: opts.dir });
|
|
257
|
+
} else {
|
|
258
|
+
for (const step of body.steps) {
|
|
259
|
+
applyStep(step, opts.dir, opts.lineage, migration.id);
|
|
260
|
+
if (step.op === "move") opts.onMove?.(step.from, step.to);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
const failed = migration.post.filter((c) => !holds(c, opts.dir));
|
|
264
|
+
if (failed.length > 0) {
|
|
265
|
+
throw new MigrationError(
|
|
266
|
+
`migration ${migration.id} ran, but its post-condition does not hold: ${failed.map(describeCheck).join("; ")}. The upgrade is refused.`,
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
opts.lineage.migrations.push(migration.id);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function applyStep(step: MigrationStep, dir: string, lineage: Lineage, id: string): void {
|
|
273
|
+
const abs = (p: string) => join(dir, p);
|
|
274
|
+
const need = (p: string) => {
|
|
275
|
+
if (!existsSync(abs(p))) throw new MigrationError(`migration ${id}: ${step.op} needs ${p}, which is not in the scope`);
|
|
276
|
+
};
|
|
277
|
+
switch (step.op) {
|
|
278
|
+
case "move": {
|
|
279
|
+
need(step.from);
|
|
280
|
+
if (existsSync(abs(step.to))) throw new MigrationError(`migration ${id}: cannot move ${step.from} to ${step.to}, which exists`);
|
|
281
|
+
mkdirSync(dirname(abs(step.to)), { recursive: true });
|
|
282
|
+
renameSync(abs(step.from), abs(step.to));
|
|
283
|
+
const entry = lineage.files[step.from];
|
|
284
|
+
if (entry) {
|
|
285
|
+
delete lineage.files[step.from];
|
|
286
|
+
lineage.files[step.to] = entry;
|
|
287
|
+
}
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
case "delete":
|
|
291
|
+
if (existsSync(abs(step.path))) rmSync(abs(step.path));
|
|
292
|
+
delete lineage.files[step.path];
|
|
293
|
+
return;
|
|
294
|
+
case "replace": {
|
|
295
|
+
need(step.path);
|
|
296
|
+
const text = readFileSync(abs(step.path), "utf-8");
|
|
297
|
+
writeFileSync(abs(step.path), text.split(step.find).join(step.with));
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
case "json-set":
|
|
301
|
+
case "json-delete": {
|
|
302
|
+
need(step.path);
|
|
303
|
+
let doc: unknown;
|
|
304
|
+
try {
|
|
305
|
+
doc = JSON.parse(readFileSync(abs(step.path), "utf-8"));
|
|
306
|
+
} catch {
|
|
307
|
+
throw new MigrationError(`migration ${id}: ${step.path} is not valid JSON`);
|
|
308
|
+
}
|
|
309
|
+
const keys = step.pointer.slice(1).split("/").map((k) => k.replace(/~1/g, "/").replace(/~0/g, "~"));
|
|
310
|
+
const last = keys.pop()!;
|
|
311
|
+
let node = doc as Record<string, unknown>;
|
|
312
|
+
for (const k of keys) {
|
|
313
|
+
if (typeof node[k] !== "object" || node[k] === null) {
|
|
314
|
+
if (step.op === "json-delete") return;
|
|
315
|
+
node[k] = {};
|
|
316
|
+
}
|
|
317
|
+
node = node[k] as Record<string, unknown>;
|
|
318
|
+
}
|
|
319
|
+
if (step.op === "json-set") node[last] = step.value;
|
|
320
|
+
else delete node[last];
|
|
321
|
+
writeFileSync(abs(step.path), JSON.stringify(doc, null, 2) + "\n");
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function holds(c: MigrationCheck, dir: string): boolean {
|
|
328
|
+
const abs = join(dir, c.path);
|
|
329
|
+
switch (c.check) {
|
|
330
|
+
case "exists":
|
|
331
|
+
return existsSync(abs);
|
|
332
|
+
case "absent":
|
|
333
|
+
return !existsSync(abs);
|
|
334
|
+
case "contains":
|
|
335
|
+
return existsSync(abs) && readFileSync(abs, "utf-8").includes(c.text);
|
|
336
|
+
case "not-contains":
|
|
337
|
+
return !existsSync(abs) || !readFileSync(abs, "utf-8").includes(c.text);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function describeCheck(c: MigrationCheck): string {
|
|
342
|
+
return "text" in c ? `${c.check} ${JSON.stringify(c.text)} in ${c.path}` : `${c.path} ${c.check}`;
|
|
343
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bring one lineage scope to a new version of its source, file by file,
|
|
3
|
+
* without deleting the project's edits (#2540, ws-038).
|
|
4
|
+
*
|
|
5
|
+
* For each path, three hashes decide what happens: the merge base recorded in
|
|
6
|
+
* the lock, the source's new version, and the file in the tree.
|
|
7
|
+
*
|
|
8
|
+
* | tree | source | result |
|
|
9
|
+
* |-----------------|--------------------------|-----------------------------------------|
|
|
10
|
+
* | equals base | anything | replaced (or removed) by the source |
|
|
11
|
+
* | equals source | anything | kept; base moves to the source |
|
|
12
|
+
* | edited | unchanged from base | kept; base stays |
|
|
13
|
+
* | edited | changed, merges cleanly | merged; base moves to the source (*) |
|
|
14
|
+
* | edited | changed or removed | kept; a manual step |
|
|
15
|
+
* | deleted | unchanged from base | stays deleted |
|
|
16
|
+
* | deleted | changed | stays deleted; a manual step |
|
|
17
|
+
* | untracked file | adds the same path | kept; a manual step |
|
|
18
|
+
*
|
|
19
|
+
* The table is for `owned` files. A `seed` file is never touched again, and a
|
|
20
|
+
* `generated` file is left for its command to rebuild (D9, D14). Files in the
|
|
21
|
+
* scope directory that the lineage does not list are never touched. This is
|
|
22
|
+
* the per-file rule of ws-005.
|
|
23
|
+
*
|
|
24
|
+
* (*) Only when the caller passes {@link MergeOptions}: the merge base's
|
|
25
|
+
* content, rebuilt by `chant workspace upgrade` (#2550), and a merge function.
|
|
26
|
+
* A file merges only when every hunk is clean; otherwise it is kept as it was
|
|
27
|
+
* and becomes one manual step. `chant vendor pull` passes no base, since a
|
|
28
|
+
* vendor source keeps no history to rebuild it from, so every conflicting
|
|
29
|
+
* path there is a manual step.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
33
|
+
import { dirname, join } from "node:path";
|
|
34
|
+
import { contentDigest, declaredFilesAt, defaultFileClass, fileHash, type Lineage, type ManualStep } from "./lineage-lock";
|
|
35
|
+
|
|
36
|
+
export interface UpdateResult {
|
|
37
|
+
written: string[];
|
|
38
|
+
/** Edited files that took the source's changes through a clean three-way merge. */
|
|
39
|
+
merged: string[];
|
|
40
|
+
/** `seed` files, which an update never touches, and `generated` files, which their command rebuilds. */
|
|
41
|
+
skipped: Array<{ path: string; class: "seed" | "generated"; command?: string }>;
|
|
42
|
+
removed: string[];
|
|
43
|
+
kept: string[];
|
|
44
|
+
manualSteps: ManualStep[];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** The three-way merge, for `chant workspace upgrade`. */
|
|
48
|
+
export interface MergeOptions {
|
|
49
|
+
/** The merge base's content, by path. A path missing here cannot be merged. */
|
|
50
|
+
base: Map<string, Buffer>;
|
|
51
|
+
/** Merged bytes, or null when a hunk conflicts. */
|
|
52
|
+
merge: (base: Buffer, ours: Buffer, theirs: Buffer) => Buffer | null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Apply `upstream` (relative path to bytes) to the scope directory `dir`,
|
|
57
|
+
* updating `lineage` in place: its files, digest and manual steps. Open manual
|
|
58
|
+
* steps for paths this update touches again are replaced, others are kept.
|
|
59
|
+
*/
|
|
60
|
+
export function applyUpstream(dir: string, lineage: Lineage, upstream: Map<string, Buffer>, merging?: MergeOptions): UpdateResult {
|
|
61
|
+
const result: UpdateResult = { written: [], merged: [], skipped: [], removed: [], kept: [], manualSteps: [] };
|
|
62
|
+
const steps = new Map(lineage.manualSteps.map((s) => [s.path, s]));
|
|
63
|
+
const paths = new Set([...Object.keys(lineage.files), ...upstream.keys()]);
|
|
64
|
+
// The declaration's generated list is read again on every update, so a
|
|
65
|
+
// file the member lists after the lock was written is rebuilt, not merged,
|
|
66
|
+
// and a file it marks hand-written is merged like any other (#2541).
|
|
67
|
+
const declared = declaredFilesAt(dir);
|
|
68
|
+
|
|
69
|
+
for (const path of [...paths].sort()) {
|
|
70
|
+
const abs = join(dir, path);
|
|
71
|
+
const recorded = lineage.files[path];
|
|
72
|
+
if (recorded && declared.has(path)) lineage.files[path] = { ...defaultFileClass(path, declared), sha256: recorded.sha256 };
|
|
73
|
+
const entry = lineage.files[path];
|
|
74
|
+
if (entry && entry.class !== "owned") {
|
|
75
|
+
// D9: a seed is written once, and a generated file is rebuilt by its
|
|
76
|
+
// command rather than merged. Neither is written here.
|
|
77
|
+
result.skipped.push({ path, class: entry.class, ...(entry.command ? { command: entry.command } : {}) });
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
const base = lineage.files[path]?.sha256;
|
|
81
|
+
const incoming = upstream.get(path);
|
|
82
|
+
const theirs = incoming ? fileHash(incoming) : undefined;
|
|
83
|
+
const local = existsSync(abs) ? fileHash(readFileSync(abs)) : undefined;
|
|
84
|
+
const step = (reason: ManualStep["reason"]): void => {
|
|
85
|
+
const s: ManualStep = { path, reason, upstream: theirs ?? null };
|
|
86
|
+
steps.set(path, s);
|
|
87
|
+
result.manualSteps.push(s);
|
|
88
|
+
};
|
|
89
|
+
const record = (sha: string): void => {
|
|
90
|
+
lineage.files[path] = { ...(lineage.files[path] ?? defaultFileClass(path, declared)), sha256: sha };
|
|
91
|
+
steps.delete(path);
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
if (theirs === undefined) {
|
|
95
|
+
// The source no longer has the file.
|
|
96
|
+
if (local === undefined || local === base) {
|
|
97
|
+
if (local !== undefined) {
|
|
98
|
+
rmSync(abs);
|
|
99
|
+
result.removed.push(path);
|
|
100
|
+
}
|
|
101
|
+
delete lineage.files[path];
|
|
102
|
+
steps.delete(path);
|
|
103
|
+
} else {
|
|
104
|
+
step("removed-upstream");
|
|
105
|
+
result.kept.push(path);
|
|
106
|
+
}
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (local === theirs) {
|
|
111
|
+
record(theirs);
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
if (local === undefined) {
|
|
115
|
+
if (base === undefined) {
|
|
116
|
+
write(abs, incoming!);
|
|
117
|
+
result.written.push(path);
|
|
118
|
+
record(theirs);
|
|
119
|
+
} else if (base === theirs) {
|
|
120
|
+
steps.delete(path);
|
|
121
|
+
} else {
|
|
122
|
+
step("deleted-locally");
|
|
123
|
+
}
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
if (base !== undefined && local === base) {
|
|
127
|
+
write(abs, incoming!);
|
|
128
|
+
result.written.push(path);
|
|
129
|
+
record(theirs);
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
// The tree has its own version of the file.
|
|
133
|
+
if (base !== undefined && base === theirs) {
|
|
134
|
+
result.kept.push(path);
|
|
135
|
+
steps.delete(path);
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
const baseData = merging?.base.get(path);
|
|
139
|
+
if (merging && baseData && fileHash(baseData) === base) {
|
|
140
|
+
const merged = merging.merge(baseData, readFileSync(abs), incoming!);
|
|
141
|
+
if (merged) {
|
|
142
|
+
writeFileSync(abs, merged);
|
|
143
|
+
result.merged.push(path);
|
|
144
|
+
record(theirs);
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
result.kept.push(path);
|
|
149
|
+
step(base === undefined ? "exists-locally" : "changed-locally");
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
lineage.manualSteps = [...steps.values()].sort((a, b) => a.path.localeCompare(b.path));
|
|
153
|
+
lineage.address = { ...(lineage.address ?? {}), digest: contentDigest(upstream) };
|
|
154
|
+
return result;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function write(abs: string, data: Buffer): void {
|
|
158
|
+
mkdirSync(dirname(abs), { recursive: true });
|
|
159
|
+
writeFileSync(abs, data);
|
|
160
|
+
}
|