@intentius/chant 0.102.0 → 0.104.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/core.d.ts +2 -0
- package/dist/audit/core.d.ts.map +1 -1
- package/dist/audit/discover.d.ts.map +1 -1
- package/dist/change-set.d.ts +232 -0
- package/dist/change-set.d.ts.map +1 -0
- package/dist/cli/commands/audit.d.ts +7 -0
- package/dist/cli/commands/audit.d.ts.map +1 -1
- package/dist/cli/commands/build.d.ts.map +1 -1
- package/dist/cli/handlers/build.d.ts.map +1 -1
- package/dist/cli/handlers/change-set.d.ts +11 -0
- package/dist/cli/handlers/change-set.d.ts.map +1 -0
- package/dist/cli/handlers/fan-out.d.ts +33 -0
- package/dist/cli/handlers/fan-out.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/pr.d.ts +31 -0
- package/dist/cli/handlers/pr.d.ts.map +1 -0
- package/dist/cli/handlers/run.d.ts.map +1 -1
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/mcp/op-tools.d.ts.map +1 -1
- package/dist/cli/mcp/workspace-tools.d.ts +3 -2
- package/dist/cli/mcp/workspace-tools.d.ts.map +1 -1
- package/dist/cli/registry.d.ts +63 -1
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/components/auto-release.d.ts +16 -0
- package/dist/components/auto-release.d.ts.map +1 -1
- package/dist/components/capability.d.ts +53 -0
- package/dist/components/capability.d.ts.map +1 -1
- package/dist/components/changed-files.d.ts +31 -0
- package/dist/components/changed-files.d.ts.map +1 -0
- package/dist/components/cli-support.d.ts +9 -1
- package/dist/components/cli-support.d.ts.map +1 -1
- package/dist/components/component.d.ts +24 -0
- package/dist/components/component.d.ts.map +1 -1
- package/dist/components/deploy-units.d.ts.map +1 -1
- package/dist/components/driver.d.ts +13 -0
- package/dist/components/driver.d.ts.map +1 -1
- package/dist/components/fan-out-output.d.ts +6 -0
- package/dist/components/fan-out-output.d.ts.map +1 -1
- package/dist/components/fan-out-record.d.ts +41 -0
- package/dist/components/fan-out-record.d.ts.map +1 -0
- package/dist/components/fan-out-run.d.ts +65 -0
- package/dist/components/fan-out-run.d.ts.map +1 -1
- package/dist/components/fan-out-support.d.ts +2 -0
- package/dist/components/fan-out-support.d.ts.map +1 -1
- package/dist/components/fan-out.d.ts +6 -0
- package/dist/components/fan-out.d.ts.map +1 -1
- package/dist/components/gated-wave-pipeline.d.ts +50 -0
- package/dist/components/gated-wave-pipeline.d.ts.map +1 -0
- package/dist/components/index.d.ts +1 -1
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/layers.d.ts +25 -0
- package/dist/components/layers.d.ts.map +1 -0
- package/dist/components/pr-pipeline.d.ts +61 -0
- package/dist/components/pr-pipeline.d.ts.map +1 -0
- package/dist/components/pr-run.d.ts +119 -0
- package/dist/components/pr-run.d.ts.map +1 -0
- package/dist/components/promote.d.ts.map +1 -1
- package/dist/components/verbs/shell.d.ts.map +1 -1
- package/dist/components/wave-plan.d.ts +49 -0
- package/dist/components/wave-plan.d.ts.map +1 -0
- package/dist/gated-waves.d.ts +147 -0
- package/dist/gated-waves.d.ts.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/lexicon.d.ts +48 -0
- package/dist/lexicon.d.ts.map +1 -1
- package/dist/lifecycle/gate-ledger.d.ts +7 -0
- package/dist/lifecycle/gate-ledger.d.ts.map +1 -1
- package/dist/lifecycle/git.d.ts +46 -1
- package/dist/lifecycle/git.d.ts.map +1 -1
- package/dist/lint/post-synth.d.ts +18 -0
- package/dist/lint/post-synth.d.ts.map +1 -1
- package/dist/op/activities/activity-contracts.d.ts +126 -0
- package/dist/op/activities/activity-contracts.d.ts.map +1 -1
- package/dist/op/activities/change-set.d.ts +62 -0
- package/dist/op/activities/change-set.d.ts.map +1 -0
- package/dist/op/activities/decide.d.ts +2 -0
- package/dist/op/activities/decide.d.ts.map +1 -1
- package/dist/op/activities/factory.d.ts +137 -0
- package/dist/op/activities/factory.d.ts.map +1 -0
- package/dist/op/activities/index.d.ts +4 -0
- package/dist/op/activities/index.d.ts.map +1 -1
- package/dist/op/activity-contract.d.ts.map +1 -1
- package/dist/op/builders.d.ts +11 -0
- package/dist/op/builders.d.ts.map +1 -1
- package/dist/op/decide-backend.d.ts +5 -1
- package/dist/op/decide-backend.d.ts.map +1 -1
- package/dist/op/decide-read-inputs.d.ts +4 -2
- package/dist/op/decide-read-inputs.d.ts.map +1 -1
- package/dist/op/factory.d.ts +83 -0
- package/dist/op/factory.d.ts.map +1 -0
- package/dist/op/index.d.ts +4 -1
- package/dist/op/index.d.ts.map +1 -1
- package/dist/op/op-ir.d.ts +2 -0
- package/dist/op/op-ir.d.ts.map +1 -1
- package/dist/op/op-verb-class.d.ts.map +1 -1
- package/dist/op/types.d.ts +8 -0
- package/dist/op/types.d.ts.map +1 -1
- package/dist/op/work-lease-run.d.ts.map +1 -1
- package/dist/plan-summary.d.ts +207 -0
- package/dist/plan-summary.d.ts.map +1 -0
- package/dist/pr-forge.d.ts +105 -0
- package/dist/pr-forge.d.ts.map +1 -0
- package/dist/pr-loop.d.ts +178 -0
- package/dist/pr-loop.d.ts.map +1 -0
- package/dist/telemetry-attribution.d.ts +31 -0
- package/dist/telemetry-attribution.d.ts.map +1 -1
- package/dist/terraform/hcl2json.d.ts +45 -0
- package/dist/terraform/hcl2json.d.ts.map +1 -0
- package/dist/terraform/parse.d.ts +4 -40
- package/dist/terraform/parse.d.ts.map +1 -1
- package/dist/workspace/box-cli.d.ts +12 -0
- package/dist/workspace/box-cli.d.ts.map +1 -0
- package/dist/workspace/box-factory.d.ts +27 -3
- package/dist/workspace/box-factory.d.ts.map +1 -1
- package/dist/workspace/box-listing.d.ts +85 -0
- package/dist/workspace/box-listing.d.ts.map +1 -0
- package/dist/workspace/box-publish.d.ts +147 -0
- package/dist/workspace/box-publish.d.ts.map +1 -0
- package/dist/workspace/broker-protocol.d.ts +190 -0
- package/dist/workspace/broker-protocol.d.ts.map +1 -0
- package/dist/workspace/checks.d.ts.map +1 -1
- package/dist/workspace/composites.d.ts +2 -2
- package/dist/workspace/composites.d.ts.map +1 -1
- package/dist/workspace/conformance/__fixtures__/reference-broker.d.ts +17 -0
- package/dist/workspace/conformance/__fixtures__/reference-broker.d.ts.map +1 -0
- package/dist/workspace/conformance/broker.d.ts +202 -0
- package/dist/workspace/conformance/broker.d.ts.map +1 -0
- package/dist/workspace/conformance/index.d.ts +2 -1
- package/dist/workspace/conformance/index.d.ts.map +1 -1
- package/dist/workspace/conformance/vitest.d.ts +9 -2
- package/dist/workspace/conformance/vitest.d.ts.map +1 -1
- package/dist/workspace/conformance/writer.d.ts +108 -14
- package/dist/workspace/conformance/writer.d.ts.map +1 -1
- package/dist/workspace/decide.d.ts +126 -12
- package/dist/workspace/decide.d.ts.map +1 -1
- package/dist/workspace/decision-points.schema.json +11 -4
- package/dist/workspace/declaration.d.ts +65 -0
- package/dist/workspace/declaration.d.ts.map +1 -1
- package/dist/workspace/declaration.schema.json +153 -4
- package/dist/workspace/environments.d.ts +36 -10
- package/dist/workspace/environments.d.ts.map +1 -1
- package/dist/workspace/factory-rules.d.ts +180 -0
- package/dist/workspace/factory-rules.d.ts.map +1 -0
- package/dist/workspace/init.d.ts +21 -0
- package/dist/workspace/init.d.ts.map +1 -1
- package/dist/workspace/intent-cli.d.ts +1 -1
- package/dist/workspace/intent-cli.d.ts.map +1 -1
- package/dist/workspace/intent-record.d.ts +28 -1
- package/dist/workspace/intent-record.d.ts.map +1 -1
- package/dist/workspace/intent.d.ts +102 -5
- package/dist/workspace/intent.d.ts.map +1 -1
- package/dist/workspace/json-edit.d.ts +24 -0
- package/dist/workspace/json-edit.d.ts.map +1 -0
- package/dist/workspace/jsonc.d.ts +11 -0
- package/dist/workspace/jsonc.d.ts.map +1 -1
- package/dist/workspace/kinds.d.ts +48 -0
- package/dist/workspace/kinds.d.ts.map +1 -1
- package/dist/workspace/lexicon-symbols.d.ts +29 -0
- package/dist/workspace/lexicon-symbols.d.ts.map +1 -0
- package/dist/workspace/lineage-upgrade.d.ts +31 -2
- package/dist/workspace/lineage-upgrade.d.ts.map +1 -1
- package/dist/workspace/point-answer.schema.json +42 -2
- package/dist/workspace/points-cli.d.ts +10 -3
- package/dist/workspace/points-cli.d.ts.map +1 -1
- package/dist/workspace/points.d.ts +66 -5
- package/dist/workspace/points.d.ts.map +1 -1
- package/dist/workspace/reason-codes.d.ts +33 -4
- package/dist/workspace/reason-codes.d.ts.map +1 -1
- package/dist/workspace/record-sessions.d.ts +5 -7
- package/dist/workspace/record-sessions.d.ts.map +1 -1
- package/dist/workspace/records-cli.d.ts +7 -0
- package/dist/workspace/records-cli.d.ts.map +1 -1
- package/dist/workspace/records-close.d.ts +7 -3
- package/dist/workspace/records-close.d.ts.map +1 -1
- package/dist/workspace/records-write.d.ts +66 -6
- package/dist/workspace/records-write.d.ts.map +1 -1
- package/dist/workspace/records.d.ts +28 -9
- package/dist/workspace/records.d.ts.map +1 -1
- package/dist/workspace/runs-cli.d.ts +138 -6
- package/dist/workspace/runs-cli.d.ts.map +1 -1
- package/dist/workspace/runs.d.ts +132 -11
- package/dist/workspace/runs.d.ts.map +1 -1
- package/dist/workspace/session-kinds.d.ts +1 -1
- package/dist/workspace/session-kinds.d.ts.map +1 -1
- package/dist/workspace/squash.d.ts +80 -0
- package/dist/workspace/squash.d.ts.map +1 -0
- package/dist/workspace/status-gates.d.ts +2 -0
- package/dist/workspace/status-gates.d.ts.map +1 -1
- package/dist/workspace/status.d.ts +54 -0
- package/dist/workspace/status.d.ts.map +1 -1
- package/dist/workspace/symbols.d.ts +16 -5
- package/dist/workspace/symbols.d.ts.map +1 -1
- package/dist/workspace/trust/run-commits.d.ts +46 -0
- package/dist/workspace/trust/run-commits.d.ts.map +1 -0
- package/dist/workspace/trust/run-statement.d.ts +128 -0
- package/dist/workspace/trust/run-statement.d.ts.map +1 -0
- package/dist/workspace/trust/seal.d.ts +6 -1
- package/dist/workspace/trust/seal.d.ts.map +1 -1
- package/dist/workspace/trust/verify-cli.d.ts +1 -1
- package/dist/workspace/trust/verify.d.ts +16 -2
- package/dist/workspace/trust/verify.d.ts.map +1 -1
- package/dist/workspace/wip-cli.d.ts +21 -0
- package/dist/workspace/wip-cli.d.ts.map +1 -0
- package/dist/workspace/wip.d.ts +268 -0
- package/dist/workspace/wip.d.ts.map +1 -0
- package/dist/workspace/work-cli.d.ts.map +1 -1
- package/dist/workspace/work-evidence.d.ts +1 -1
- package/dist/workspace/work-evidence.d.ts.map +1 -1
- package/dist/workspace/work-size.d.ts +55 -0
- package/dist/workspace/work-size.d.ts.map +1 -0
- package/dist/workspace/workspace-kinds.schema.json +122 -0
- package/dist/workspace/write-lock-cli.d.ts +60 -0
- package/dist/workspace/write-lock-cli.d.ts.map +1 -0
- package/dist/workspace/write-lock.d.ts +152 -0
- package/dist/workspace/write-lock.d.ts.map +1 -0
- package/dist/workspace/write-scope.d.ts +6 -2
- package/dist/workspace/write-scope.d.ts.map +1 -1
- package/package.json +6 -1
- package/src/__fixtures__/change-set/lifecycle-plan.floci.json +59 -0
- package/src/__fixtures__/change-set/lifecycle-v1.ts.txt +4 -0
- package/src/__fixtures__/change-set/lifecycle-v2.ts.txt +4 -0
- package/src/__fixtures__/change-set/lifecycle.change-set.golden.json +95 -0
- package/src/__fixtures__/change-set/warden.change-set.golden.json +106 -0
- package/src/__fixtures__/change-set/warden.plan.json +44 -0
- package/src/__fixtures__/plan-summary/normalization-vectors.json +1593 -0
- package/src/__fixtures__/pr-loop/pr-apply-applied.note.golden.md +31 -0
- package/src/__fixtures__/pr-loop/pr-apply-refused.note.golden.md +31 -0
- package/src/__fixtures__/pr-loop/pr-plan.golden.json +127 -0
- package/src/__fixtures__/pr-loop/pr-plan.note.golden.md +37 -0
- package/src/audit/core.test.ts +15 -0
- package/src/audit/core.ts +8 -2
- package/src/audit/discover.test.ts +11 -0
- package/src/audit/discover.ts +7 -2
- package/src/change-set-bundle.test.ts +57 -0
- package/src/change-set.test.ts +215 -0
- package/src/change-set.ts +445 -0
- package/src/cli/commands/audit-opt-in.test.ts +54 -0
- package/src/cli/commands/audit.ts +29 -2
- package/src/cli/commands/build.test.ts +19 -0
- package/src/cli/commands/build.ts +3 -2
- package/src/cli/handlers/build.ts +13 -1
- package/src/cli/handlers/change-set.test.ts +56 -0
- package/src/cli/handlers/change-set.ts +44 -0
- package/src/cli/handlers/fan-out.test.ts +221 -2
- package/src/cli/handlers/fan-out.ts +165 -75
- package/src/cli/handlers/graph.ts +3 -0
- package/src/cli/handlers/operator.test.ts +21 -0
- package/src/cli/handlers/operator.ts +34 -3
- package/src/cli/handlers/pr.ts +373 -0
- package/src/cli/handlers/run.ts +1 -0
- package/src/cli/main.test.ts +20 -0
- package/src/cli/main.ts +185 -23
- package/src/cli/mcp/never-over-mcp.test.ts +101 -0
- package/src/cli/mcp/op-tools.ts +24 -2
- package/src/cli/mcp/workspace-tools.test.ts +1 -1
- package/src/cli/mcp/workspace-tools.ts +34 -6
- package/src/cli/registry.ts +63 -1
- package/src/components/auto-release.ts +1 -1
- package/src/components/capability.ts +55 -0
- package/src/components/changed-files.ts +66 -0
- package/src/components/cli-support.ts +28 -5
- package/src/components/component.schema.json +17 -0
- package/src/components/component.ts +25 -0
- package/src/components/deploy-units.ts +3 -0
- package/src/components/driver.test.ts +52 -0
- package/src/components/driver.ts +31 -13
- package/src/components/fan-out-output.ts +42 -1
- package/src/components/fan-out-record.ts +69 -0
- package/src/components/fan-out-run.test.ts +17 -0
- package/src/components/fan-out-run.ts +304 -1
- package/src/components/fan-out-support.ts +3 -0
- package/src/components/fan-out-waves.test.ts +375 -0
- package/src/components/fan-out.ts +18 -17
- package/src/components/gated-wave-pipeline.ts +82 -0
- package/src/components/index.ts +1 -0
- package/src/components/layers.ts +30 -0
- package/src/components/pr-pipeline.ts +91 -0
- package/src/components/pr-run.test.ts +205 -0
- package/src/components/pr-run.ts +280 -0
- package/src/components/promote.ts +5 -0
- package/src/components/verbs/shell.test.ts +11 -0
- package/src/components/verbs/shell.ts +7 -3
- package/src/components/wave-plan.ts +124 -0
- package/src/discovery/sandbox/bundle.ts +1 -1
- package/src/gated-waves-bundle.test.ts +62 -0
- package/src/gated-waves.test.ts +118 -0
- package/src/gated-waves.ts +240 -0
- package/src/index.ts +1 -0
- package/src/lexicon.ts +51 -0
- package/src/lifecycle/gate-ledger.ts +7 -0
- package/src/lifecycle/git.ts +102 -16
- package/src/lint/post-synth.test.ts +19 -0
- package/src/lint/post-synth.ts +19 -1
- package/src/meta/declared-imports.test.ts +1 -1
- package/src/op/activities/activity-contracts.ts +84 -0
- package/src/op/activities/change-set.ts +114 -0
- package/src/op/activities/decide.ts +3 -0
- package/src/op/activities/factory.ts +675 -0
- package/src/op/activities/index.ts +6 -0
- package/src/op/activity-contract.test.ts +21 -1
- package/src/op/activity-contract.ts +19 -9
- package/src/op/builders.ts +23 -0
- package/src/op/decide-backend.ts +5 -1
- package/src/op/decide-read-inputs.ts +33 -5
- package/src/op/factory.e2e.test.ts +232 -0
- package/src/op/factory.ts +118 -0
- package/src/op/index.ts +9 -1
- package/src/op/op-ir.test.ts +9 -0
- package/src/op/op-ir.ts +4 -0
- package/src/op/op-verb-class.ts +3 -0
- package/src/op/types.ts +8 -0
- package/src/op/work-lease-run.ts +3 -0
- package/src/plan-summary.test.ts +263 -0
- package/src/plan-summary.ts +845 -0
- package/src/pr-forge.test.ts +237 -0
- package/src/pr-forge.ts +332 -0
- package/src/pr-loop-bundle.test.ts +46 -0
- package/src/pr-loop.test.ts +171 -0
- package/src/pr-loop.ts +324 -0
- package/src/telemetry-attribution.test.ts +23 -1
- package/src/telemetry-attribution.ts +51 -0
- package/src/terraform/hcl2json.test.ts +21 -0
- package/src/terraform/hcl2json.ts +81 -0
- package/src/terraform/parse.ts +11 -74
- package/src/workspace/__fixtures__/studio-template-84c8b21.json +1 -1
- package/src/workspace/app-kind.test.ts +196 -0
- package/src/workspace/box-cli.ts +113 -0
- package/src/workspace/box-factory.test.ts +4 -1
- package/src/workspace/box-factory.ts +37 -4
- package/src/workspace/box-listing-write.schema.json +204 -0
- package/src/workspace/box-listing.test.ts +234 -0
- package/src/workspace/box-listing.ts +324 -0
- package/src/workspace/box-publish.schema.json +498 -0
- package/src/workspace/box-publish.test.ts +204 -0
- package/src/workspace/box-publish.ts +420 -0
- package/src/workspace/box-ship.test.ts +96 -0
- package/src/workspace/broker-protocol.schema.json +186 -0
- package/src/workspace/broker-protocol.test.ts +122 -0
- package/src/workspace/broker-protocol.ts +317 -0
- package/src/workspace/builder-tiers.test.ts +146 -0
- package/src/workspace/change-set.schema.json +460 -0
- package/src/workspace/chant-migrations.e2e.test.ts +17 -5
- package/src/workspace/checks/boxes.test.ts +3 -0
- package/src/workspace/checks.ts +10 -3
- package/src/workspace/component-environments.test.ts +105 -0
- package/src/workspace/composites.schema.json +23 -4
- package/src/workspace/composites.test.ts +8 -8
- package/src/workspace/composites.ts +13 -2
- package/src/workspace/conformance/__fixture__/decisions/decision.schema.json +2 -2
- package/src/workspace/conformance/__fixtures__/reference-broker.ts +207 -0
- package/src/workspace/conformance/__writer_fixture__/answers/answer.kind.mjs +7 -2
- package/src/workspace/conformance/__writer_fixture__/answers/answer.schema.json +42 -2
- package/src/workspace/conformance/__writer_fixture__/contracts/contract.kind.mjs +37 -0
- package/src/workspace/conformance/__writer_fixture__/contracts/contract.schema.json +213 -0
- package/src/workspace/conformance/__writer_fixture__/decisions/points.json +96 -0
- package/src/workspace/conformance/__writer_fixture__/drivers/driver.kind.mjs +15 -0
- package/src/workspace/conformance/__writer_fixture__/drivers/driver.schema.json +38 -0
- package/src/workspace/conformance/__writer_fixture__/sessions/session.kind.mjs +14 -8
- package/src/workspace/conformance/__writer_fixture__/sessions/session.schema.json +187 -6
- package/src/workspace/conformance/__writer_fixture__/work/work.schema.json +25 -0
- package/src/workspace/conformance/broker.test.ts +122 -0
- package/src/workspace/conformance/broker.ts +792 -0
- package/src/workspace/conformance/conformance.e2e.test.ts +2 -1
- package/src/workspace/conformance/index.mjs +22 -1
- package/src/workspace/conformance/index.ts +5 -1
- package/src/workspace/conformance/vitest.ts +33 -1
- package/src/workspace/conformance/writer-conformance.e2e.test.ts +49 -3
- package/src/workspace/conformance/writer.test.ts +32 -2
- package/src/workspace/conformance/writer.ts +339 -28
- package/src/workspace/decide.test.ts +208 -4
- package/src/workspace/decide.ts +427 -73
- package/src/workspace/decision-points.schema.json +11 -4
- package/src/workspace/declaration.schema.json +153 -4
- package/src/workspace/declaration.ts +141 -0
- package/src/workspace/environments.ts +66 -14
- package/src/workspace/factory-rules.test.ts +62 -0
- package/src/workspace/factory-rules.ts +218 -0
- package/src/workspace/graph-cli.ts +2 -2
- package/src/workspace/graph.schema.json +67 -0
- package/src/workspace/identity.test.ts +23 -0
- package/src/workspace/init.ts +82 -2
- package/src/workspace/intent-cli.ts +26 -9
- package/src/workspace/intent-patch-id.test.ts +162 -0
- package/src/workspace/intent-record.schema.json +130 -3
- package/src/workspace/intent-record.ts +74 -28
- package/src/workspace/intent-reviews.test.ts +76 -0
- package/src/workspace/intent-squash.test.ts +213 -0
- package/src/workspace/intent-why.test.ts +5 -5
- package/src/workspace/intent.schema.json +234 -4
- package/src/workspace/intent.ts +260 -66
- package/src/workspace/json-edit.test.ts +121 -0
- package/src/workspace/json-edit.ts +142 -0
- package/src/workspace/jsonc.ts +20 -0
- package/src/workspace/kinds.ts +157 -2
- package/src/workspace/lexicon-symbols.test.ts +148 -0
- package/src/workspace/lexicon-symbols.ts +125 -0
- package/src/workspace/lineage-upgrade.test.ts +63 -0
- package/src/workspace/lineage-upgrade.ts +146 -5
- package/src/workspace/plan-summary.schema.json +155 -0
- package/src/workspace/point-answer.schema.json +42 -2
- package/src/workspace/points-cli.ts +42 -16
- package/src/workspace/points-write.schema.json +162 -4
- package/src/workspace/points.schema.json +157 -2
- package/src/workspace/points.test.ts +13 -1
- package/src/workspace/points.ts +118 -12
- package/src/workspace/pr-report.schema.json +95 -0
- package/src/workspace/profiles.test.ts +159 -0
- package/src/workspace/read-contract.test.ts +2 -2
- package/src/workspace/reason-codes.test.ts +13 -1
- package/src/workspace/reason-codes.ts +38 -4
- package/src/workspace/record-sessions.ts +9 -8
- package/src/workspace/records-amend.schema.json +78 -0
- package/src/workspace/records-cli.ts +20 -6
- package/src/workspace/records-close.schema.json +78 -0
- package/src/workspace/records-close.ts +31 -7
- package/src/workspace/records-concurrent.e2e.test.ts +135 -0
- package/src/workspace/records-concurrent.test.ts +145 -0
- package/src/workspace/records-formats.test.ts +46 -10
- package/src/workspace/records-new.schema.json +7 -0
- package/src/workspace/records-review.schema.json +78 -0
- package/src/workspace/records-sessions-write.test.ts +69 -0
- package/src/workspace/records-sessions.test.ts +22 -0
- package/src/workspace/records-write.ts +192 -25
- package/src/workspace/records.schema.json +16 -0
- package/src/workspace/records.ts +50 -9
- package/src/workspace/reference-kinds/app/package.json +10 -0
- package/src/workspace/reference-kinds/app/workspace-kinds.json +45 -0
- package/src/workspace/run-statement.schema.json +622 -0
- package/src/workspace/runs-cli.ts +343 -14
- package/src/workspace/runs-statement.test.ts +239 -0
- package/src/workspace/runs-write.schema.json +416 -5
- package/src/workspace/runs.schema.json +451 -4
- package/src/workspace/runs.test.ts +39 -0
- package/src/workspace/runs.ts +296 -35
- package/src/workspace/session-kinds.ts +3 -3
- package/src/workspace/squash.ts +198 -0
- package/src/workspace/status-contract.test.ts +5 -2
- package/src/workspace/status-gates.ts +3 -1
- package/src/workspace/status.schema.json +126 -3
- package/src/workspace/status.ts +90 -1
- package/src/workspace/symbols.ts +20 -9
- package/src/workspace/trust/run-commits.ts +115 -0
- package/src/workspace/trust/run-statement.ts +226 -0
- package/src/workspace/trust/seal.ts +8 -2
- package/src/workspace/trust/verify-cli.ts +6 -5
- package/src/workspace/trust/verify.ts +39 -6
- package/src/workspace/wip-cli.ts +97 -0
- package/src/workspace/wip-write.schema.json +504 -0
- package/src/workspace/wip.schema.json +334 -0
- package/src/workspace/wip.test.ts +294 -0
- package/src/workspace/wip.ts +753 -0
- package/src/workspace/work-cli.ts +3 -0
- package/src/workspace/work-evidence.schema.json +3 -0
- package/src/workspace/work-size.test.ts +77 -0
- package/src/workspace/work-size.ts +93 -0
- package/src/workspace/workspace-kinds.schema.json +122 -0
- package/src/workspace/write-lock-cli.ts +131 -0
- package/src/workspace/write-lock.schema.json +172 -0
- package/src/workspace/write-lock.test.ts +189 -0
- package/src/workspace/write-lock.ts +474 -0
- package/src/workspace/write-scope.test.ts +16 -1
- package/src/workspace/write-scope.ts +33 -11
package/src/components/driver.ts
CHANGED
|
@@ -47,11 +47,14 @@
|
|
|
47
47
|
*/
|
|
48
48
|
|
|
49
49
|
import { topoSort } from "../codegen/topo-sort";
|
|
50
|
+
import { kahnLayers } from "./layers";
|
|
50
51
|
import { evaluateGate, gitGateLedgerPort, type GateDigestMismatch, type GateLedgerPort } from "../op/gate";
|
|
51
52
|
import { gateName } from "../op/gate-name";
|
|
52
53
|
import { componentPlanDigest } from "./gate-plan";
|
|
53
54
|
import type { PendingGateRecord } from "../lifecycle/gate-ledger";
|
|
54
55
|
import type { CapabilityRegistry, DeployContext } from "./capability";
|
|
56
|
+
import { isPromotedArtifact } from "./auto-release";
|
|
57
|
+
import type { ReleaseIdentity } from "../telemetry-attribution";
|
|
55
58
|
import type { RunProgressEvent, RunProgressStatus } from "./run-progress";
|
|
56
59
|
|
|
57
60
|
export type { RunProgressEvent } from "./run-progress";
|
|
@@ -237,18 +240,9 @@ export function resolveComponentGraph(components: DriverComponent[]): ComponentG
|
|
|
237
240
|
const deps = new Map<string, Set<string>>();
|
|
238
241
|
for (const c of components) deps.set(c.name, new Set(c.dependsOn ?? []));
|
|
239
242
|
|
|
240
|
-
const
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
const wave = [...remaining]
|
|
244
|
-
.filter((n) => [...deps.get(n)!].every((d) => !remaining.has(d)))
|
|
245
|
-
.sort();
|
|
246
|
-
if (wave.length === 0) {
|
|
247
|
-
throw new DependencyCycleError([...remaining].sort());
|
|
248
|
-
}
|
|
249
|
-
for (const n of wave) remaining.delete(n);
|
|
250
|
-
waves.push(wave);
|
|
251
|
-
}
|
|
243
|
+
const layered = kahnLayers(deps);
|
|
244
|
+
if (layered.cycle) throw new DependencyCycleError(layered.cycle);
|
|
245
|
+
const waves = layered.waves;
|
|
252
246
|
|
|
253
247
|
const order = topoSort(
|
|
254
248
|
components,
|
|
@@ -566,6 +560,11 @@ async function runPhase(
|
|
|
566
560
|
});
|
|
567
561
|
if (record.status === "ok") {
|
|
568
562
|
phaseOutputs[phaseDef.phase] = { ...(phaseOutputs[phaseDef.phase] ?? {}), ...(output as object) };
|
|
563
|
+
// The release's service.version is the digest a later deploy step stamps (#3061): the
|
|
564
|
+
// last artifact this component promoted, the same one auto-release records.
|
|
565
|
+
if (ctx.release && isPromotedArtifact(output) && typeof output.digest === "string" && output.digest !== "") {
|
|
566
|
+
ctx.release.version = output.digest;
|
|
567
|
+
}
|
|
569
568
|
}
|
|
570
569
|
return {
|
|
571
570
|
records: [record],
|
|
@@ -715,11 +714,15 @@ export async function runComponentDeploy(
|
|
|
715
714
|
onProgress?: (event: RunProgressEvent) => void,
|
|
716
715
|
gateContext: GateContext = { port: gitGateLedgerPort() },
|
|
717
716
|
): Promise<DriverComponentResult> {
|
|
717
|
+
// Each component tracks its own release: a digest one component promoted never stamps another's deploy.
|
|
718
|
+
if (ctx.release) ctx = { ...ctx, release: { ...ctx.release } };
|
|
718
719
|
const phaseOutputs: Record<string, Record<string, unknown>> = {};
|
|
719
720
|
// #2574: every gate this component reaches binds its approval to this
|
|
720
721
|
// environment and this composition, so an approval for staging or for last
|
|
721
722
|
// week's deploy cannot pass it.
|
|
722
723
|
const release = gateContext.releases?.get(component.name);
|
|
724
|
+
// A promote or rollback deploys a recorded digest without publishing one (#2574); that digest is the release's version.
|
|
725
|
+
if (ctx.release && release !== undefined && ctx.release.version === undefined) ctx.release.version = release;
|
|
723
726
|
const gates: GateContext = {
|
|
724
727
|
...gateContext,
|
|
725
728
|
planDigest: componentPlanDigest({
|
|
@@ -909,6 +912,12 @@ export interface InterpretRunOptions {
|
|
|
909
912
|
now?: string;
|
|
910
913
|
/** The recorded digest a promote or rollback deploys, per component (#2574). Part of the plan a gate approval is bound to. */
|
|
911
914
|
releases?: ReadonlyMap<string, string>;
|
|
915
|
+
/**
|
|
916
|
+
* The release each component deploys (#3061, ws-081), threaded into its
|
|
917
|
+
* `DeployContext.release`. The driver fills in `version` when a publish
|
|
918
|
+
* step promotes an artifact. Absent: no deploy step sees a release.
|
|
919
|
+
*/
|
|
920
|
+
releaseIdentity?: (component: string) => ReleaseIdentity | undefined;
|
|
912
921
|
}
|
|
913
922
|
|
|
914
923
|
/**
|
|
@@ -930,6 +939,15 @@ export interface InterpretRunOptions {
|
|
|
930
939
|
* purely on the generic `Component`/`Phase`/`Step` shapes and the registry —
|
|
931
940
|
* no branch anywhere names a specific component or capability.
|
|
932
941
|
*/
|
|
942
|
+
/** A component's `DeployContext` for a run: its env, vars and, when the caller knows it, its release. */
|
|
943
|
+
export function deployContext(
|
|
944
|
+
options: { env: string; vars?: Record<string, unknown>; releaseIdentity?: (component: string) => ReleaseIdentity | undefined },
|
|
945
|
+
component: string,
|
|
946
|
+
): DeployContext {
|
|
947
|
+
const release = options.releaseIdentity?.(component);
|
|
948
|
+
return { env: options.env, component, ...(options.vars ? { vars: options.vars } : {}), ...(release ? { release: { ...release } } : {}) };
|
|
949
|
+
}
|
|
950
|
+
|
|
933
951
|
export async function runInterpretDriver(
|
|
934
952
|
components: DriverComponent[],
|
|
935
953
|
registry: CapabilityRegistry,
|
|
@@ -960,7 +978,7 @@ export async function runInterpretDriver(
|
|
|
960
978
|
onProgress?.({ type: "component-start", wave: waveNum, component: component.name });
|
|
961
979
|
const result = await runComponentDeploy(
|
|
962
980
|
component,
|
|
963
|
-
|
|
981
|
+
deployContext(options, component.name),
|
|
964
982
|
registry,
|
|
965
983
|
componentOutputs,
|
|
966
984
|
onProgress,
|
|
@@ -31,6 +31,7 @@
|
|
|
31
31
|
|
|
32
32
|
import type { FanOutPlan, FanOutSkip } from "./fan-out";
|
|
33
33
|
import type { FanOutRunResult } from "./fan-out-run";
|
|
34
|
+
import { waveGateName, type WaveRecord } from "../gated-waves";
|
|
34
35
|
|
|
35
36
|
type Writer = (line: string) => void;
|
|
36
37
|
|
|
@@ -47,6 +48,12 @@ export interface FanOutRenderOptions {
|
|
|
47
48
|
write?: Writer;
|
|
48
49
|
/** Present when a gate covers the set, so the render can print the exact approve line. */
|
|
49
50
|
gate?: FanOutGateRef;
|
|
51
|
+
/**
|
|
52
|
+
* Present when each wave has its own gate (#3049). A wave's digest is known
|
|
53
|
+
* only once it is planned, so a plan-only render names each wave's gate and
|
|
54
|
+
* no digest.
|
|
55
|
+
*/
|
|
56
|
+
waveGate?: FanOutGateRef;
|
|
50
57
|
}
|
|
51
58
|
|
|
52
59
|
/** `unaffected` → `unaffected`; `blocked` also names the failure it was reached from. */
|
|
@@ -85,7 +92,8 @@ export function renderFanOutPlan(plan: FanOutPlan, options: FanOutRenderOptions
|
|
|
85
92
|
write(" nothing to run");
|
|
86
93
|
} else {
|
|
87
94
|
for (const [index, wave] of plan.waves.entries()) {
|
|
88
|
-
|
|
95
|
+
const gated = options.waveGate ? ` (gate ${waveGateName(options.waveGate.gate, index + 1)})` : "";
|
|
96
|
+
write(` wave ${index + 1}: ${wave.join(", ")}${gated}`);
|
|
89
97
|
}
|
|
90
98
|
}
|
|
91
99
|
|
|
@@ -117,6 +125,28 @@ export function renderFanOutPlan(plan: FanOutPlan, options: FanOutRenderOptions
|
|
|
117
125
|
if (options.gate) {
|
|
118
126
|
write(` approve: chant approve ${options.gate.op} ${options.gate.gate} --plan ${plan.digest}`);
|
|
119
127
|
}
|
|
128
|
+
if (options.waveGate) {
|
|
129
|
+
write(` each wave is planned when it is reached, and its gate binds that wave's set digest`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** One line per wave a gated-wave run reached (#3049): its digest, members and where it got to. */
|
|
134
|
+
function renderWaves(waves: readonly WaveRecord[], write: Writer): void {
|
|
135
|
+
for (const wave of waves) {
|
|
136
|
+
const state =
|
|
137
|
+
wave.status === "applied"
|
|
138
|
+
? `applied, approved by ${wave.approvedBy ?? "unknown"}`
|
|
139
|
+
: wave.status === "gated"
|
|
140
|
+
? wave.approved
|
|
141
|
+
? "stopped: the set changed after it was approved"
|
|
142
|
+
: "waiting for approval"
|
|
143
|
+
: "failed";
|
|
144
|
+
write(`wave ${wave.wave} (${wave.gate}): ${state}`);
|
|
145
|
+
if (wave.digest) write(` set digest: ${wave.digest}`);
|
|
146
|
+
for (const member of wave.members) write(` ${member.member} ${member.planDigest}`);
|
|
147
|
+
if (wave.approved) write(` approved digest: ${wave.approved}`);
|
|
148
|
+
for (const f of wave.failed ?? []) write(` ${f.component} failed${f.error ? `: ${f.error}` : ""}`);
|
|
149
|
+
}
|
|
120
150
|
}
|
|
121
151
|
|
|
122
152
|
/**
|
|
@@ -130,6 +160,17 @@ export function renderFanOutPlan(plan: FanOutPlan, options: FanOutRenderOptions
|
|
|
130
160
|
export function renderFanOutHuman(result: FanOutRunResult, options: FanOutRenderOptions = {}): void {
|
|
131
161
|
const write = options.write ?? stderr;
|
|
132
162
|
renderFanOutPlan(result.plan, { ...options, write });
|
|
163
|
+
if (result.waves) renderWaves(result.waves, write);
|
|
164
|
+
|
|
165
|
+
if (result.status === "gated" && result.waves) {
|
|
166
|
+
const at = result.waves[result.waves.length - 1];
|
|
167
|
+
write(
|
|
168
|
+
`gated at wave ${at?.wave ?? "?"}: ${result.completed.length} applied before it, ` +
|
|
169
|
+
`nothing in it or after it ran.`,
|
|
170
|
+
);
|
|
171
|
+
if (result.gate?.expiresAt) write(` expires: ${result.gate.expiresAt}`);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
133
174
|
|
|
134
175
|
if (result.status === "gated") {
|
|
135
176
|
const gate = result.gate;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The fan-out attempt record (`--resume`, #2420, #2417, #3049): what an
|
|
3
|
+
* attempt left behind and what the next one reads.
|
|
4
|
+
*
|
|
5
|
+
* It lives here rather than in the CLI handler so a caller that runs a wave
|
|
6
|
+
* per CI job (#3183) reads and writes the same file without importing the
|
|
7
|
+
* command.
|
|
8
|
+
*
|
|
9
|
+
* The digest is stored alongside the progress because progress is only
|
|
10
|
+
* meaningful for the plan it was made against. A fan-out derived from different
|
|
11
|
+
* source is a different fan-out, and carrying "cluster-a already applied" into
|
|
12
|
+
* it would be a claim about work nobody did.
|
|
13
|
+
*
|
|
14
|
+
* `outputs` holds what each completed component exposed. A component left to
|
|
15
|
+
* run may read one of them through `stackOutput()`, and without the record the
|
|
16
|
+
* re-run would resolve that reference to nothing unless somebody supplied the
|
|
17
|
+
* value by hand with `--seed-outputs`.
|
|
18
|
+
*
|
|
19
|
+
* `waves`, for a gated-wave fan-out, holds one entry per wave reached: its set
|
|
20
|
+
* digest, its members, its gate and where it got to. A resumed attempt plans
|
|
21
|
+
* a wave afresh rather than trusting this, since the point of the gate is the
|
|
22
|
+
* plan now. The entries are for whoever reads the record, the plan report
|
|
23
|
+
* (#3349) among them.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { dirname } from "node:path";
|
|
27
|
+
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
28
|
+
import { readWaveRecords, type WaveRecord } from "../gated-waves";
|
|
29
|
+
|
|
30
|
+
export interface FanOutAttempt {
|
|
31
|
+
digest: string;
|
|
32
|
+
completed: string[];
|
|
33
|
+
failed: string[];
|
|
34
|
+
outputs: Record<string, Record<string, unknown>>;
|
|
35
|
+
/** Gated-wave fan-outs only (#3049). */
|
|
36
|
+
waves?: WaveRecord[];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
40
|
+
typeof value === "object" && value !== null && !Array.isArray(value);
|
|
41
|
+
|
|
42
|
+
/** Read an attempt record, or `undefined` when there is none yet. Throws on a file that is not JSON. */
|
|
43
|
+
export function readFanOutAttempt(path: string): FanOutAttempt | undefined {
|
|
44
|
+
if (!existsSync(path)) return undefined;
|
|
45
|
+
const parsed = JSON.parse(readFileSync(path, "utf8")) as Partial<FanOutAttempt>;
|
|
46
|
+
const outputs: Record<string, Record<string, unknown>> = {};
|
|
47
|
+
if (isRecord(parsed.outputs)) {
|
|
48
|
+
for (const [name, value] of Object.entries(parsed.outputs)) if (isRecord(value)) outputs[name] = value;
|
|
49
|
+
}
|
|
50
|
+
const waves = readWaveRecords(parsed.waves);
|
|
51
|
+
return {
|
|
52
|
+
digest: typeof parsed.digest === "string" ? parsed.digest : "",
|
|
53
|
+
completed: Array.isArray(parsed.completed) ? parsed.completed.filter((n): n is string => typeof n === "string") : [],
|
|
54
|
+
failed: Array.isArray(parsed.failed) ? parsed.failed.filter((n): n is string => typeof n === "string") : [],
|
|
55
|
+
outputs,
|
|
56
|
+
...(waves.length > 0 ? { waves } : {}),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Written through a temporary file and a rename, so a process killed during the
|
|
62
|
+
* write leaves the previous record whole rather than half a JSON document.
|
|
63
|
+
*/
|
|
64
|
+
export function writeFanOutAttempt(path: string, attempt: FanOutAttempt): void {
|
|
65
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
66
|
+
const temporary = `${path}.${process.pid}.tmp`;
|
|
67
|
+
writeFileSync(temporary, JSON.stringify(attempt, null, 2) + "\n");
|
|
68
|
+
renameSync(temporary, path);
|
|
69
|
+
}
|
|
@@ -116,6 +116,23 @@ describe("resuming", () => {
|
|
|
116
116
|
expect(ran.sort()).toEqual(["app-one", "app-two", "cluster-a"]);
|
|
117
117
|
expect(second.plan.skipped).toContainEqual({ component: "net", reason: "already-applied" });
|
|
118
118
|
});
|
|
119
|
+
|
|
120
|
+
test("each component is reported as it settles, so progress survives a run that never returns", async () => {
|
|
121
|
+
const plan = planFanOut({ components: ESTATE, changed: ["net"] });
|
|
122
|
+
const settled: string[] = [];
|
|
123
|
+
const { registry, ran } = registryWith(["cluster-a"]);
|
|
124
|
+
const result = await runFanOut(plan, ESTATE, registry, {
|
|
125
|
+
...opts(),
|
|
126
|
+
onComponentSettled: (r) => {
|
|
127
|
+
settled.push(`${r.component}:${r.status}`);
|
|
128
|
+
// Every report lands before any later wave starts.
|
|
129
|
+
if (r.component === "net") expect(ran).toEqual(["net"]);
|
|
130
|
+
},
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
expect(result.status).toBe("fail");
|
|
134
|
+
expect(settled.sort()).toEqual(["app-three:ok", "cluster-a:fail", "cluster-b:ok", "net:ok"]);
|
|
135
|
+
});
|
|
119
136
|
});
|
|
120
137
|
|
|
121
138
|
describe("one approval over the whole set", () => {
|
|
@@ -27,19 +27,75 @@
|
|
|
27
27
|
* behind one fan-out is one approval, and re-deriving the same fan-out does not
|
|
28
28
|
* expire it. A component's *own* authored gates still decide per component —
|
|
29
29
|
* this adds a gate over the set, it does not remove the ones inside it.
|
|
30
|
+
*
|
|
31
|
+
* ## Or one approval per wave
|
|
32
|
+
*
|
|
33
|
+
* `waveGate` (#3049) is the other mode. Each wave is planned once the waves
|
|
34
|
+
* before it applied, and its gate binds that wave's set digest
|
|
35
|
+
* (`../gated-waves.ts`). See {@link runGatedWaves}.
|
|
30
36
|
*/
|
|
31
37
|
|
|
32
38
|
import {
|
|
39
|
+
deployContext,
|
|
33
40
|
runComponentDeploy,
|
|
34
41
|
type DriverComponent,
|
|
35
42
|
type DriverComponentResult,
|
|
36
43
|
type GateContext,
|
|
37
44
|
} from "./driver";
|
|
45
|
+
import type { ReleaseIdentity } from "../telemetry-attribution";
|
|
38
46
|
import { downstreamWithin, remainingFanOut, type FanOutPlan, type FanOutProgress, type FanOutSkip } from "./fan-out";
|
|
39
47
|
import { evaluateGate, gitGateLedgerPort, type GateLedgerPort } from "../op/gate";
|
|
40
48
|
import type { RunProgressEvent } from "./run-progress";
|
|
41
49
|
import type { CapabilityRegistry } from "./capability";
|
|
42
50
|
import type { PendingGateRecord } from "../lifecycle/gate-ledger";
|
|
51
|
+
import type { DeployContext } from "./capability";
|
|
52
|
+
import { planWaveComponent, type WaveComponentPlan } from "./wave-plan";
|
|
53
|
+
import { waveGateName, waveSetDigest, type WaveMember, type WaveRecord } from "../gated-waves";
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* One gate per wave (#3049), each bound to its wave's set digest. The other
|
|
57
|
+
* mode beside {@link FanOutGate}: that one approves the whole set once, this
|
|
58
|
+
* one approves each wave after the waves before it applied.
|
|
59
|
+
*/
|
|
60
|
+
export interface FanOutWaveGate {
|
|
61
|
+
/** The op the gates are recorded under, `fan-out` from the CLI. */
|
|
62
|
+
op: string;
|
|
63
|
+
/** The base gate name. Wave `n` waits on `waveGateName(gate, n)`. */
|
|
64
|
+
gate: string;
|
|
65
|
+
description?: string;
|
|
66
|
+
timeout?: string;
|
|
67
|
+
/**
|
|
68
|
+
* Plan one wave's components. Called once per wave, after every earlier
|
|
69
|
+
* wave settled, with the outputs they left. Defaults to
|
|
70
|
+
* `planWaveComponent` over the run's registry.
|
|
71
|
+
*/
|
|
72
|
+
planComponent?: (
|
|
73
|
+
component: DriverComponent,
|
|
74
|
+
ctx: DeployContext,
|
|
75
|
+
componentOutputs: Record<string, Record<string, unknown>>,
|
|
76
|
+
) => Promise<WaveComponentPlan>;
|
|
77
|
+
/**
|
|
78
|
+
* Run this wave and no other: CI's one job per wave. An earlier wave with
|
|
79
|
+
* work left refuses the run, since this wave's plan would read outputs that
|
|
80
|
+
* do not exist yet. A later wave is left for its own job.
|
|
81
|
+
*/
|
|
82
|
+
only?: number;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Thrown when `FanOutWaveGate.only` names a wave whose predecessors have not all applied. */
|
|
86
|
+
export class EarlierWaveNotAppliedError extends Error {
|
|
87
|
+
constructor(
|
|
88
|
+
readonly wave: number,
|
|
89
|
+
readonly earlier: number,
|
|
90
|
+
readonly components: string[],
|
|
91
|
+
) {
|
|
92
|
+
super(
|
|
93
|
+
`wave ${wave} cannot run yet: wave ${earlier} has not applied (${components.join(", ")}). ` +
|
|
94
|
+
`Run wave ${earlier} first, with the same --resume record.`,
|
|
95
|
+
);
|
|
96
|
+
this.name = "EarlierWaveNotAppliedError";
|
|
97
|
+
}
|
|
98
|
+
}
|
|
43
99
|
|
|
44
100
|
/** The single gate over a whole fan-out. Omit it to run without one. */
|
|
45
101
|
export interface FanOutGate {
|
|
@@ -60,8 +116,29 @@ export interface FanOutRunOptions {
|
|
|
60
116
|
now?: string;
|
|
61
117
|
/** One approval over the ordered set, bound to `plan.digest`. */
|
|
62
118
|
gate?: FanOutGate;
|
|
119
|
+
/** One approval per wave, each bound to its wave's set digest (#3049). Not with `gate`. */
|
|
120
|
+
waveGate?: FanOutWaveGate;
|
|
121
|
+
/**
|
|
122
|
+
* Plans made before the run, per component and then per member, handed to
|
|
123
|
+
* each component's steps as `DeployContext.plans` (#3183). A pull request's
|
|
124
|
+
* apply plans every member, decides its gate against the set, and passes
|
|
125
|
+
* the plans here so each step applies the plan that was approved. Not
|
|
126
|
+
* with `waveGate`, which plans each wave itself.
|
|
127
|
+
*/
|
|
128
|
+
plans?: Record<string, Record<string, unknown>>;
|
|
129
|
+
/** Called as each wave settles or stops at its gate, so a record survives a kill. */
|
|
130
|
+
onWaveSettled?: (record: WaveRecord) => void;
|
|
63
131
|
/** What an earlier attempt at this same plan already did. */
|
|
64
132
|
progress?: FanOutProgress;
|
|
133
|
+
/** The release each component deploys (#3061), as `InterpretRunOptions.releaseIdentity`. */
|
|
134
|
+
releaseIdentity?: (component: string) => ReleaseIdentity | undefined;
|
|
135
|
+
/**
|
|
136
|
+
* Called as each component settles, with the outputs it exposed, before the
|
|
137
|
+
* rest of its wave finishes. A caller that records progress here keeps what
|
|
138
|
+
* already applied when the process is killed mid-fan-out, which waiting for
|
|
139
|
+
* the returned result cannot do.
|
|
140
|
+
*/
|
|
141
|
+
onComponentSettled?: (result: DriverComponentResult, outputs: Record<string, unknown> | undefined) => void;
|
|
65
142
|
}
|
|
66
143
|
|
|
67
144
|
export interface FanOutRunResult {
|
|
@@ -78,6 +155,8 @@ export interface FanOutRunResult {
|
|
|
78
155
|
componentOutputs: Record<string, Record<string, unknown>>;
|
|
79
156
|
/** Present when `status === "gated"`: the pending fact to approve. */
|
|
80
157
|
gate?: PendingGateRecord;
|
|
158
|
+
/** With `waveGate`: each wave this attempt planned, in order, the one it stopped at included. */
|
|
159
|
+
waves?: WaveRecord[];
|
|
81
160
|
}
|
|
82
161
|
|
|
83
162
|
/**
|
|
@@ -103,6 +182,16 @@ export async function runFanOut(
|
|
|
103
182
|
// and a re-run does not re-approve. The digest is carried, not recomputed.
|
|
104
183
|
const active = options.progress ? remainingFanOut(plan, components, options.progress) : plan;
|
|
105
184
|
|
|
185
|
+
if (options.gate && options.waveGate) {
|
|
186
|
+
throw new Error("a fan-out takes one gate over the set or one gate per wave, not both");
|
|
187
|
+
}
|
|
188
|
+
if (options.waveGate && options.plans) {
|
|
189
|
+
throw new Error("a gated-wave fan-out plans each wave itself, so it takes no plans made beforehand");
|
|
190
|
+
}
|
|
191
|
+
if (options.waveGate) {
|
|
192
|
+
return runGatedWaves(plan, active, components, registry, options, options.waveGate, gates, componentOutputs);
|
|
193
|
+
}
|
|
194
|
+
|
|
106
195
|
if (options.gate) {
|
|
107
196
|
const check = await evaluateGate(gates.port, {
|
|
108
197
|
op: options.gate.op,
|
|
@@ -155,9 +244,10 @@ export async function runFanOut(
|
|
|
155
244
|
const waveResults = await Promise.all(
|
|
156
245
|
runnable.map(async (name) => {
|
|
157
246
|
options.onProgress?.({ type: "component-start", wave: waveNum, component: name });
|
|
247
|
+
const planned = options.plans?.[name];
|
|
158
248
|
const result = await runComponentDeploy(
|
|
159
249
|
byName.get(name)!,
|
|
160
|
-
{
|
|
250
|
+
{ ...deployContext(options, name), ...(planned && Object.keys(planned).length > 0 ? { plans: planned } : {}) },
|
|
161
251
|
registry,
|
|
162
252
|
componentOutputs,
|
|
163
253
|
options.onProgress,
|
|
@@ -169,6 +259,7 @@ export async function runFanOut(
|
|
|
169
259
|
component: name,
|
|
170
260
|
status: result.status === "fail" ? "failed" : result.status,
|
|
171
261
|
});
|
|
262
|
+
options.onComponentSettled?.(result, componentOutputs[name]);
|
|
172
263
|
return result;
|
|
173
264
|
}),
|
|
174
265
|
);
|
|
@@ -219,3 +310,215 @@ export async function runFanOut(
|
|
|
219
310
|
...(gatedFact ? { gate: gatedFact } : {}),
|
|
220
311
|
};
|
|
221
312
|
}
|
|
313
|
+
|
|
314
|
+
/** A plan failure as a component result, so it reads like any other failed component. */
|
|
315
|
+
function planFailure(component: string, error: string): DriverComponentResult {
|
|
316
|
+
return {
|
|
317
|
+
component,
|
|
318
|
+
ok: false,
|
|
319
|
+
status: "fail",
|
|
320
|
+
records: [{ component, phase: "Plan", kind: "plan", status: "fail", durationMs: 0, error }],
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* The per-wave mode (#3049). Waves keep the numbers the derivation gave them,
|
|
326
|
+
* so on a resumed attempt wave 3 is still wave 3 and its gate, and the
|
|
327
|
+
* approval recorded for it, are the same ones.
|
|
328
|
+
*
|
|
329
|
+
* For each wave with work left: plan its components against the outputs
|
|
330
|
+
* earlier waves wrote, take the set digest of what was planned, and decide
|
|
331
|
+
* `waveGateName(gate, n)` against it. Approved, the wave runs, each
|
|
332
|
+
* component applying the plans its steps just made. Not approved, the run
|
|
333
|
+
* stops there with nothing in that wave or after it applied. An approval
|
|
334
|
+
* that stands for another digest (the set changed since someone approved
|
|
335
|
+
* it) is the same stop, and the record names both digests.
|
|
336
|
+
*
|
|
337
|
+
* A component that fails to plan or to apply blocks its own dependents and
|
|
338
|
+
* nothing else, as in the whole-set mode.
|
|
339
|
+
*/
|
|
340
|
+
async function runGatedWaves(
|
|
341
|
+
plan: FanOutPlan,
|
|
342
|
+
active: FanOutPlan,
|
|
343
|
+
components: DriverComponent[],
|
|
344
|
+
registry: CapabilityRegistry,
|
|
345
|
+
options: FanOutRunOptions,
|
|
346
|
+
waveGate: FanOutWaveGate,
|
|
347
|
+
gates: GateContext,
|
|
348
|
+
componentOutputs: Record<string, Record<string, unknown>>,
|
|
349
|
+
): Promise<FanOutRunResult> {
|
|
350
|
+
const byName = new Map(components.map((c) => [c.name, c]));
|
|
351
|
+
const left = new Set(active.order);
|
|
352
|
+
const planComponent =
|
|
353
|
+
waveGate.planComponent ??
|
|
354
|
+
((component: DriverComponent, ctx: DeployContext, outputs: Record<string, Record<string, unknown>>) =>
|
|
355
|
+
planWaveComponent(component, registry, ctx, outputs));
|
|
356
|
+
|
|
357
|
+
const results: DriverComponentResult[] = [];
|
|
358
|
+
const completed: string[] = [];
|
|
359
|
+
const failed: string[] = [];
|
|
360
|
+
const blockedBy = new Map<string, string>();
|
|
361
|
+
const waves: WaveRecord[] = [];
|
|
362
|
+
let gatedFact: PendingGateRecord | undefined;
|
|
363
|
+
|
|
364
|
+
const markBlocked = (root: string): void => {
|
|
365
|
+
for (const [component, by] of downstreamWithin(components, left, [root])) {
|
|
366
|
+
if (blockedBy.has(component) || failed.includes(component)) continue;
|
|
367
|
+
blockedBy.set(component, by);
|
|
368
|
+
}
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
options.onProgress?.({ type: "run-start", waves: plan.waves });
|
|
372
|
+
|
|
373
|
+
for (const [waveIndex, wave] of plan.waves.entries()) {
|
|
374
|
+
const waveNum = waveIndex + 1;
|
|
375
|
+
const runnable = wave.filter((name) => left.has(name) && !blockedBy.has(name));
|
|
376
|
+
if (waveGate.only !== undefined && waveNum < waveGate.only) {
|
|
377
|
+
if (runnable.length > 0) throw new EarlierWaveNotAppliedError(waveGate.only, waveNum, runnable);
|
|
378
|
+
continue;
|
|
379
|
+
}
|
|
380
|
+
if (waveGate.only !== undefined && waveNum > waveGate.only) break;
|
|
381
|
+
if (runnable.length === 0) continue;
|
|
382
|
+
|
|
383
|
+
const gateName = waveGateName(waveGate.gate, waveNum);
|
|
384
|
+
const contexts = new Map(runnable.map((name) => [name, deployContext(options, name)]));
|
|
385
|
+
const planned = await Promise.all(
|
|
386
|
+
runnable.map((name) => planComponent(byName.get(name)!, contexts.get(name)!, componentOutputs)),
|
|
387
|
+
);
|
|
388
|
+
|
|
389
|
+
const members: WaveMember[] = [];
|
|
390
|
+
const plansFor = new Map<string, Record<string, unknown>>();
|
|
391
|
+
const waveFailures: Array<{ component: string; error?: string }> = [];
|
|
392
|
+
for (const p of planned) {
|
|
393
|
+
if ("error" in p) {
|
|
394
|
+
waveFailures.push({ component: p.component, error: p.error });
|
|
395
|
+
continue;
|
|
396
|
+
}
|
|
397
|
+
members.push(...p.members);
|
|
398
|
+
plansFor.set(p.component, p.plans);
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
let digest: string;
|
|
402
|
+
try {
|
|
403
|
+
// A wave where nothing planned has no set, and so no digest.
|
|
404
|
+
digest = members.length > 0 ? waveSetDigest(members) : "";
|
|
405
|
+
} catch (err) {
|
|
406
|
+
// Two components planning the same root: there is no one plan for it.
|
|
407
|
+
const error = err instanceof Error ? err.message : String(err);
|
|
408
|
+
for (const name of plansFor.keys()) waveFailures.push({ component: name, error });
|
|
409
|
+
plansFor.clear();
|
|
410
|
+
digest = "";
|
|
411
|
+
}
|
|
412
|
+
for (const f of waveFailures) {
|
|
413
|
+
failed.push(f.component);
|
|
414
|
+
results.push(planFailure(f.component, f.error ?? "plan failed"));
|
|
415
|
+
markBlocked(f.component);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const toRun = runnable.filter((name) => plansFor.has(name));
|
|
419
|
+
const record: WaveRecord = {
|
|
420
|
+
wave: waveNum,
|
|
421
|
+
op: waveGate.op,
|
|
422
|
+
gate: gateName,
|
|
423
|
+
components: [...runnable].sort(),
|
|
424
|
+
digest,
|
|
425
|
+
members: [...members].sort((a, b) => a.member.localeCompare(b.member)),
|
|
426
|
+
status: "failed",
|
|
427
|
+
...(waveFailures.length > 0 ? { failed: waveFailures } : {}),
|
|
428
|
+
};
|
|
429
|
+
if (toRun.length === 0) {
|
|
430
|
+
waves.push(record);
|
|
431
|
+
options.onWaveSettled?.(record);
|
|
432
|
+
continue;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const check = await evaluateGate(gates.port, {
|
|
436
|
+
op: waveGate.op,
|
|
437
|
+
gate: gateName,
|
|
438
|
+
planDigest: digest,
|
|
439
|
+
description: waveGate.description ?? `wave ${waveNum} of ${plan.waves.length}: ${toRun.join(", ")}`,
|
|
440
|
+
...(waveGate.timeout ? { timeout: waveGate.timeout } : {}),
|
|
441
|
+
...(options.now ? { now: options.now } : {}),
|
|
442
|
+
});
|
|
443
|
+
if (!check.satisfied) {
|
|
444
|
+
// Nothing in this wave ran, and no later wave is planned: its plan
|
|
445
|
+
// would read outputs this wave has not written.
|
|
446
|
+
const stopped: WaveRecord = {
|
|
447
|
+
...record,
|
|
448
|
+
status: "gated",
|
|
449
|
+
...(check.mismatch?.approved ? { approved: check.mismatch.approved } : {}),
|
|
450
|
+
};
|
|
451
|
+
waves.push(stopped);
|
|
452
|
+
options.onWaveSettled?.(stopped);
|
|
453
|
+
gatedFact = check.pending;
|
|
454
|
+
break;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
options.onProgress?.({ type: "wave-start", wave: waveNum, components: toRun });
|
|
458
|
+
const waveResults = await Promise.all(
|
|
459
|
+
toRun.map(async (name) => {
|
|
460
|
+
options.onProgress?.({ type: "component-start", wave: waveNum, component: name });
|
|
461
|
+
const plans = plansFor.get(name)!;
|
|
462
|
+
const ctx = { ...contexts.get(name)!, ...(Object.keys(plans).length > 0 ? { plans } : {}) };
|
|
463
|
+
const result = await runComponentDeploy(byName.get(name)!, ctx, registry, componentOutputs, options.onProgress, gates);
|
|
464
|
+
options.onProgress?.({
|
|
465
|
+
type: "component-done",
|
|
466
|
+
wave: waveNum,
|
|
467
|
+
component: name,
|
|
468
|
+
status: result.status === "fail" ? "failed" : result.status,
|
|
469
|
+
});
|
|
470
|
+
options.onComponentSettled?.(result, componentOutputs[name]);
|
|
471
|
+
return result;
|
|
472
|
+
}),
|
|
473
|
+
);
|
|
474
|
+
results.push(...waveResults);
|
|
475
|
+
|
|
476
|
+
for (const result of waveResults) {
|
|
477
|
+
if (result.status === "ok") {
|
|
478
|
+
completed.push(result.component);
|
|
479
|
+
continue;
|
|
480
|
+
}
|
|
481
|
+
if (result.status === "fail") {
|
|
482
|
+
failed.push(result.component);
|
|
483
|
+
waveFailures.push({ component: result.component });
|
|
484
|
+
} else {
|
|
485
|
+
gatedFact ??= result.gate;
|
|
486
|
+
}
|
|
487
|
+
markBlocked(result.component);
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
const settled: WaveRecord = {
|
|
491
|
+
...record,
|
|
492
|
+
status: waveFailures.length > 0 ? "failed" : "applied",
|
|
493
|
+
approvedBy: check.resolution.resolvedBy,
|
|
494
|
+
...(waveFailures.length > 0 ? { failed: waveFailures } : {}),
|
|
495
|
+
};
|
|
496
|
+
waves.push(settled);
|
|
497
|
+
options.onWaveSettled?.(settled);
|
|
498
|
+
options.onProgress?.({
|
|
499
|
+
type: "wave-done",
|
|
500
|
+
wave: waveNum,
|
|
501
|
+
status: waveFailures.length > 0 ? "failed" : waveResults.some((r) => r.status === "gated") ? "gated" : "ok",
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
const blocked: FanOutSkip[] = [...blockedBy.entries()]
|
|
506
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
507
|
+
.map(([component, by]) => ({ component, reason: "blocked" as const, blockedBy: by }));
|
|
508
|
+
|
|
509
|
+
const status: FanOutRunResult["status"] = failed.length > 0 ? "fail" : gatedFact ? "gated" : "ok";
|
|
510
|
+
options.onProgress?.({ type: "run-done", status: status === "fail" ? "failed" : status });
|
|
511
|
+
|
|
512
|
+
return {
|
|
513
|
+
// The derivation's waves, not the narrowed re-layering: their numbers name the gates.
|
|
514
|
+
plan: { ...active, waves: plan.waves },
|
|
515
|
+
status,
|
|
516
|
+
results,
|
|
517
|
+
completed: [...completed].sort(),
|
|
518
|
+
failed: [...failed].sort(),
|
|
519
|
+
blocked,
|
|
520
|
+
componentOutputs,
|
|
521
|
+
...(gatedFact ? { gate: gatedFact } : {}),
|
|
522
|
+
waves,
|
|
523
|
+
};
|
|
524
|
+
}
|
|
@@ -33,6 +33,8 @@ export interface DeriveFanOutOptions {
|
|
|
33
33
|
buildParams?: BuildParamProvenance[];
|
|
34
34
|
/** Already-loaded project config, when the caller read it for its own reasons. */
|
|
35
35
|
config?: ChantConfig;
|
|
36
|
+
/** Components that form wave 1 (#3049). See `FanOutRequest.canary`. */
|
|
37
|
+
canary?: string[];
|
|
36
38
|
}
|
|
37
39
|
|
|
38
40
|
export interface DerivedFanOut {
|
|
@@ -80,6 +82,7 @@ export async function deriveFanOut(options: DeriveFanOutOptions): Promise<Derive
|
|
|
80
82
|
components,
|
|
81
83
|
changed: signal.changed,
|
|
82
84
|
...(signal.indeterminate.length > 0 ? { indeterminate: signal.indeterminate } : {}),
|
|
85
|
+
...(options.canary?.length ? { canary: options.canary } : {}),
|
|
83
86
|
});
|
|
84
87
|
|
|
85
88
|
return { success: true, components, signal, plan };
|