@intentius/chant 0.90.0 → 0.93.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/cli/commands/build.d.ts.map +1 -1
- package/dist/cli/commands/lint.d.ts.map +1 -1
- package/dist/cli/handlers/components.d.ts +8 -0
- package/dist/cli/handlers/components.d.ts.map +1 -1
- package/dist/cli/handlers/graph.d.ts.map +1 -1
- package/dist/cli/handlers/operator.d.ts +20 -0
- package/dist/cli/handlers/operator.d.ts.map +1 -1
- package/dist/cli/handlers/run.d.ts.map +1 -1
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/mcp/workspace-tools.d.ts +7 -4
- package/dist/cli/mcp/workspace-tools.d.ts.map +1 -1
- package/dist/cli/plugins.d.ts.map +1 -1
- package/dist/cli/registry.d.ts +42 -1
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/codegen/pinned-upgrade.d.ts.map +1 -1
- package/dist/components/verbs/vuln-scan.d.ts +72 -0
- package/dist/components/verbs/vuln-scan.d.ts.map +1 -1
- package/dist/config.d.ts +22 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/lexicon-module.d.ts +22 -0
- package/dist/lexicon-module.d.ts.map +1 -1
- package/dist/lifecycle/converge-ledger.d.ts +19 -0
- package/dist/lifecycle/converge-ledger.d.ts.map +1 -1
- package/dist/lifecycle/git.d.ts +40 -5
- package/dist/lifecycle/git.d.ts.map +1 -1
- package/dist/lifecycle/lease.d.ts +72 -16
- package/dist/lifecycle/lease.d.ts.map +1 -1
- package/dist/lifecycle/member-ledger.d.ts +3 -2
- package/dist/lifecycle/member-ledger.d.ts.map +1 -1
- package/dist/lifecycle/plan-ledger.d.ts +114 -0
- package/dist/lifecycle/plan-ledger.d.ts.map +1 -0
- package/dist/lifecycle/run-ledger.d.ts +2 -0
- package/dist/lifecycle/run-ledger.d.ts.map +1 -1
- package/dist/lifecycle/symptoms.d.ts +51 -0
- package/dist/lifecycle/symptoms.d.ts.map +1 -1
- package/dist/lifecycle/work-lease.d.ts +140 -0
- package/dist/lifecycle/work-lease.d.ts.map +1 -0
- package/dist/lint/rules/index.d.ts +4 -2
- package/dist/lint/rules/index.d.ts.map +1 -1
- package/dist/lint/rules/op/index.d.ts +14 -3
- package/dist/lint/rules/op/index.d.ts.map +1 -1
- package/dist/lint/rules/op/ops014-converge-rule-refusals.d.ts.map +1 -1
- package/dist/lint/rules/op/sys010-key-over-plain-http.d.ts +29 -0
- package/dist/lint/rules/op/sys010-key-over-plain-http.d.ts.map +1 -0
- package/dist/lint/rules/sys001-no-literal-key.d.ts +3 -0
- package/dist/lint/rules/sys001-no-literal-key.d.ts.map +1 -0
- package/dist/op/__fixtures__/decide-stub-backend.d.ts +53 -0
- package/dist/op/__fixtures__/decide-stub-backend.d.ts.map +1 -0
- package/dist/op/activities/activity-contracts.d.ts +203 -2
- package/dist/op/activities/activity-contracts.d.ts.map +1 -1
- package/dist/op/activities/apply.d.ts.map +1 -1
- package/dist/op/activities/change-coverage.d.ts +44 -0
- package/dist/op/activities/change-coverage.d.ts.map +1 -0
- package/dist/op/activities/converge.d.ts +24 -1
- package/dist/op/activities/converge.d.ts.map +1 -1
- package/dist/op/activities/decide.d.ts +107 -0
- package/dist/op/activities/decide.d.ts.map +1 -0
- package/dist/op/activities/index.d.ts +10 -0
- package/dist/op/activities/index.d.ts.map +1 -1
- package/dist/op/activities/shell.d.ts +32 -0
- package/dist/op/activities/shell.d.ts.map +1 -1
- package/dist/op/activities/source-release.d.ts +86 -0
- package/dist/op/activities/source-release.d.ts.map +1 -0
- package/dist/op/activities/source-rollback.d.ts +100 -0
- package/dist/op/activities/source-rollback.d.ts.map +1 -0
- package/dist/op/activities/work-evidence.d.ts +42 -0
- package/dist/op/activities/work-evidence.d.ts.map +1 -0
- package/dist/op/activity-contract.d.ts +1 -0
- package/dist/op/activity-contract.d.ts.map +1 -1
- package/dist/op/activity-profiles.d.ts +16 -0
- package/dist/op/activity-profiles.d.ts.map +1 -1
- package/dist/op/builders.d.ts +84 -2
- package/dist/op/builders.d.ts.map +1 -1
- package/dist/op/composites/converge-op.d.ts +31 -4
- package/dist/op/composites/converge-op.d.ts.map +1 -1
- package/dist/op/decide-backend.d.ts +116 -0
- package/dist/op/decide-backend.d.ts.map +1 -0
- package/dist/op/decide-config.d.ts +120 -0
- package/dist/op/decide-config.d.ts.map +1 -0
- package/dist/op/decide-read-inputs.d.ts +40 -0
- package/dist/op/decide-read-inputs.d.ts.map +1 -0
- package/dist/op/discover.d.ts +25 -0
- package/dist/op/discover.d.ts.map +1 -1
- package/dist/op/gate-wait.d.ts +43 -0
- package/dist/op/gate-wait.d.ts.map +1 -0
- package/dist/op/gate.d.ts +5 -2
- package/dist/op/gate.d.ts.map +1 -1
- package/dist/op/index.d.ts +18 -5
- package/dist/op/index.d.ts.map +1 -1
- package/dist/op/lifecycle-receipt-store.d.ts +34 -0
- package/dist/op/lifecycle-receipt-store.d.ts.map +1 -0
- package/dist/op/local-executor.d.ts +48 -4
- package/dist/op/local-executor.d.ts.map +1 -1
- package/dist/op/local-output.d.ts +2 -0
- package/dist/op/local-output.d.ts.map +1 -1
- package/dist/op/op-ir.d.ts +12 -1
- package/dist/op/op-ir.d.ts.map +1 -1
- package/dist/op/op-verb-class.d.ts.map +1 -1
- package/dist/op/operator.d.ts +126 -1
- package/dist/op/operator.d.ts.map +1 -1
- package/dist/op/runtime.d.ts +46 -5
- package/dist/op/runtime.d.ts.map +1 -1
- package/dist/op/runtimes/local.d.ts.map +1 -1
- package/dist/op/source-archive.d.ts +63 -0
- package/dist/op/source-archive.d.ts.map +1 -0
- package/dist/op/step-output-ref.d.ts +2 -2
- package/dist/op/step-output-ref.d.ts.map +1 -1
- package/dist/op/steward-points.d.ts +136 -0
- package/dist/op/steward-points.d.ts.map +1 -0
- package/dist/op/steward-turn.d.ts +53 -0
- package/dist/op/steward-turn.d.ts.map +1 -0
- package/dist/op/steward.d.ts +161 -0
- package/dist/op/steward.d.ts.map +1 -0
- package/dist/op/types.d.ts +58 -0
- package/dist/op/types.d.ts.map +1 -1
- package/dist/op/work-lease-decl.d.ts +18 -0
- package/dist/op/work-lease-decl.d.ts.map +1 -0
- package/dist/op/work-lease-run.d.ts +173 -0
- package/dist/op/work-lease-run.d.ts.map +1 -0
- package/dist/workspace/__fixtures__/decide-workspace.d.ts +100 -0
- package/dist/workspace/__fixtures__/decide-workspace.d.ts.map +1 -0
- package/dist/workspace/answers-ledger.d.ts +76 -0
- package/dist/workspace/answers-ledger.d.ts.map +1 -0
- package/dist/workspace/box-isolation.d.ts +99 -0
- package/dist/workspace/box-isolation.d.ts.map +1 -0
- package/dist/workspace/changes-cli.d.ts +16 -0
- package/dist/workspace/changes-cli.d.ts.map +1 -0
- package/dist/workspace/changes.d.ts +200 -0
- package/dist/workspace/changes.d.ts.map +1 -0
- package/dist/workspace/chant-migrations/chud-lexicon-exit-fly-site.d.ts +59 -0
- package/dist/workspace/chant-migrations/chud-lexicon-exit-fly-site.d.ts.map +1 -0
- package/dist/workspace/chant-migrations/chud-lexicon-exit-live-names.d.ts +51 -0
- package/dist/workspace/chant-migrations/chud-lexicon-exit-live-names.d.ts.map +1 -0
- package/dist/workspace/chant-migrations/chud-lexicon-exit-rollback.d.ts +26 -0
- package/dist/workspace/chant-migrations/chud-lexicon-exit-rollback.d.ts.map +1 -0
- package/dist/workspace/chant-migrations/chud-lexicon-exit-ship-inputs.d.ts +59 -0
- package/dist/workspace/chant-migrations/chud-lexicon-exit-ship-inputs.d.ts.map +1 -0
- package/dist/workspace/chant-migrations/chud-lexicon-exit-ship-policy.d.ts +22 -0
- package/dist/workspace/chant-migrations/chud-lexicon-exit-ship-policy.d.ts.map +1 -0
- package/dist/workspace/chant-migrations/chud-lexicon-exit.d.ts +71 -0
- package/dist/workspace/chant-migrations/chud-lexicon-exit.d.ts.map +1 -0
- package/dist/workspace/chant-migrations.d.ts +99 -0
- package/dist/workspace/chant-migrations.d.ts.map +1 -0
- package/dist/workspace/checks/box-isolation.d.ts +18 -0
- package/dist/workspace/checks/box-isolation.d.ts.map +1 -0
- package/dist/workspace/checks/boxes.d.ts +104 -0
- package/dist/workspace/checks/boxes.d.ts.map +1 -0
- package/dist/workspace/checks/diagrams.d.ts +31 -0
- package/dist/workspace/checks/diagrams.d.ts.map +1 -0
- package/dist/workspace/checks/records.d.ts +8 -0
- package/dist/workspace/checks/records.d.ts.map +1 -1
- package/dist/workspace/checks.d.ts +13 -1
- package/dist/workspace/checks.d.ts.map +1 -1
- package/dist/workspace/compose-graph.d.ts +2 -0
- package/dist/workspace/compose-graph.d.ts.map +1 -1
- package/dist/workspace/conformance/index.d.ts.map +1 -1
- package/dist/workspace/decide.d.ts +229 -0
- package/dist/workspace/decide.d.ts.map +1 -0
- package/dist/workspace/decision-points.schema.json +137 -0
- package/dist/workspace/declaration.d.ts +103 -0
- package/dist/workspace/declaration.d.ts.map +1 -1
- package/dist/workspace/declaration.schema.json +285 -0
- package/dist/workspace/declared-kinds.d.ts +32 -0
- package/dist/workspace/declared-kinds.d.ts.map +1 -1
- package/dist/workspace/generated-files.d.ts +7 -0
- package/dist/workspace/generated-files.d.ts.map +1 -1
- package/dist/workspace/intent.d.ts.map +1 -1
- package/dist/workspace/lineage-check.d.ts +3 -0
- package/dist/workspace/lineage-check.d.ts.map +1 -1
- package/dist/workspace/lineage-upgrade.d.ts +6 -1
- package/dist/workspace/lineage-upgrade.d.ts.map +1 -1
- package/dist/workspace/ls.d.ts +39 -1
- package/dist/workspace/ls.d.ts.map +1 -1
- package/dist/workspace/point-answer.schema.json +95 -0
- package/dist/workspace/points-cli.d.ts +113 -0
- package/dist/workspace/points-cli.d.ts.map +1 -0
- package/dist/workspace/points.d.ts +320 -0
- package/dist/workspace/points.d.ts.map +1 -0
- package/dist/workspace/reason-codes.d.ts +38 -0
- package/dist/workspace/reason-codes.d.ts.map +1 -1
- package/dist/workspace/record-assets.d.ts.map +1 -1
- package/dist/workspace/records-cli.d.ts +23 -1
- package/dist/workspace/records-cli.d.ts.map +1 -1
- package/dist/workspace/records-close.d.ts +1 -1
- package/dist/workspace/records-close.d.ts.map +1 -1
- package/dist/workspace/records-write.d.ts +11 -5
- package/dist/workspace/records-write.d.ts.map +1 -1
- package/dist/workspace/records.d.ts +42 -6
- package/dist/workspace/records.d.ts.map +1 -1
- package/dist/workspace/status-gates.d.ts +7 -2
- package/dist/workspace/status-gates.d.ts.map +1 -1
- package/dist/workspace/status-stewards.d.ts +179 -0
- package/dist/workspace/status-stewards.d.ts.map +1 -0
- package/dist/workspace/status.d.ts +68 -1
- package/dist/workspace/status.d.ts.map +1 -1
- package/dist/workspace/work-cli.d.ts +189 -0
- package/dist/workspace/work-cli.d.ts.map +1 -0
- package/dist/workspace/work-evidence.d.ts +74 -0
- package/dist/workspace/work-evidence.d.ts.map +1 -0
- package/dist/workspace/work.d.ts +43 -1
- package/dist/workspace/work.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/cli/commands/audit.test.ts +9 -1
- package/src/cli/commands/build.ts +3 -2
- package/src/cli/commands/lint.ts +7 -1
- package/src/cli/handlers/components.test.ts +93 -0
- package/src/cli/handlers/components.ts +44 -3
- package/src/cli/handlers/graph.test.ts +20 -0
- package/src/cli/handlers/graph.ts +16 -2
- package/src/cli/handlers/operator.ts +141 -4
- package/src/cli/handlers/run.test.ts +173 -2
- package/src/cli/handlers/run.ts +174 -3
- package/src/cli/main.test.ts +50 -0
- package/src/cli/main.ts +97 -2
- package/src/cli/mcp/workspace-tools.test.ts +48 -19
- package/src/cli/mcp/workspace-tools.ts +110 -8
- package/src/cli/plugins.ts +11 -3
- package/src/cli/registry.ts +42 -1
- package/src/codegen/pinned-upgrade.ts +2 -1
- package/src/codegen/release-wiring.test.ts +49 -2
- package/src/components/capability-plugin-loader.ts +3 -3
- package/src/components/cli-support.ts +2 -2
- package/src/components/verbs/vuln-scan.test.ts +124 -1
- package/src/components/verbs/vuln-scan.ts +142 -1
- package/src/config.ts +11 -0
- package/src/lexicon-module.ts +56 -1
- package/src/lexicon-package.test.ts +96 -0
- package/src/lifecycle/converge-ledger.ts +20 -0
- package/src/lifecycle/git.ts +65 -15
- package/src/lifecycle/lease.test.ts +22 -0
- package/src/lifecycle/lease.ts +133 -29
- package/src/lifecycle/member-ledger.ts +3 -2
- package/src/lifecycle/plan-ledger.test.ts +148 -0
- package/src/lifecycle/plan-ledger.ts +158 -0
- package/src/lifecycle/run-ledger.ts +6 -0
- package/src/lifecycle/symptoms.ts +95 -0
- package/src/lifecycle/work-lease.test.ts +236 -0
- package/src/lifecycle/work-lease.ts +426 -0
- package/src/lint/rules/index.ts +6 -2
- package/src/lint/rules/op/index.ts +17 -4
- package/src/lint/rules/op/ops014-converge-rule-refusals.test.ts +23 -1
- package/src/lint/rules/op/ops014-converge-rule-refusals.ts +10 -6
- package/src/lint/rules/op/sys010-key-over-plain-http.test.ts +52 -0
- package/src/lint/rules/op/sys010-key-over-plain-http.ts +100 -0
- package/src/lint/rules/sys001-no-literal-key.test.ts +31 -0
- package/src/lint/rules/sys001-no-literal-key.ts +67 -0
- package/src/op/__fixtures__/decide-stub-backend.ts +143 -0
- package/src/op/activities/activity-contracts.ts +190 -1
- package/src/op/activities/apply.ts +7 -6
- package/src/op/activities/change-coverage.ts +60 -0
- package/src/op/activities/converge.test.ts +55 -1
- package/src/op/activities/converge.ts +208 -29
- package/src/op/activities/decide.live.test.ts +78 -0
- package/src/op/activities/decide.test.ts +300 -0
- package/src/op/activities/decide.ts +267 -0
- package/src/op/activities/index.ts +21 -0
- package/src/op/activities/shell.test.ts +91 -1
- package/src/op/activities/shell.ts +91 -2
- package/src/op/activities/source-release.test.ts +118 -0
- package/src/op/activities/source-release.ts +168 -0
- package/src/op/activities/source-rollback.test.ts +131 -0
- package/src/op/activities/source-rollback.ts +239 -0
- package/src/op/activities/work-evidence.ts +62 -0
- package/src/op/activity-contract-registry.test.ts +2 -0
- package/src/op/activity-contract-registry.ts +2 -2
- package/src/op/activity-contract.test.ts +12 -0
- package/src/op/activity-contract.ts +6 -1
- package/src/op/activity-profiles.ts +29 -0
- package/src/op/activity-registry.test.ts +1 -1
- package/src/op/activity-registry.ts +2 -2
- package/src/op/builders.ts +124 -5
- package/src/op/command-gate.test.ts +192 -0
- package/src/op/composites/converge-op.test.ts +27 -1
- package/src/op/composites/converge-op.ts +69 -15
- package/src/op/decide-backend.test.ts +44 -0
- package/src/op/decide-backend.ts +212 -0
- package/src/op/decide-config.test.ts +52 -0
- package/src/op/decide-config.ts +80 -0
- package/src/op/decide-read-inputs.ts +117 -0
- package/src/op/discover.ts +71 -0
- package/src/op/gate-approval.ts +2 -2
- package/src/op/gate-wait.ts +57 -0
- package/src/op/gate.ts +8 -3
- package/src/op/generate-pipeline.ts +2 -2
- package/src/op/index.ts +34 -6
- package/src/op/lifecycle-receipt-store.test.ts +60 -0
- package/src/op/lifecycle-receipt-store.ts +61 -0
- package/src/op/local-executor.ts +367 -33
- package/src/op/local-output.test.ts +33 -0
- package/src/op/local-output.ts +33 -2
- package/src/op/op-ir.test.ts +10 -0
- package/src/op/op-ir.ts +18 -0
- package/src/op/op-verb-class.ts +1 -0
- package/src/op/operator.ts +360 -10
- package/src/op/runtime.ts +37 -8
- package/src/op/runtimes/local.ts +6 -1
- package/src/op/source-archive.ts +161 -0
- package/src/op/step-output-ref.ts +6 -2
- package/src/op/steward-points.test.ts +313 -0
- package/src/op/steward-points.ts +194 -0
- package/src/op/steward-turn.ts +79 -0
- package/src/op/steward.test.ts +296 -0
- package/src/op/steward.ts +277 -0
- package/src/op/types.ts +60 -0
- package/src/op/work-lease-decl.ts +80 -0
- package/src/op/work-lease-run.test.ts +345 -0
- package/src/op/work-lease-run.ts +395 -0
- package/src/workspace/__fixtures__/decide-workspace.ts +129 -0
- package/src/workspace/__fixtures__/studio-template-84c8b21.json +67 -0
- package/src/workspace/answers-ledger.ts +118 -0
- package/src/workspace/box-isolation.test.ts +261 -0
- package/src/workspace/box-isolation.ts +205 -0
- package/src/workspace/changes-cli.ts +56 -0
- package/src/workspace/changes.schema.json +472 -0
- package/src/workspace/changes.test.ts +315 -0
- package/src/workspace/changes.ts +512 -0
- package/src/workspace/chant-migrations/chud-lexicon-exit-fly-site.ts +234 -0
- package/src/workspace/chant-migrations/chud-lexicon-exit-live-names.ts +164 -0
- package/src/workspace/chant-migrations/chud-lexicon-exit-rollback.ts +223 -0
- package/src/workspace/chant-migrations/chud-lexicon-exit-ship-inputs.ts +195 -0
- package/src/workspace/chant-migrations/chud-lexicon-exit-ship-policy.ts +108 -0
- package/src/workspace/chant-migrations/chud-lexicon-exit.ts +882 -0
- package/src/workspace/chant-migrations.e2e.test.ts +1089 -0
- package/src/workspace/chant-migrations.ts +164 -0
- package/src/workspace/check-contract.test.ts +5 -1
- package/src/workspace/check.schema.json +25 -7
- package/src/workspace/checks/box-isolation.ts +68 -0
- package/src/workspace/checks/boxes.test.ts +276 -0
- package/src/workspace/checks/boxes.ts +442 -0
- package/src/workspace/checks/diagrams.test.ts +134 -0
- package/src/workspace/checks/diagrams.ts +108 -0
- package/src/workspace/checks/records.ts +72 -0
- package/src/workspace/checks.test.ts +7 -0
- package/src/workspace/checks.ts +27 -3
- package/src/workspace/compose-graph.ts +2 -0
- package/src/workspace/conformance/__fixture__/decisions/decision.kind.mjs +10 -0
- package/src/workspace/conformance/__fixture__/decisions/decision.schema.json +26 -0
- package/src/workspace/conformance/index.ts +5 -0
- package/src/workspace/decide.test.ts +224 -0
- package/src/workspace/decide.ts +709 -0
- package/src/workspace/decision-points.schema.json +137 -0
- package/src/workspace/declaration.schema.json +285 -0
- package/src/workspace/declaration.test.ts +58 -0
- package/src/workspace/declaration.ts +241 -0
- package/src/workspace/declared-kinds.test.ts +5 -5
- package/src/workspace/declared-kinds.ts +71 -3
- package/src/workspace/generated-files.ts +14 -0
- package/src/workspace/graph-cli.ts +1 -1
- package/src/workspace/graph.schema.json +9 -1
- package/src/workspace/intent.schema.json +12 -3
- package/src/workspace/intent.ts +3 -10
- package/src/workspace/lineage-check.ts +16 -1
- package/src/workspace/lineage-upgrade-cli.ts +1 -1
- package/src/workspace/lineage-upgrade.test.ts +70 -0
- package/src/workspace/lineage-upgrade.ts +81 -10
- package/src/workspace/ls-contract.test.ts +57 -0
- package/src/workspace/ls.schema.json +46 -1
- package/src/workspace/ls.ts +62 -3
- package/src/workspace/point-answer.schema.json +95 -0
- package/src/workspace/points-cli.ts +284 -0
- package/src/workspace/points-write.schema.json +499 -0
- package/src/workspace/points.schema.json +743 -0
- package/src/workspace/points.test.ts +264 -0
- package/src/workspace/points.ts +564 -0
- package/src/workspace/read-contract.test.ts +18 -1
- package/src/workspace/reason-codes.test.ts +24 -1
- package/src/workspace/reason-codes.ts +49 -0
- package/src/workspace/record-assets.test.ts +3 -2
- package/src/workspace/record-assets.ts +4 -1
- package/src/workspace/records-amend.schema.json +2 -0
- package/src/workspace/records-cli.ts +28 -5
- package/src/workspace/records-close.schema.json +2 -0
- package/src/workspace/records-contract.test.ts +3 -2
- package/src/workspace/records-new.schema.json +2 -0
- package/src/workspace/records-review.schema.json +2 -0
- package/src/workspace/records-sessions-write.e2e.test.ts +77 -0
- package/src/workspace/records-sessions-write.test.ts +2 -34
- package/src/workspace/records-write.e2e.test.ts +108 -0
- package/src/workspace/records-write.test.ts +32 -41
- package/src/workspace/records-write.ts +33 -15
- package/src/workspace/records.schema.json +53 -2
- package/src/workspace/records.test.ts +48 -3
- package/src/workspace/records.ts +142 -6
- package/src/workspace/status-contract.test.ts +189 -6
- package/src/workspace/status-gates.ts +9 -4
- package/src/workspace/status-stewards.ts +329 -0
- package/src/workspace/status.schema.json +335 -5
- package/src/workspace/status.ts +142 -6
- package/src/workspace/work-acceptance.test.ts +274 -0
- package/src/workspace/work-cli.e2e.test.ts +180 -0
- package/src/workspace/work-cli.ts +429 -0
- package/src/workspace/work-evidence.schema.json +212 -0
- package/src/workspace/work-evidence.ts +169 -0
- package/src/workspace/work-history.schema.json +349 -0
- package/src/workspace/work-history.test.ts +156 -0
- package/src/workspace/work-lease.schema.json +233 -0
- package/src/workspace/work-readiness-chud.test.ts +145 -0
- package/src/workspace/work.test.ts +3 -2
- package/src/workspace/work.ts +100 -1
- /package/src/cli/{discovery-skip.test.ts → discovery-skip.e2e.test.ts} +0 -0
- /package/src/workspace/conformance/{conformance.test.ts → conformance.e2e.test.ts} +0 -0
- /package/src/workspace/{which-chant.test.ts → which-chant.e2e.test.ts} +0 -0
package/src/lifecycle/git.ts
CHANGED
|
@@ -1100,34 +1100,70 @@ export async function readBlobBySha(sha: string, opts?: { cwd?: string }): Promi
|
|
|
1100
1100
|
* Push one arbitrary ref (e.g. a lease ref) to the remote, guarded the same
|
|
1101
1101
|
* way {@link pushLifecycle} guards the ledger branch: `--force-with-lease`
|
|
1102
1102
|
* keyed to the remote SHA last observed locally, so a concurrent push from a
|
|
1103
|
-
* second machine is rejected rather than silently clobbered.
|
|
1104
|
-
*
|
|
1105
|
-
* commit descending from the previous one
|
|
1106
|
-
*
|
|
1103
|
+
* second machine is rejected rather than silently clobbered. The lease alone
|
|
1104
|
+
* lets a push replace a value that is not an ancestor — a lease ref's value is
|
|
1105
|
+
* a bare blob SHA, not a commit descending from the previous one — as long as
|
|
1106
|
+
* the remote still holds the expected value. No `--force` beside it: git lets
|
|
1107
|
+
* `--force` override `--force-with-lease`, which made every lease push
|
|
1108
|
+
* unconditional before #2732.
|
|
1107
1109
|
*
|
|
1108
1110
|
* Returns `false` (never throws) when no remote is configured — a
|
|
1109
1111
|
* remote-less project's lease is local-only by construction (see
|
|
1110
1112
|
* `./lease.ts`'s module doc), or when the push itself is rejected (the
|
|
1111
|
-
* caller re-reads and retries; see `acquireLease`).
|
|
1113
|
+
* caller re-reads and retries; see `acquireLease`). {@link pushRefStatus}
|
|
1114
|
+
* tells those two apart.
|
|
1112
1115
|
*/
|
|
1113
|
-
export async function pushRef(ref: string, opts?: { cwd?: string }): Promise<boolean> {
|
|
1116
|
+
export async function pushRef(ref: string, opts?: { cwd?: string; expect?: string | null }): Promise<boolean> {
|
|
1117
|
+
return (await pushRefStatus(ref, opts)) === "pushed";
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
/** What {@link pushRefStatus} did: the remote took the ref, there is no remote, or the remote refused it. */
|
|
1121
|
+
export type PushRefStatus = "pushed" | "no-remote" | "rejected";
|
|
1122
|
+
|
|
1123
|
+
/**
|
|
1124
|
+
* {@link pushRef}, saying why a push did not land (#2732).
|
|
1125
|
+
*
|
|
1126
|
+
* `opts.expect` is the value the caller last saw the ref at on the remote
|
|
1127
|
+
* (`null`: absent there), and becomes the `--force-with-lease` expectation.
|
|
1128
|
+
* Without it the expectation is `refs/remotes/<remote>/<ref>`, which nothing
|
|
1129
|
+
* fetches a lease ref into, so it reads as "absent" and only a first push
|
|
1130
|
+
* lands. A lease passes the value of its own remote-tracking ref instead
|
|
1131
|
+
* (`./lease.ts`'s `readLease`).
|
|
1132
|
+
*
|
|
1133
|
+
* A ref that no longer exists locally is pushed as a deletion, under the same
|
|
1134
|
+
* expectation, so a released lease is released on the remote too. Deleting a
|
|
1135
|
+
* ref the caller expects to be absent already is a no-op that reports
|
|
1136
|
+
* `pushed`.
|
|
1137
|
+
*/
|
|
1138
|
+
export async function pushRefStatus(ref: string, opts?: { cwd?: string; expect?: string | null }): Promise<PushRefStatus> {
|
|
1114
1139
|
const rt = getRuntime();
|
|
1115
1140
|
const remoteResult = await rt.spawn(["git", "remote"], { cwd: opts?.cwd });
|
|
1116
|
-
if (remoteResult.exitCode !== 0 || !remoteResult.stdout.trim()) return
|
|
1141
|
+
if (remoteResult.exitCode !== 0 || !remoteResult.stdout.trim()) return "no-remote";
|
|
1117
1142
|
const remote = remoteResult.stdout.trim().split("\n")[0];
|
|
1118
1143
|
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1144
|
+
let expected: string | null;
|
|
1145
|
+
if (opts?.expect !== undefined) {
|
|
1146
|
+
expected = opts.expect;
|
|
1147
|
+
} else {
|
|
1148
|
+
const remoteRef = `refs/remotes/${remote}/${ref.replace(/^refs\//, "")}`;
|
|
1149
|
+
const expectedResult = await rt.spawn(["git", "rev-parse", "--verify", remoteRef], { cwd: opts?.cwd });
|
|
1150
|
+
expected = expectedResult.exitCode === 0 ? expectedResult.stdout.trim() : null;
|
|
1151
|
+
}
|
|
1122
1152
|
const lease = `${ref}:${expected ?? ""}`;
|
|
1123
1153
|
|
|
1154
|
+
const local = await readRefSha(ref, opts);
|
|
1155
|
+
if (local === null && expected === null) return "pushed";
|
|
1156
|
+
const refspec = local === null ? `:${ref}` : `${ref}:${ref}`;
|
|
1124
1157
|
const pushResult = await rt.spawn(
|
|
1125
|
-
["git", "push",
|
|
1158
|
+
["git", "push", `--force-with-lease=${lease}`, remote, refspec],
|
|
1126
1159
|
{ cwd: opts?.cwd },
|
|
1127
1160
|
);
|
|
1128
|
-
return pushResult.exitCode === 0;
|
|
1161
|
+
return pushResult.exitCode === 0 ? "pushed" : "rejected";
|
|
1129
1162
|
}
|
|
1130
1163
|
|
|
1164
|
+
/** What {@link fetchRefIntoStatus} found: the ref fetched, no remote, the remote answered without the ref, or the fetch failed. */
|
|
1165
|
+
export type FetchRefStatus = "fetched" | "no-remote" | "missing" | "failed";
|
|
1166
|
+
|
|
1131
1167
|
/**
|
|
1132
1168
|
* Fetch one arbitrary remote ref into a local ref of a possibly *different*
|
|
1133
1169
|
* name (#1959 finding 3). `+` forces the update even when it isn't a
|
|
@@ -1145,12 +1181,26 @@ export async function fetchRefInto(
|
|
|
1145
1181
|
localRef: string,
|
|
1146
1182
|
opts?: { cwd?: string },
|
|
1147
1183
|
): Promise<boolean> {
|
|
1184
|
+
return (await fetchRefIntoStatus(remoteRef, localRef, opts)) === "fetched";
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
/**
|
|
1188
|
+
* {@link fetchRefInto}, saying why nothing was fetched (#2732). `missing`
|
|
1189
|
+
* means the remote answered and does not carry the ref, which for a lease is
|
|
1190
|
+
* the news that it was released there.
|
|
1191
|
+
*/
|
|
1192
|
+
export async function fetchRefIntoStatus(
|
|
1193
|
+
remoteRef: string,
|
|
1194
|
+
localRef: string,
|
|
1195
|
+
opts?: { cwd?: string },
|
|
1196
|
+
): Promise<FetchRefStatus> {
|
|
1148
1197
|
const rt = getRuntime();
|
|
1149
1198
|
const remoteResult = await rt.spawn(["git", "remote"], { cwd: opts?.cwd });
|
|
1150
|
-
if (remoteResult.exitCode !== 0 || !remoteResult.stdout.trim()) return
|
|
1199
|
+
if (remoteResult.exitCode !== 0 || !remoteResult.stdout.trim()) return "no-remote";
|
|
1151
1200
|
const remote = remoteResult.stdout.trim().split("\n")[0];
|
|
1152
|
-
const fetchResult = await rt.spawn(["git", "fetch", remote, `+${remoteRef}:${localRef}`], { cwd: opts?.cwd });
|
|
1153
|
-
|
|
1201
|
+
const fetchResult = await rt.spawn(["git", "fetch", remote, `+${remoteRef}:${localRef}`], { cwd: opts?.cwd, env: C_LOCALE_ENV });
|
|
1202
|
+
if (fetchResult.exitCode === 0) return "fetched";
|
|
1203
|
+
return NO_SUCH_REMOTE_REF_RE.test(fetchResult.stderr ?? "") ? "missing" : "failed";
|
|
1154
1204
|
}
|
|
1155
1205
|
|
|
1156
1206
|
/**
|
|
@@ -285,6 +285,28 @@ describe("lifecycle/lease", () => {
|
|
|
285
285
|
}
|
|
286
286
|
});
|
|
287
287
|
|
|
288
|
+
// #2732: pushRef passed `--force` beside `--force-with-lease`, and git
|
|
289
|
+
// lets `--force` win, so every lease push was unconditional. It now pushes
|
|
290
|
+
// under the lease alone, expecting the value last fetched, so a renewal
|
|
291
|
+
// lands and a release deletes the ref on the remote.
|
|
292
|
+
test("a renewal and a release reach the remote, under the lease the last fetch saw (#2732)", async () => {
|
|
293
|
+
const { clonePath, remotePath, cleanup } = await setupClonePair();
|
|
294
|
+
try {
|
|
295
|
+
const ref = leaseRef("fountain-converge");
|
|
296
|
+
const first = await acquireLease("fountain-converge", "operator-a", { cwd: clonePath, ttlMs: 60_000 });
|
|
297
|
+
expect(first.acquired).toBe(true);
|
|
298
|
+
const renewed = await acquireLease("fountain-converge", "operator-a", { cwd: clonePath, ttlMs: 120_000 });
|
|
299
|
+
expect(renewed.lease?.token).toBe(first.lease?.token);
|
|
300
|
+
const local = git(["rev-parse", ref], clonePath).stdout.trim();
|
|
301
|
+
expect(git(["rev-parse", ref], remotePath).stdout.trim()).toBe(local);
|
|
302
|
+
|
|
303
|
+
expect(await releaseLease("fountain-converge", "operator-a", renewed.lease!.token, { cwd: clonePath })).toBe(true);
|
|
304
|
+
expect(git(["rev-parse", "--verify", ref], remotePath).exitCode).not.toBe(0);
|
|
305
|
+
} finally {
|
|
306
|
+
await cleanup();
|
|
307
|
+
}
|
|
308
|
+
});
|
|
309
|
+
|
|
288
310
|
// ── #1959 finding 3 ──────────────────────────────────────────────────
|
|
289
311
|
//
|
|
290
312
|
// `readLease` used to fetch the remote lease ref directly into the same
|
package/src/lifecycle/lease.ts
CHANGED
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
*/
|
|
34
34
|
import { hostname } from "node:os";
|
|
35
35
|
import { randomUUID } from "node:crypto";
|
|
36
|
-
import { readRefSha, updateRefCAS, deleteRefCAS, writeBlob, readBlobBySha,
|
|
36
|
+
import { readRefSha, updateRefCAS, deleteRefCAS, writeBlob, readBlobBySha, pushRefStatus, fetchRefIntoStatus, RefCASConflictError, StaleLockError } from "./git";
|
|
37
37
|
import { resolveMemberLedger } from "./member-ledger";
|
|
38
38
|
|
|
39
39
|
export const LEASE_REF_PREFIX = "refs/chant/lease/";
|
|
@@ -95,7 +95,7 @@ function leaseFreshnessKey(record?: LeaseRecord): string {
|
|
|
95
95
|
return record ? `${record.acquiredAt} ${record.expiresAt}` : "";
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
-
/** One lease's live state
|
|
98
|
+
/** One lease's live state: the current holder only. An operator lease keeps no history; a work lease (./work-lease.ts) appends each change to `_leases/<id>.jsonl` beside it. */
|
|
99
99
|
export interface LeaseRecord {
|
|
100
100
|
op: string;
|
|
101
101
|
/** `<hostname>:<pid>:<random>` — a diagnostic identity, not itself the fencing mechanism (`token` is). */
|
|
@@ -137,6 +137,23 @@ export interface ReadLeaseResult {
|
|
|
137
137
|
/** The ref's current SHA (the CAS anchor for the next write), or `null` if no lease has ever been written. */
|
|
138
138
|
sha: string | null;
|
|
139
139
|
record?: LeaseRecord;
|
|
140
|
+
/**
|
|
141
|
+
* The value of the remote-tracking ref (`refs/chant/lease-remote/<op>`)
|
|
142
|
+
* after the fetch: what the remote held when last seen, or `null` when it
|
|
143
|
+
* held nothing. The `--force-with-lease` expectation of the next push
|
|
144
|
+
* (#2732).
|
|
145
|
+
*/
|
|
146
|
+
remoteSha: string | null;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** Options every lease read and write takes. `cwd` also picks the ledger, and so the member prefix of the refs (./member-ledger.ts). */
|
|
150
|
+
export interface LeaseOptions {
|
|
151
|
+
cwd?: string;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async function readRecordAt(ref: string, opts?: { cwd?: string }): Promise<{ sha: string | null; record?: LeaseRecord }> {
|
|
155
|
+
const sha = await readRefSha(ref, opts);
|
|
156
|
+
return { sha, record: sha ? parseLease((await readBlobBySha(sha, opts)) ?? "") : undefined };
|
|
140
157
|
}
|
|
141
158
|
|
|
142
159
|
/**
|
|
@@ -159,26 +176,76 @@ export interface ReadLeaseResult {
|
|
|
159
176
|
* against — is always the local ref's own actual value; only the local
|
|
160
177
|
* canonical ref is ever a valid basis for a `updateRefCAS`/`deleteRefCAS`
|
|
161
178
|
* call against it, regardless of what the comparison decided about `record`.
|
|
179
|
+
*
|
|
180
|
+
* When the remote answers without the ref, the lease was released there, and
|
|
181
|
+
* the tracking ref is dropped so a stale copy of it no longer counts (#2732).
|
|
182
|
+
* A read-only report that must stay off the network reads the refs itself
|
|
183
|
+
* (`./work-lease.ts`'s `listWorkLeases`).
|
|
162
184
|
*/
|
|
163
|
-
export async function readLease(opName: string, opts?:
|
|
185
|
+
export async function readLease(opName: string, opts?: LeaseOptions): Promise<ReadLeaseResult> {
|
|
164
186
|
const { ref, trackingRef } = await projectLeaseRefs(opName, opts);
|
|
165
|
-
await
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
187
|
+
const fetched = await fetchRefIntoStatus(ref, trackingRef, opts).catch(() => "failed" as const);
|
|
188
|
+
if (fetched === "missing") {
|
|
189
|
+
const stale = await readRefSha(trackingRef, opts);
|
|
190
|
+
if (stale) await deleteRefCAS(trackingRef, stale, opts).catch(() => undefined);
|
|
191
|
+
}
|
|
169
192
|
|
|
170
|
-
const
|
|
171
|
-
const
|
|
193
|
+
const { sha, record: localRecord } = await readRecordAt(ref, opts);
|
|
194
|
+
const { sha: remoteSha, record: remoteRecord } = await readRecordAt(trackingRef, opts);
|
|
172
195
|
|
|
173
196
|
const record = leaseFreshnessKey(remoteRecord) > leaseFreshnessKey(localRecord) ? remoteRecord : localRecord;
|
|
174
|
-
return { sha, record };
|
|
197
|
+
return { sha, record, remoteSha };
|
|
175
198
|
}
|
|
176
199
|
|
|
200
|
+
/** Why {@link acquireLease} did not acquire (#2732). */
|
|
201
|
+
export type LeaseRefusal =
|
|
202
|
+
/** Someone holds it live: another holder, or, under `mode: "claim"`, anyone. */
|
|
203
|
+
| "held"
|
|
204
|
+
/** `mode: "renew"` and nobody holds it live any more: it expired or was released. */
|
|
205
|
+
| "not-held"
|
|
206
|
+
/** `mode: "renew"` and the live lease has another token than the one given. */
|
|
207
|
+
| "token-mismatch"
|
|
208
|
+
/** Another writer changed the ref between this call's read and its write. */
|
|
209
|
+
| "race"
|
|
210
|
+
/** `requirePush` and the remote refused the push: another clone got there first, or the remote could not be reached. */
|
|
211
|
+
| "push-rejected";
|
|
212
|
+
|
|
177
213
|
export interface AcquireLeaseResult {
|
|
178
214
|
acquired: boolean;
|
|
179
215
|
lease?: LeaseRecord;
|
|
180
216
|
/** Present when not acquired: the lease record currently held by someone else. */
|
|
181
217
|
heldBy?: LeaseRecord;
|
|
218
|
+
/** Present when not acquired: why (#2732). */
|
|
219
|
+
reason?: LeaseRefusal;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** Options for {@link acquireLease}. */
|
|
223
|
+
export interface AcquireLeaseOptions extends LeaseOptions {
|
|
224
|
+
ttlMs?: number;
|
|
225
|
+
now?: () => Date;
|
|
226
|
+
/**
|
|
227
|
+
* `acquire` (the default, the operator's): take it when free or expired,
|
|
228
|
+
* renew it when `holder` already holds it. `claim`: take it only when free
|
|
229
|
+
* or expired, and refuse even its own holder. `renew`: only move the expiry
|
|
230
|
+
* of a live lease `holder` holds (#2732).
|
|
231
|
+
*/
|
|
232
|
+
mode?: "acquire" | "claim" | "renew";
|
|
233
|
+
/** With `mode: "renew"`, the token the caller holds; a live lease with another token is refused. */
|
|
234
|
+
token?: string;
|
|
235
|
+
/**
|
|
236
|
+
* Count the write only once the remote has taken it, when there is a
|
|
237
|
+
* remote. A rejected push undoes the local write and refuses with
|
|
238
|
+
* `push-rejected`, so two clones racing for one lease cannot both hold it.
|
|
239
|
+
* The operator leaves it off: its push is best-effort (#2732).
|
|
240
|
+
*/
|
|
241
|
+
requirePush?: boolean;
|
|
242
|
+
/**
|
|
243
|
+
* Wait out a `.lock` another process holds on the ref for up to this long,
|
|
244
|
+
* retrying, before surfacing it as a {@link StaleLockError}. A lock left by
|
|
245
|
+
* a killed process outlives the wait; one held by a racing writer does
|
|
246
|
+
* not. Off (0) by default, as the operator has always had it.
|
|
247
|
+
*/
|
|
248
|
+
lockWaitMs?: number;
|
|
182
249
|
}
|
|
183
250
|
|
|
184
251
|
/**
|
|
@@ -198,22 +265,42 @@ export interface AcquireLeaseResult {
|
|
|
198
265
|
* against a lease nobody can ever actually acquire again without manual
|
|
199
266
|
* intervention. It propagates instead, so the caller (`../op/operator.ts`'s
|
|
200
267
|
* `runOperatorRound`) can surface it as its own distinct, diagnosable event
|
|
201
|
-
* rather than a silent, permanent skip.
|
|
268
|
+
* rather than a silent, permanent skip. `lockWaitMs` waits a racing writer's
|
|
269
|
+
* lock out first.
|
|
270
|
+
*
|
|
271
|
+
* `mode`, `token` and `requirePush` are the work lease's (#2732,
|
|
272
|
+
* ./work-lease.ts); the operator uses none of them.
|
|
202
273
|
*/
|
|
203
274
|
export async function acquireLease(
|
|
204
275
|
opName: string,
|
|
205
276
|
holder: string,
|
|
206
|
-
opts?:
|
|
277
|
+
opts?: AcquireLeaseOptions,
|
|
207
278
|
): Promise<AcquireLeaseResult> {
|
|
279
|
+
const deadline = Date.now() + (opts?.lockWaitMs ?? 0);
|
|
280
|
+
for (;;) {
|
|
281
|
+
try {
|
|
282
|
+
return await acquireOnce(opName, holder, opts);
|
|
283
|
+
} catch (err) {
|
|
284
|
+
if (!(err instanceof StaleLockError) || Date.now() >= deadline) throw err;
|
|
285
|
+
await new Promise((r) => setTimeout(r, 20 + Math.floor(Math.random() * 60)));
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
async function acquireOnce(opName: string, holder: string, opts?: AcquireLeaseOptions): Promise<AcquireLeaseResult> {
|
|
208
291
|
const ttlMs = opts?.ttlMs ?? DEFAULT_LEASE_TTL_MS;
|
|
209
292
|
const now = opts?.now?.() ?? new Date();
|
|
293
|
+
const mode = opts?.mode ?? "acquire";
|
|
210
294
|
|
|
211
|
-
const { sha, record: current } = await readLease(opName, opts);
|
|
295
|
+
const { sha, record: current, remoteSha } = await readLease(opName, opts);
|
|
212
296
|
const expired = !current || isExpired(current, now);
|
|
213
297
|
const ownedByUs = current?.holder === holder;
|
|
214
298
|
|
|
215
|
-
if (
|
|
216
|
-
return { acquired: false, heldBy: current };
|
|
299
|
+
if (mode === "renew") {
|
|
300
|
+
if (!current || expired || !ownedByUs) return { acquired: false, reason: current && !expired ? "held" : "not-held", ...(current ? { heldBy: current } : {}) };
|
|
301
|
+
if (opts?.token !== undefined && current.token !== opts.token) return { acquired: false, reason: "token-mismatch", heldBy: current };
|
|
302
|
+
} else if (current && !expired && (!ownedByUs || mode === "claim")) {
|
|
303
|
+
return { acquired: false, heldBy: current, reason: "held" };
|
|
217
304
|
}
|
|
218
305
|
|
|
219
306
|
const renewing = !!current && ownedByUs && !expired;
|
|
@@ -225,21 +312,31 @@ export async function acquireLease(
|
|
|
225
312
|
expiresAt: new Date(now.getTime() + ttlMs).toISOString(),
|
|
226
313
|
};
|
|
227
314
|
|
|
228
|
-
const { ref } = await projectLeaseRefs(opName, opts);
|
|
315
|
+
const { ref, trackingRef } = await projectLeaseRefs(opName, opts);
|
|
229
316
|
const blobSha = await writeBlob(JSON.stringify(record), opts);
|
|
230
317
|
try {
|
|
231
318
|
await updateRefCAS(ref, blobSha, sha, opts);
|
|
232
319
|
} catch (err) {
|
|
233
320
|
if (err instanceof RefCASConflictError) {
|
|
234
321
|
const retry = await readLease(opName, opts);
|
|
235
|
-
return { acquired: false, heldBy: retry.record };
|
|
322
|
+
return { acquired: false, heldBy: retry.record, reason: "race" };
|
|
236
323
|
}
|
|
237
324
|
// A StaleLockError (or any other non-CAS failure) is NOT "someone else
|
|
238
325
|
// has it" — propagate it as its own distinct error rather than folding
|
|
239
326
|
// it into `heldBy`, per this function's doc.
|
|
240
327
|
throw err;
|
|
241
328
|
}
|
|
242
|
-
await
|
|
329
|
+
const pushed = await pushRefStatus(ref, { ...opts, expect: remoteSha }).catch(() => "rejected" as const);
|
|
330
|
+
if (pushed === "pushed") {
|
|
331
|
+
// What the remote holds now; the next push expects it.
|
|
332
|
+
await updateRefCAS(trackingRef, blobSha, remoteSha, opts).catch(() => undefined);
|
|
333
|
+
} else if (pushed === "rejected" && opts?.requirePush) {
|
|
334
|
+
// Undo the local write, which the remote refused, and report who has it there.
|
|
335
|
+
await (sha === null ? deleteRefCAS(ref, blobSha, opts) : updateRefCAS(ref, sha, blobSha, opts)).catch(() => undefined);
|
|
336
|
+
const retry = await readLease(opName, opts);
|
|
337
|
+
const heldBy = retry.record && retry.record.token !== record.token ? retry.record : undefined;
|
|
338
|
+
return { acquired: false, reason: "push-rejected", ...(heldBy ? { heldBy } : {}) };
|
|
339
|
+
}
|
|
243
340
|
return { acquired: true, lease: record };
|
|
244
341
|
}
|
|
245
342
|
|
|
@@ -249,23 +346,30 @@ export async function acquireLease(
|
|
|
249
346
|
* silently drop someone else's. Best-effort courtesy: a lease nobody
|
|
250
347
|
* releases is reclaimed anyway once its TTL passes, so a failed release
|
|
251
348
|
* (returns `false`, never throws) is not itself a correctness problem.
|
|
349
|
+
*
|
|
350
|
+
* The deletion is pushed to the remote, expecting the value last fetched
|
|
351
|
+
* there, so another clone sees the lease free without waiting out its TTL
|
|
352
|
+
* (#2732).
|
|
252
353
|
*/
|
|
253
354
|
export async function releaseLease(
|
|
254
355
|
opName: string,
|
|
255
356
|
holder: string,
|
|
256
357
|
token: string,
|
|
257
|
-
opts?:
|
|
358
|
+
opts?: LeaseOptions,
|
|
258
359
|
): Promise<boolean> {
|
|
259
|
-
const { sha, record } = await readLease(opName, opts);
|
|
260
|
-
if (!
|
|
261
|
-
const { ref } = await projectLeaseRefs(opName, opts);
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
360
|
+
const { sha, record, remoteSha } = await readLease(opName, opts);
|
|
361
|
+
if (!record || record.holder !== holder || record.token !== token) return false;
|
|
362
|
+
const { ref, trackingRef } = await projectLeaseRefs(opName, opts);
|
|
363
|
+
if (sha) {
|
|
364
|
+
try {
|
|
365
|
+
await deleteRefCAS(ref, sha, opts);
|
|
366
|
+
} catch {
|
|
367
|
+
return false;
|
|
368
|
+
}
|
|
266
369
|
}
|
|
267
|
-
await
|
|
268
|
-
|
|
370
|
+
const pushed = await pushRefStatus(ref, { ...opts, expect: remoteSha }).catch(() => "rejected" as const);
|
|
371
|
+
if (pushed === "pushed" && remoteSha) await deleteRefCAS(trackingRef, remoteSha, opts).catch(() => undefined);
|
|
372
|
+
return sha !== null || pushed === "pushed";
|
|
269
373
|
}
|
|
270
374
|
|
|
271
375
|
/**
|
|
@@ -278,7 +382,7 @@ export async function stillHoldsLease(
|
|
|
278
382
|
opName: string,
|
|
279
383
|
holder: string,
|
|
280
384
|
token: string,
|
|
281
|
-
opts?:
|
|
385
|
+
opts?: LeaseOptions,
|
|
282
386
|
): Promise<boolean> {
|
|
283
387
|
const { record } = await readLease(opName, opts);
|
|
284
388
|
return record?.holder === holder && record?.token === token;
|
|
@@ -5,8 +5,9 @@
|
|
|
5
5
|
* A workspace member writes every lifecycle store under
|
|
6
6
|
* `_members/<member>/` on the one existing branch: releases, snapshots, runs,
|
|
7
7
|
* converge records and observation baselines under `<env>/`, gates under
|
|
8
|
-
* `_gates/`, build records under `_builds
|
|
9
|
-
*
|
|
8
|
+
* `_gates/`, build records under `_builds/`, release plans under `_plans/`
|
|
9
|
+
* (ws-055, #2733). Its operator leases move the same way, to
|
|
10
|
+
* `refs/chant/lease/_members/<member>/<op>`. Push, fetch, lease and
|
|
10
11
|
* staleness code are untouched: they still see one branch and plain refs.
|
|
11
12
|
*
|
|
12
13
|
* Everything else keeps today's flat layout, byte for byte:
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import { describe, test, expect } from "vitest";
|
|
2
|
+
import { withTestDir } from "@intentius/chant-test-utils";
|
|
3
|
+
import { spawnSync } from "node:child_process";
|
|
4
|
+
import { writeFileSync, mkdirSync } from "node:fs";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { persistReleasePlan, readReleasePlan, InvalidReleasePlanError, type ReleasePlan } from "./plan-ledger";
|
|
7
|
+
import { readRefSha } from "./git";
|
|
8
|
+
|
|
9
|
+
function git(args: string[], cwd: string): { stdout: string; exitCode: number } {
|
|
10
|
+
const r = spawnSync("git", args, { cwd, encoding: "utf-8" });
|
|
11
|
+
return { stdout: r.stdout ?? "", exitCode: r.status ?? -1 };
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async function initRepo(dir: string): Promise<void> {
|
|
15
|
+
git(["init", "-q", "-b", "main"], dir);
|
|
16
|
+
git(["config", "user.email", "test@chant.dev"], dir);
|
|
17
|
+
git(["config", "user.name", "Test"], dir);
|
|
18
|
+
writeFileSync(join(dir, "README.md"), "fixture\n");
|
|
19
|
+
git(["add", "README.md"], dir);
|
|
20
|
+
git(["commit", "-q", "-m", "init"], dir);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function makePlan(digest: string, extra: Record<string, unknown> = {}): ReleasePlan {
|
|
24
|
+
return { digest, release: "r-abc12345", units: [{ id: "w-1", contract: "c-1" }], evidence: [], ...extra };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
describe("lifecycle/plan-ledger", () => {
|
|
28
|
+
describe("persistReleasePlan / readReleasePlan", () => {
|
|
29
|
+
test("round-trips a plan by its own digest", async () => {
|
|
30
|
+
await withTestDir(async (dir) => {
|
|
31
|
+
await initRepo(dir);
|
|
32
|
+
const plan = makePlan("sha256:" + "1".repeat(64));
|
|
33
|
+
|
|
34
|
+
const { commit, written } = await persistReleasePlan(plan, { cwd: dir });
|
|
35
|
+
expect(written).toBe(true);
|
|
36
|
+
expect(commit).toMatch(/^[0-9a-f]{40}$/);
|
|
37
|
+
|
|
38
|
+
const readBack = await readReleasePlan(plan.digest, { cwd: dir });
|
|
39
|
+
expect(readBack).toEqual(plan);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("readReleasePlan returns null for an unknown digest", async () => {
|
|
44
|
+
await withTestDir(async (dir) => {
|
|
45
|
+
await initRepo(dir);
|
|
46
|
+
expect(await readReleasePlan("sha256:doesnotexist", { cwd: dir })).toBeNull();
|
|
47
|
+
});
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("a plan already written is never rewritten: no second commit, content unchanged", async () => {
|
|
51
|
+
await withTestDir(async (dir) => {
|
|
52
|
+
await initRepo(dir);
|
|
53
|
+
const plan = makePlan("sha256:" + "2".repeat(64));
|
|
54
|
+
|
|
55
|
+
const first = await persistReleasePlan(plan, { cwd: dir });
|
|
56
|
+
expect(first.written).toBe(true);
|
|
57
|
+
const tipAfterFirst = await readRefSha("refs/heads/chant/lifecycle", { cwd: dir });
|
|
58
|
+
|
|
59
|
+
const second = await persistReleasePlan(plan, { cwd: dir });
|
|
60
|
+
expect(second.written).toBe(false);
|
|
61
|
+
expect(second.commit).toBeNull();
|
|
62
|
+
const tipAfterSecond = await readRefSha("refs/heads/chant/lifecycle", { cwd: dir });
|
|
63
|
+
|
|
64
|
+
// Not a new commit — the branch tip is unchanged.
|
|
65
|
+
expect(tipAfterSecond).toBe(tipAfterFirst);
|
|
66
|
+
expect(await readReleasePlan(plan.digest, { cwd: dir })).toEqual(plan);
|
|
67
|
+
});
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("a plan with a different digest is a separate entry, coexisting with the first", async () => {
|
|
71
|
+
await withTestDir(async (dir) => {
|
|
72
|
+
await initRepo(dir);
|
|
73
|
+
const planA = makePlan("sha256:" + "a".repeat(64), { release: "r-a" });
|
|
74
|
+
const planB = makePlan("sha256:" + "b".repeat(64), { release: "r-b" });
|
|
75
|
+
|
|
76
|
+
await persistReleasePlan(planA, { cwd: dir });
|
|
77
|
+
await persistReleasePlan(planB, { cwd: dir });
|
|
78
|
+
|
|
79
|
+
expect(await readReleasePlan(planA.digest, { cwd: dir })).toEqual(planA);
|
|
80
|
+
expect(await readReleasePlan(planB.digest, { cwd: dir })).toEqual(planB);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("refuses a plan with no digest field", async () => {
|
|
85
|
+
await withTestDir(async (dir) => {
|
|
86
|
+
await initRepo(dir);
|
|
87
|
+
const plan = { release: "r-nodigest" } as unknown as ReleasePlan;
|
|
88
|
+
await expect(persistReleasePlan(plan, { cwd: dir })).rejects.toThrow(InvalidReleasePlanError);
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test("release ledger and plan store coexist on the same orphan branch", async () => {
|
|
93
|
+
const { appendReleaseRecord, readReleaseLedger } = await import("./release-ledger");
|
|
94
|
+
await withTestDir(async (dir) => {
|
|
95
|
+
await initRepo(dir);
|
|
96
|
+
const plan = makePlan("sha256:" + "3".repeat(64));
|
|
97
|
+
await persistReleasePlan(plan, { cwd: dir });
|
|
98
|
+
await appendReleaseRecord(
|
|
99
|
+
{
|
|
100
|
+
component: "svc",
|
|
101
|
+
env: "prod",
|
|
102
|
+
digest: plan.digest,
|
|
103
|
+
gitSha: "deadbeef",
|
|
104
|
+
runId: "run-1",
|
|
105
|
+
timestamp: "2026-01-01T00:00:00.000Z",
|
|
106
|
+
actor: "ci-bot",
|
|
107
|
+
},
|
|
108
|
+
{ cwd: dir },
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
expect(await readReleasePlan(plan.digest, { cwd: dir })).toEqual(plan);
|
|
112
|
+
const { records } = await readReleaseLedger("prod", { cwd: dir });
|
|
113
|
+
expect(records).toHaveLength(1);
|
|
114
|
+
expect(records[0].digest).toBe(plan.digest);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("a member's plan lives under _members/<member>/_plans/ (#2524 D7), reached via the prefix readReleasePlan takes", async () => {
|
|
119
|
+
await withTestDir(async (dir) => {
|
|
120
|
+
await initRepo(dir);
|
|
121
|
+
mkdirSync(join(dir, "apps", "web"), { recursive: true });
|
|
122
|
+
writeFileSync(join(dir, "chant.workspace.json"), JSON.stringify({
|
|
123
|
+
name: "acme",
|
|
124
|
+
schema: 1,
|
|
125
|
+
members: [{ name: "web", dir: "apps/web", kind: "chant" }],
|
|
126
|
+
}));
|
|
127
|
+
writeFileSync(join(dir, "apps", "web", "chant.config.ts"), "");
|
|
128
|
+
git(["add", "-A"], dir);
|
|
129
|
+
git(["commit", "-q", "-m", "workspace"], dir);
|
|
130
|
+
|
|
131
|
+
const plan = makePlan("sha256:" + "4".repeat(64));
|
|
132
|
+
// Persisted from inside the member's own directory, the way `chant
|
|
133
|
+
// components release` runs there — writeBlobToPath resolves the
|
|
134
|
+
// _members/web/ prefix automatically from cwd.
|
|
135
|
+
const webDir = join(dir, "apps", "web");
|
|
136
|
+
await persistReleasePlan(plan, { cwd: webDir });
|
|
137
|
+
|
|
138
|
+
// readReleasePlan reads it back two ways: with the matching prefix
|
|
139
|
+
// (as ../workspace/status.ts resolves it for a "members"-layout
|
|
140
|
+
// release), and directly at the branch path it actually landed on.
|
|
141
|
+
expect(await readReleasePlan(plan.digest, { cwd: dir, prefix: "_members/web/" })).toEqual(plan);
|
|
142
|
+
const raw = git(["show", `chant/lifecycle:_members/web/_plans/sha256_${"4".repeat(64)}.json`], dir);
|
|
143
|
+
expect(raw.exitCode).toBe(0);
|
|
144
|
+
expect(JSON.parse(raw.stdout)).toEqual(plan);
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
});
|