@intentius/chant 0.124.28 → 0.124.30
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/build-params.d.ts +34 -1
- package/dist/build-params.d.ts.map +1 -1
- package/dist/build-params.js +31 -1
- package/dist/change-set-graph.d.ts +8 -1
- package/dist/change-set-graph.d.ts.map +1 -1
- package/dist/change-set-graph.js +4 -2
- package/dist/change-set.d.ts +10 -0
- package/dist/change-set.d.ts.map +1 -1
- package/dist/change-set.js +4 -3
- package/dist/cli/build-params-cli.d.ts +7 -1
- package/dist/cli/build-params-cli.d.ts.map +1 -1
- package/dist/cli/build-params-cli.js +17 -8
- package/dist/cli/commands/build.d.ts.map +1 -1
- package/dist/cli/commands/build.js +6 -3
- package/dist/cli/handlers/approve.d.ts.map +1 -1
- package/dist/cli/handlers/approve.js +45 -0
- package/dist/cli/handlers/build.d.ts.map +1 -1
- package/dist/cli/handlers/build.js +18 -4
- package/dist/cli/handlers/fan-out.d.ts +9 -0
- package/dist/cli/handlers/fan-out.d.ts.map +1 -1
- package/dist/cli/handlers/fan-out.js +67 -8
- package/dist/cli/handlers/lifecycle-gc.d.ts +19 -0
- package/dist/cli/handlers/lifecycle-gc.d.ts.map +1 -0
- package/dist/cli/handlers/lifecycle-gc.js +143 -0
- package/dist/cli/handlers/pr.d.ts +11 -3
- package/dist/cli/handlers/pr.d.ts.map +1 -1
- package/dist/cli/handlers/pr.js +134 -26
- package/dist/cli/handlers/promote.d.ts +7 -0
- package/dist/cli/handlers/promote.d.ts.map +1 -1
- package/dist/cli/handlers/promote.js +210 -33
- package/dist/cli/handlers/release.d.ts +62 -5
- package/dist/cli/handlers/release.d.ts.map +1 -1
- package/dist/cli/handlers/release.js +414 -28
- package/dist/cli/handlers/run.d.ts +11 -1
- package/dist/cli/handlers/run.d.ts.map +1 -1
- package/dist/cli/handlers/run.js +67 -6
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/main.js +91 -10
- package/dist/cli/parse-args.d.ts.map +1 -1
- package/dist/cli/parse-args.js +73 -2
- package/dist/cli/registry.d.ts +32 -4
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/cli/run-events.d.ts +3 -2
- package/dist/cli/run-events.d.ts.map +1 -1
- package/dist/cli/run-events.js +5 -3
- package/dist/components/auto-release.d.ts +2 -0
- package/dist/components/auto-release.d.ts.map +1 -1
- package/dist/components/auto-release.js +1 -0
- package/dist/components/capability.d.ts +9 -0
- package/dist/components/capability.d.ts.map +1 -1
- package/dist/components/driver.d.ts +8 -0
- package/dist/components/driver.d.ts.map +1 -1
- package/dist/components/driver.js +35 -0
- package/dist/components/fan-out-inputs.d.ts +69 -0
- package/dist/components/fan-out-inputs.d.ts.map +1 -0
- package/dist/components/fan-out-inputs.js +169 -0
- package/dist/components/fan-out-record.d.ts +13 -0
- package/dist/components/fan-out-record.d.ts.map +1 -1
- package/dist/components/fan-out-record.js +16 -0
- package/dist/components/fan-out-run.d.ts.map +1 -1
- package/dist/components/fan-out-run.js +50 -0
- package/dist/components/fan-out.d.ts +4 -0
- package/dist/components/fan-out.d.ts.map +1 -1
- package/dist/components/pr-pipeline.d.ts +49 -0
- package/dist/components/pr-pipeline.d.ts.map +1 -1
- package/dist/components/pr-pipeline.js +38 -4
- package/dist/components/pr-run.d.ts +20 -2
- package/dist/components/pr-run.d.ts.map +1 -1
- package/dist/components/pr-run.js +26 -13
- package/dist/components/promote.d.ts +23 -0
- package/dist/components/promote.d.ts.map +1 -1
- package/dist/components/promote.js +60 -2
- package/dist/components/release-compile.d.ts +9 -0
- package/dist/components/release-compile.d.ts.map +1 -1
- package/dist/components/release-compile.js +9 -3
- package/dist/components/release-gate.d.ts +11 -1
- package/dist/components/release-gate.d.ts.map +1 -1
- package/dist/components/release-gate.js +10 -4
- package/dist/components/wave-plan.d.ts +16 -0
- package/dist/components/wave-plan.d.ts.map +1 -1
- package/dist/components/wave-plan.js +36 -2
- package/dist/config.d.ts +38 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +2 -0
- package/dist/flag-params.d.ts +54 -0
- package/dist/flag-params.d.ts.map +1 -0
- package/dist/flag-params.js +130 -0
- package/dist/lifecycle/__fixtures__/publish-fakes.d.ts +120 -0
- package/dist/lifecycle/__fixtures__/publish-fakes.d.ts.map +1 -0
- package/dist/lifecycle/__fixtures__/publish-fakes.js +304 -0
- package/dist/lifecycle/change-set-store.d.ts +2 -1
- package/dist/lifecycle/change-set-store.d.ts.map +1 -1
- package/dist/lifecycle/change-set-store.js +2 -1
- package/dist/lifecycle/delivery-metrics.d.ts +139 -0
- package/dist/lifecycle/delivery-metrics.d.ts.map +1 -0
- package/dist/lifecycle/delivery-metrics.js +145 -0
- package/dist/lifecycle/deploy-outcome.d.ts +81 -0
- package/dist/lifecycle/deploy-outcome.d.ts.map +1 -0
- package/dist/lifecycle/deploy-outcome.js +101 -0
- package/dist/lifecycle/gate-change-set.d.ts +10 -1
- package/dist/lifecycle/gate-change-set.d.ts.map +1 -1
- package/dist/lifecycle/gate-change-set.js +35 -3
- package/dist/lifecycle/gate-ledger.d.ts +2 -3
- package/dist/lifecycle/gate-ledger.d.ts.map +1 -1
- package/dist/lifecycle/git.d.ts +30 -1
- package/dist/lifecycle/git.d.ts.map +1 -1
- package/dist/lifecycle/git.js +31 -5
- package/dist/lifecycle/index.d.ts +4 -0
- package/dist/lifecycle/index.d.ts.map +1 -1
- package/dist/lifecycle/index.js +4 -0
- package/dist/lifecycle/mixed-environment.d.ts +95 -0
- package/dist/lifecycle/mixed-environment.d.ts.map +1 -0
- package/dist/lifecycle/mixed-environment.js +154 -0
- package/dist/lifecycle/publish-attachments.d.ts +79 -0
- package/dist/lifecycle/publish-attachments.d.ts.map +1 -0
- package/dist/lifecycle/publish-attachments.js +179 -0
- package/dist/lifecycle/publish-credentials.d.ts +37 -0
- package/dist/lifecycle/publish-credentials.d.ts.map +1 -0
- package/dist/lifecycle/publish-credentials.js +69 -0
- package/dist/lifecycle/publish-declaration.d.ts +97 -0
- package/dist/lifecycle/publish-declaration.d.ts.map +1 -0
- package/dist/lifecycle/publish-declaration.js +83 -0
- package/dist/lifecycle/publish-forge.d.ts +53 -0
- package/dist/lifecycle/publish-forge.d.ts.map +1 -0
- package/dist/lifecycle/publish-forge.js +298 -0
- package/dist/lifecycle/publish-npm.d.ts +61 -0
- package/dist/lifecycle/publish-npm.d.ts.map +1 -0
- package/dist/lifecycle/publish-npm.js +242 -0
- package/dist/lifecycle/publish-oci.d.ts +64 -0
- package/dist/lifecycle/publish-oci.d.ts.map +1 -0
- package/dist/lifecycle/publish-oci.js +383 -0
- package/dist/lifecycle/publish-record.d.ts +66 -0
- package/dist/lifecycle/publish-record.d.ts.map +1 -0
- package/dist/lifecycle/publish-record.js +84 -0
- package/dist/lifecycle/publish-targets.d.ts +45 -0
- package/dist/lifecycle/publish-targets.d.ts.map +1 -0
- package/dist/lifecycle/publish-targets.js +85 -0
- package/dist/lifecycle/release-cut.d.ts +48 -1
- package/dist/lifecycle/release-cut.d.ts.map +1 -1
- package/dist/lifecycle/release-cut.js +116 -23
- package/dist/lifecycle/release-join.d.ts +95 -0
- package/dist/lifecycle/release-join.d.ts.map +1 -0
- package/dist/lifecycle/release-join.js +139 -0
- package/dist/lifecycle/release-ledger.d.ts +8 -3
- package/dist/lifecycle/release-ledger.d.ts.map +1 -1
- package/dist/lifecycle/release-ledger.js +2 -0
- package/dist/lifecycle/release-publish.d.ts +312 -0
- package/dist/lifecycle/release-publish.d.ts.map +1 -0
- package/dist/lifecycle/release-publish.js +335 -0
- package/dist/lifecycle/release-revocation.d.ts +80 -0
- package/dist/lifecycle/release-revocation.d.ts.map +1 -0
- package/dist/lifecycle/release-revocation.js +123 -0
- package/dist/lifecycle/release-store.d.ts +12 -2
- package/dist/lifecycle/release-store.d.ts.map +1 -1
- package/dist/lifecycle/release-store.js +4 -2
- package/dist/lifecycle/release-verify.d.ts +61 -0
- package/dist/lifecycle/release-verify.d.ts.map +1 -0
- package/dist/lifecycle/release-verify.js +92 -0
- package/dist/lifecycle/release-workstation.d.ts +91 -0
- package/dist/lifecycle/release-workstation.d.ts.map +1 -0
- package/dist/lifecycle/release-workstation.js +180 -0
- package/dist/lifecycle/retention-policy.d.ts +45 -0
- package/dist/lifecycle/retention-policy.d.ts.map +1 -0
- package/dist/lifecycle/retention-policy.js +43 -0
- package/dist/lifecycle/retention.d.ts +185 -0
- package/dist/lifecycle/retention.d.ts.map +1 -0
- package/dist/lifecycle/retention.js +400 -0
- package/dist/lifecycle/run-ledger.d.ts +1 -1
- package/dist/lifecycle/run-ledger.d.ts.map +1 -1
- package/dist/lifecycle/run-ledger.js +9 -0
- package/dist/op/activities/activity-contracts.d.ts +1 -0
- package/dist/op/activities/activity-contracts.d.ts.map +1 -1
- package/dist/op/activities/activity-contracts.js +1 -0
- package/dist/op/activities/shell.d.ts +6 -0
- package/dist/op/activities/shell.d.ts.map +1 -1
- package/dist/op/activities/shell.js +1 -1
- package/dist/op/local-executor.d.ts +23 -1
- package/dist/op/local-executor.d.ts.map +1 -1
- package/dist/op/local-executor.js +9 -3
- package/dist/op/runtime.d.ts +27 -0
- package/dist/op/runtime.d.ts.map +1 -1
- package/dist/op/runtime.js +20 -0
- package/dist/plan-summary.d.ts.map +1 -1
- package/dist/plan-summary.js +8 -2
- package/dist/pr-forge.d.ts +23 -0
- package/dist/pr-forge.d.ts.map +1 -1
- package/dist/pr-forge.js +44 -0
- package/dist/pr-loop.d.ts +17 -0
- package/dist/pr-loop.d.ts.map +1 -1
- package/dist/pr-loop.js +30 -4
- package/dist/pr-report-url.d.ts +82 -0
- package/dist/pr-report-url.d.ts.map +1 -0
- package/dist/pr-report-url.js +174 -0
- package/dist/provenance.d.ts +2 -2
- package/dist/provenance.d.ts.map +1 -1
- package/dist/run-event-store.d.ts +29 -2
- package/dist/run-event-store.d.ts.map +1 -1
- package/dist/run-event-store.js +50 -3
- package/dist/run-event-stream.d.ts +4 -3
- package/dist/run-event-stream.d.ts.map +1 -1
- package/dist/run-event-stream.js +7 -3
- package/dist/run-event.d.ts +34 -1
- package/dist/run-event.d.ts.map +1 -1
- package/dist/run-event.js +4 -0
- package/dist/wave-runtime.mjs +260 -229
- package/dist/workspace/__fixtures__/flag-workspace.d.ts +31 -0
- package/dist/workspace/__fixtures__/flag-workspace.d.ts.map +1 -0
- package/dist/workspace/__fixtures__/flag-workspace.js +114 -0
- package/dist/workspace/change-set.schema.json +5 -1
- package/dist/workspace/check.schema.json +4 -2
- package/dist/workspace/checks/records.d.ts +9 -1
- package/dist/workspace/checks/records.d.ts.map +1 -1
- package/dist/workspace/checks/records.js +35 -1
- package/dist/workspace/checks.d.ts +1 -0
- package/dist/workspace/checks.d.ts.map +1 -1
- package/dist/workspace/checks.js +2 -1
- package/dist/workspace/ci-green-forge.d.ts +2 -0
- package/dist/workspace/ci-green-forge.d.ts.map +1 -1
- package/dist/workspace/ci-green.d.ts +10 -0
- package/dist/workspace/ci-green.d.ts.map +1 -1
- package/dist/workspace/ci-green.js +14 -5
- package/dist/workspace/ci-run.d.ts +222 -0
- package/dist/workspace/ci-run.d.ts.map +1 -0
- package/dist/workspace/ci-run.js +464 -0
- package/dist/workspace/ci-serve.d.ts +214 -0
- package/dist/workspace/ci-serve.d.ts.map +1 -0
- package/dist/workspace/ci-serve.js +412 -0
- package/dist/workspace/declaration.d.ts +16 -0
- package/dist/workspace/declaration.d.ts.map +1 -1
- package/dist/workspace/declaration.js +26 -0
- package/dist/workspace/declaration.schema.json +64 -0
- package/dist/workspace/declared-kinds.d.ts +16 -0
- package/dist/workspace/declared-kinds.d.ts.map +1 -1
- package/dist/workspace/declared-kinds.js +25 -0
- package/dist/workspace/delivery-metrics.d.ts +91 -0
- package/dist/workspace/delivery-metrics.d.ts.map +1 -0
- package/dist/workspace/delivery-metrics.js +217 -0
- package/dist/workspace/delivery-metrics.schema.json +519 -0
- package/dist/workspace/flags-read.d.ts +37 -0
- package/dist/workspace/flags-read.d.ts.map +1 -0
- package/dist/workspace/flags-read.js +103 -0
- package/dist/workspace/flags.d.ts +100 -0
- package/dist/workspace/flags.d.ts.map +1 -0
- package/dist/workspace/flags.js +98 -0
- package/dist/workspace/graph-cli.js +1 -1
- package/dist/workspace/intent-cli.d.ts +11 -2
- package/dist/workspace/intent-cli.d.ts.map +1 -1
- package/dist/workspace/intent-cli.js +54 -5
- package/dist/workspace/intent-trace.d.ts +219 -0
- package/dist/workspace/intent-trace.d.ts.map +1 -0
- package/dist/workspace/intent-trace.js +314 -0
- package/dist/workspace/intent.d.ts +30 -6
- package/dist/workspace/intent.d.ts.map +1 -1
- package/dist/workspace/intent.js +141 -21
- package/dist/workspace/intent.schema.json +702 -6
- package/dist/workspace/overlap.d.ts +146 -0
- package/dist/workspace/overlap.d.ts.map +1 -0
- package/dist/workspace/overlap.js +276 -0
- package/dist/workspace/pr-report.schema.json +5 -0
- package/dist/workspace/reason-codes.d.ts +2 -0
- package/dist/workspace/reason-codes.d.ts.map +1 -1
- package/dist/workspace/reason-codes.js +3 -0
- package/dist/workspace/records.d.ts +15 -2
- package/dist/workspace/records.d.ts.map +1 -1
- package/dist/workspace/records.js +32 -0
- package/dist/workspace/records.schema.json +21 -2
- package/dist/workspace/reference-kinds/flag/flag.kind.mjs +32 -0
- package/dist/workspace/reference-kinds/flag/flag.schema.json +53 -0
- package/dist/workspace/releases.d.ts +9 -0
- package/dist/workspace/releases.d.ts.map +1 -1
- package/dist/workspace/releases.js +9 -0
- package/dist/workspace/releases.schema.json +123 -1
- package/dist/workspace/run-event.schema.json +24 -6
- package/dist/workspace/status-gates.d.ts +8 -0
- package/dist/workspace/status-gates.d.ts.map +1 -1
- package/dist/workspace/status-stewards.d.ts +2 -0
- package/dist/workspace/status-stewards.d.ts.map +1 -1
- package/dist/workspace/status-stewards.js +1 -0
- package/dist/workspace/status.d.ts +78 -0
- package/dist/workspace/status.d.ts.map +1 -1
- package/dist/workspace/status.js +170 -3
- package/dist/workspace/status.schema.json +210 -2
- package/dist/workspace/wip.d.ts.map +1 -1
- package/dist/workspace/wip.js +11 -3
- package/package.json +1 -1
- package/src/build-params.test.ts +46 -0
- package/src/build-params.ts +55 -1
- package/src/change-set-graph.ts +10 -2
- package/src/change-set.ts +14 -3
- package/src/cli/build-params-cli.ts +22 -9
- package/src/cli/commands/build.ts +6 -3
- package/src/cli/handlers/approve-release.test.ts +5 -1
- package/src/cli/handlers/approve-revoked.test.ts +155 -0
- package/src/cli/handlers/approve.ts +47 -0
- package/src/cli/handlers/build.test.ts +30 -0
- package/src/cli/handlers/build.ts +20 -4
- package/src/cli/handlers/fan-out.test.ts +262 -1
- package/src/cli/handlers/fan-out.ts +72 -8
- package/src/cli/handlers/intent-trace.e2e.test.ts +201 -0
- package/src/cli/handlers/lifecycle-gc.test.ts +126 -0
- package/src/cli/handlers/lifecycle-gc.ts +146 -0
- package/src/cli/handlers/pr.test.ts +148 -1
- package/src/cli/handlers/pr.ts +144 -28
- package/src/cli/handlers/promote.test.ts +280 -2
- package/src/cli/handlers/promote.ts +210 -34
- package/src/cli/handlers/release-implicit.e2e.test.ts +12 -4
- package/src/cli/handlers/release-join.e2e.test.ts +185 -0
- package/src/cli/handlers/release-publish-targets.e2e.test.ts +295 -0
- package/src/cli/handlers/release-verify.e2e.test.ts +117 -0
- package/src/cli/handlers/release-workstation-ci.e2e.test.ts +135 -0
- package/src/cli/handlers/release.e2e.test.ts +146 -32
- package/src/cli/handlers/release.ts +415 -28
- package/src/cli/handlers/run-auto-release.test.ts +9 -0
- package/src/cli/handlers/run-release.test.ts +55 -0
- package/src/cli/handlers/run.ts +69 -5
- package/src/cli/main.test.ts +1 -0
- package/src/cli/main.ts +91 -10
- package/src/cli/parse-args.ts +54 -2
- package/src/cli/registry.ts +32 -4
- package/src/cli/run-events.ts +5 -2
- package/src/components/auto-release.test.ts +9 -0
- package/src/components/auto-release.ts +3 -0
- package/src/components/capability.ts +9 -0
- package/src/components/driver.test.ts +24 -0
- package/src/components/driver.ts +36 -0
- package/src/components/fan-out-inputs.test.ts +85 -0
- package/src/components/fan-out-inputs.ts +192 -0
- package/src/components/fan-out-record.test.ts +10 -0
- package/src/components/fan-out-record.ts +19 -0
- package/src/components/fan-out-run.ts +48 -0
- package/src/components/fan-out.ts +4 -0
- package/src/components/pr-pipeline.ts +67 -4
- package/src/components/pr-run.test.ts +127 -0
- package/src/components/pr-run.ts +32 -12
- package/src/components/promote.ts +68 -2
- package/src/components/release-compile.ts +17 -3
- package/src/components/release-gate.ts +18 -4
- package/src/components/wave-plan.ts +45 -2
- package/src/config.ts +8 -0
- package/src/flag-params.test.ts +144 -0
- package/src/flag-params.ts +134 -0
- package/src/lifecycle/__fixtures__/publish-fakes.ts +351 -0
- package/src/lifecycle/change-set-store.ts +2 -1
- package/src/lifecycle/delivery-metrics.ts +263 -0
- package/src/lifecycle/deploy-outcome.ts +147 -0
- package/src/lifecycle/gate-change-set.ts +37 -6
- package/src/lifecycle/gate-ledger.ts +2 -2
- package/src/lifecycle/git.ts +61 -8
- package/src/lifecycle/index.ts +4 -0
- package/src/lifecycle/mixed-environment.test.ts +97 -0
- package/src/lifecycle/mixed-environment.ts +204 -0
- package/src/lifecycle/publish-attachments.ts +196 -0
- package/src/lifecycle/publish-credentials.ts +84 -0
- package/src/lifecycle/publish-declaration.ts +95 -0
- package/src/lifecycle/publish-forge.ts +325 -0
- package/src/lifecycle/publish-npm.ts +259 -0
- package/src/lifecycle/publish-oci.ts +396 -0
- package/src/lifecycle/publish-record.ts +113 -0
- package/src/lifecycle/publish-resume.test.ts +168 -0
- package/src/lifecycle/publish-targets.test.ts +494 -0
- package/src/lifecycle/publish-targets.ts +100 -0
- package/src/lifecycle/release-cut.test.ts +74 -2
- package/src/lifecycle/release-cut.ts +158 -22
- package/src/lifecycle/release-flag.test.ts +112 -0
- package/src/lifecycle/release-join.test.ts +68 -0
- package/src/lifecycle/release-join.ts +182 -0
- package/src/lifecycle/release-ledger.ts +9 -3
- package/src/lifecycle/release-publish.test.ts +225 -0
- package/src/lifecycle/release-publish.ts +568 -0
- package/src/lifecycle/release-revocation.ts +147 -0
- package/src/lifecycle/release-store.ts +15 -4
- package/src/lifecycle/release-verify.test.ts +110 -0
- package/src/lifecycle/release-verify.ts +120 -0
- package/src/lifecycle/release-workstation.test.ts +204 -0
- package/src/lifecycle/release-workstation.ts +216 -0
- package/src/lifecycle/retention-growth.e2e.test.ts +279 -0
- package/src/lifecycle/retention-policy.ts +63 -0
- package/src/lifecycle/retention.test.ts +363 -0
- package/src/lifecycle/retention.ts +508 -0
- package/src/lifecycle/run-ledger.ts +7 -1
- package/src/op/activities/activity-contracts.ts +1 -0
- package/src/op/activities/shell.ts +7 -1
- package/src/op/local-executor.ts +27 -4
- package/src/op/runtime.ts +42 -0
- package/src/plan-summary.test.ts +20 -0
- package/src/plan-summary.ts +8 -1
- package/src/pr-forge.test.ts +25 -0
- package/src/pr-forge.ts +62 -0
- package/src/pr-loop.test.ts +13 -1
- package/src/pr-loop.ts +41 -4
- package/src/pr-report-url.test.ts +131 -0
- package/src/pr-report-url.ts +209 -0
- package/src/provenance.ts +2 -2
- package/src/run-event-store.ts +63 -3
- package/src/run-event-stream.ts +8 -4
- package/src/run-event.ts +38 -1
- package/src/workspace/__fixtures__/flag-workspace.ts +132 -0
- package/src/workspace/__fixtures__/older-records/change-set-984534ea9.json +100 -0
- package/src/workspace/__fixtures__/older-records/older-records.json +326 -0
- package/src/workspace/change-set.schema.json +5 -1
- package/src/workspace/check.schema.json +4 -2
- package/src/workspace/checks/records.ts +35 -1
- package/src/workspace/checks.ts +2 -1
- package/src/workspace/ci-green-forge.ts +2 -0
- package/src/workspace/ci-green.ts +20 -5
- package/src/workspace/ci-run.test.ts +416 -0
- package/src/workspace/ci-run.ts +571 -0
- package/src/workspace/ci-serve.test.ts +268 -0
- package/src/workspace/ci-serve.ts +521 -0
- package/src/workspace/declaration.schema.json +64 -0
- package/src/workspace/declaration.test.ts +28 -0
- package/src/workspace/declaration.ts +38 -0
- package/src/workspace/declared-kinds.ts +40 -1
- package/src/workspace/delivery-metrics.schema.json +519 -0
- package/src/workspace/delivery-metrics.test.ts +236 -0
- package/src/workspace/delivery-metrics.ts +286 -0
- package/src/workspace/flags-read.ts +121 -0
- package/src/workspace/flags.test.ts +141 -0
- package/src/workspace/flags.ts +157 -0
- package/src/workspace/graph-cli.ts +1 -1
- package/src/workspace/intent-cli.ts +46 -5
- package/src/workspace/intent-trace.test.ts +173 -0
- package/src/workspace/intent-trace.ts +484 -0
- package/src/workspace/intent.schema.json +702 -6
- package/src/workspace/intent.ts +148 -25
- package/src/workspace/older-records.test.ts +412 -0
- package/src/workspace/overlap.test.ts +165 -0
- package/src/workspace/overlap.ts +340 -0
- package/src/workspace/pr-report.schema.json +5 -0
- package/src/workspace/read-contract.e2e.test.ts +2 -1
- package/src/workspace/reason-codes.test.ts +5 -0
- package/src/workspace/reason-codes.ts +3 -0
- package/src/workspace/record-assets.test.ts +2 -1
- package/src/workspace/records-contract.e2e.test.ts +2 -1
- package/src/workspace/records.schema.json +21 -2
- package/src/workspace/records.ts +36 -2
- package/src/workspace/reference-kinds/flag/flag.kind.mjs +32 -0
- package/src/workspace/reference-kinds/flag/flag.schema.json +53 -0
- package/src/workspace/releases.schema.json +123 -1
- package/src/workspace/releases.ts +11 -0
- package/src/workspace/run-event.schema.json +24 -6
- package/src/workspace/status-contract.test.ts +29 -0
- package/src/workspace/status-gates.ts +8 -0
- package/src/workspace/status-mixed.test.ts +152 -0
- package/src/workspace/status-revoked.test.ts +153 -0
- package/src/workspace/status-stewards.ts +3 -0
- package/src/workspace/status.schema.json +210 -2
- package/src/workspace/status.ts +254 -3
- package/src/workspace/wip.ts +11 -2
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Feature flags as workspace records (#4024): the flag kind's records on
|
|
3
|
+
* read, the stale-flag check WSP118, the flags of a member by parameter, and
|
|
4
|
+
* `status --json`'s `featureFlags`, on the fixture in
|
|
5
|
+
* `__fixtures__/flag-workspace.ts`.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
import { afterAll, beforeAll, describe, expect, test } from "vitest";
|
|
10
|
+
import { cleanScratch, contract } from "./__fixtures__/contract-repo";
|
|
11
|
+
import { flagWorkspace, flagRecord } from "./__fixtures__/flag-workspace";
|
|
12
|
+
import { runDeclarationChecks } from "./checks";
|
|
13
|
+
import { memberFlagBindings, readWorkspaceFlags } from "./flags-read";
|
|
14
|
+
import { loadRecordKind } from "./records";
|
|
15
|
+
import { queryRecords } from "./records-cli";
|
|
16
|
+
import recordsSchema from "./records.schema.json";
|
|
17
|
+
import { workspaceStatus } from "./status";
|
|
18
|
+
import statusSchema from "./status.schema.json";
|
|
19
|
+
|
|
20
|
+
let root: string;
|
|
21
|
+
beforeAll(() => {
|
|
22
|
+
root = flagWorkspace();
|
|
23
|
+
});
|
|
24
|
+
afterAll(cleanScratch);
|
|
25
|
+
|
|
26
|
+
const KIND = "flags/flag.kind.mjs";
|
|
27
|
+
|
|
28
|
+
describe("the flag kind (#4024)", () => {
|
|
29
|
+
test("the canonical kind chant ships loads, with its flag block and schema", async () => {
|
|
30
|
+
const loaded = await loadRecordKind(join(import.meta.dirname, "reference-kinds", "flag", "flag.kind.mjs"));
|
|
31
|
+
expect(loaded.kind.name).toBe("flag");
|
|
32
|
+
expect(loaded.kind.flag).toEqual({ member: "member", param: "param", owner: "owner", values: "values", work: { field: "work", kind: "../work/work.kind.mjs" } });
|
|
33
|
+
expect(loaded.schema.$id).toBe("urn:intentius:chant:flag:1");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("a kind with a flag block and no idField is refused", async () => {
|
|
37
|
+
const dir = flagWorkspace({
|
|
38
|
+
[KIND]: `export const recordKind = { name: "flag", location: { dir: ".", match: "^x$" }, format: "json", schema: { id: "urn:intentius:chant:flag:1", path: "flag.schema.json" }, idFrom: "sha256", flag: { member: "m", param: "p", owner: "o", values: "v", work: { field: "w", kind: "../work/work.kind.mjs" } } };\n`,
|
|
39
|
+
});
|
|
40
|
+
await expect(loadRecordKind(join(dir, KIND))).rejects.toThrow(/a flag kind names its id with idField/);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("records gives each flag its work item, the decisions it implements, and whether it is stale", async () => {
|
|
44
|
+
const doc = await queryRecords({ kind: join(root, KIND), cwd: root, workGaps: false });
|
|
45
|
+
contract(recordsSchema).expectValid(doc);
|
|
46
|
+
if ("error" in doc || !("records" in doc)) throw new Error(JSON.stringify(doc));
|
|
47
|
+
const byId = new Map(doc.records.map((r) => [r.id, r]));
|
|
48
|
+
expect(byId.get("old-banner")).toMatchObject({
|
|
49
|
+
valid: true,
|
|
50
|
+
flag: { work: { id: "W-001", state: "done" }, implements: [{ id: "d-001", state: "confirmed", confirmed: true }], stale: true },
|
|
51
|
+
});
|
|
52
|
+
expect(byId.get("old-banner")!.warnings.map((w) => w.code)).toEqual(["flag-stale"]);
|
|
53
|
+
expect(byId.get("new-checkout")).toMatchObject({
|
|
54
|
+
valid: true,
|
|
55
|
+
flag: { work: { id: "W-002", state: "in-progress" }, implements: [{ id: "d-002", state: "decided", confirmed: false }], stale: false },
|
|
56
|
+
});
|
|
57
|
+
expect(byId.get("new-checkout")!.warnings).toEqual([]);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("a flag that names no known work item is warned flag-work-unknown and is never stale", async () => {
|
|
61
|
+
const dir = flagWorkspace({ "flags/ghost.flag.md": flagRecord("ghost", { work: "W-404", member: "shop", param: "ghost", values: {} }) });
|
|
62
|
+
const doc = await queryRecords({ kind: join(dir, KIND), cwd: dir, workGaps: false });
|
|
63
|
+
if ("error" in doc || !("records" in doc)) throw new Error(JSON.stringify(doc));
|
|
64
|
+
const ghost = doc.records.find((r) => r.id === "ghost")!;
|
|
65
|
+
expect(ghost.flag).toEqual({ work: { id: "W-404", state: null }, implements: [], stale: false });
|
|
66
|
+
expect(ghost.warnings.map((w) => w.code)).toEqual(["flag-work-unknown"]);
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("a superseded decision is not confirmed, so its flag is not stale", async () => {
|
|
70
|
+
const dir = flagWorkspace({
|
|
71
|
+
"decisions/d-001-the-banner.md": `---\nid: "d-001"\nstate: "confirmed"\nsupersedes: []\n---\n`,
|
|
72
|
+
"decisions/d-003-a-new-banner.md": `---\nid: "d-003"\nstate: "confirmed"\nsupersedes: ["d-001"]\n---\n`,
|
|
73
|
+
});
|
|
74
|
+
const { flags } = await readWorkspaceFlags(dir);
|
|
75
|
+
expect(flags.find((f) => f.id === "old-banner")).toMatchObject({ implements: [{ id: "d-001", confirmed: false }], stale: false });
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe("WSP118, the stale-flag check (#4024)", () => {
|
|
80
|
+
test("reports the flag whose work item's decisions are all confirmed, on its record, as a warning", async () => {
|
|
81
|
+
const result = await runDeclarationChecks(root);
|
|
82
|
+
const found = result.diagnostics.filter((d) => d.ruleId === "WSP118");
|
|
83
|
+
expect(found).toHaveLength(1);
|
|
84
|
+
expect(found[0]).toMatchObject({ severity: "warning", code: "flag-stale", file: "flags/old-banner.flag.md" });
|
|
85
|
+
expect(found[0].message).toMatch(/^flag old-banner is due for removal: its work item W-001 implements d-001 \(confirmed\)/);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("reports nothing once the stale flag is removed", async () => {
|
|
89
|
+
const dir = flagWorkspace({ "flags/old-banner.flag.md": null });
|
|
90
|
+
const result = await runDeclarationChecks(dir);
|
|
91
|
+
expect(result.diagnostics.filter((d) => d.ruleId === "WSP118")).toEqual([]);
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
describe("a member's flags by parameter", () => {
|
|
96
|
+
test("names the flag and its value in the environment, and none where the record gives none", async () => {
|
|
97
|
+
expect(await memberFlagBindings(root, join(root, "shop"), "prod")).toEqual({
|
|
98
|
+
member: "shop",
|
|
99
|
+
byParam: { newCheckout: { flag: "new-checkout", value: false } },
|
|
100
|
+
errors: [],
|
|
101
|
+
});
|
|
102
|
+
expect((await memberFlagBindings(root, join(root, "shop"), "staging"))!.byParam).toEqual({ newCheckout: { flag: "new-checkout", value: undefined } });
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("is null for a directory no member holds", async () => {
|
|
106
|
+
expect(await memberFlagBindings(root, join(root, "flags"), "prod")).toBeNull();
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test("two flags for one parameter, and an invalid flag record naming the member, are errors", async () => {
|
|
110
|
+
const dir = flagWorkspace({
|
|
111
|
+
"flags/twin.flag.md": flagRecord("twin", { work: "W-002", member: "shop", param: "newCheckout", values: { prod: true } }),
|
|
112
|
+
"flags/broken.flag.md": `---\nschema: 1\nid: "broken"\nmember: "shop"\n---\n`,
|
|
113
|
+
});
|
|
114
|
+
const bindings = await memberFlagBindings(dir, join(dir, "shop"), "prod");
|
|
115
|
+
expect(bindings!.errors.join("\n")).toMatch(/flags new-checkout and twin both set build parameter "newCheckout" of member shop/);
|
|
116
|
+
expect(bindings!.errors.join("\n")).toMatch(/flag record flags\/broken\.flag\.md is not valid/);
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
describe("status --json lists featureFlags per member (#4024)", () => {
|
|
121
|
+
test("each flag with its values per environment, its work item and whether it is stale", async () => {
|
|
122
|
+
const doc = await workspaceStatus({ cwd: root, env: "prod", readLedger: async () => ({ records: [], malformed: 0 }), readPromotionOrder: async () => [] });
|
|
123
|
+
contract(statusSchema).expectValid(doc);
|
|
124
|
+
if ("error" in doc) throw new Error(JSON.stringify(doc));
|
|
125
|
+
const shop = doc.members.find((m) => m.name === "shop")!;
|
|
126
|
+
expect(shop.featureFlags).toEqual([
|
|
127
|
+
{
|
|
128
|
+
id: "new-checkout",
|
|
129
|
+
param: "newCheckout",
|
|
130
|
+
owner: "alice",
|
|
131
|
+
values: { dev: true, prod: false },
|
|
132
|
+
work: { id: "W-002", state: "in-progress" },
|
|
133
|
+
implements: [{ id: "d-002", state: "decided", confirmed: false }],
|
|
134
|
+
stale: false,
|
|
135
|
+
kind: "flags/flag.kind.mjs",
|
|
136
|
+
path: "flags/new-checkout.flag.md",
|
|
137
|
+
},
|
|
138
|
+
]);
|
|
139
|
+
expect(doc.members.find((m) => m.name === "blog")!.featureFlags!.map((f) => [f.id, f.stale])).toEqual([["old-banner", true]]);
|
|
140
|
+
});
|
|
141
|
+
});
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Feature flags as workspace records (#4024, decided in #4016).
|
|
3
|
+
*
|
|
4
|
+
* A flag is a record of a kind with a `flag` block (`records.ts`). It names a
|
|
5
|
+
* member, one of that member's declared build-time parameters, an owner, the
|
|
6
|
+
* work item the flag hides and a value per environment. The member's build
|
|
7
|
+
* reads it as that parameter (`../flag-params.ts`), so turning a flag on in
|
|
8
|
+
* an environment is a commit that changes the record, and the release cut
|
|
9
|
+
* from that commit carries the new value as a `params` entry whose `source`
|
|
10
|
+
* is `flag:<id>` (ws-104).
|
|
11
|
+
*
|
|
12
|
+
* This module and `./flags-read.ts` do three things:
|
|
13
|
+
*
|
|
14
|
+
* - {@link applyFlags}: on a `records` read of a flag kind, gives each record
|
|
15
|
+
* its work item with that item's state, the decisions the item implements,
|
|
16
|
+
* and whether the flag is due for removal, with the warnings
|
|
17
|
+
* `flag-work-unknown` and `flag-stale`.
|
|
18
|
+
* - `readWorkspaceFlags` (`./flags-read.ts`): every flag of every flag kind
|
|
19
|
+
* the declaration names, for `workspace status` (`featureFlags`) and the
|
|
20
|
+
* build.
|
|
21
|
+
* - `memberFlagBindings` (`./flags-read.ts`): the flags of the member holding
|
|
22
|
+
* a directory, by parameter, for one environment.
|
|
23
|
+
*
|
|
24
|
+
* The readers live apart so that `records.ts`, which loads this module for a
|
|
25
|
+
* flag kind, never reaches `records-cli.ts` and what it imports.
|
|
26
|
+
*
|
|
27
|
+
* A flag is due for removal when its work item implements at least one
|
|
28
|
+
* decision and every decision it implements is confirmed: in one of its
|
|
29
|
+
* kind's closed states and superseded by nothing. For chant's decision kind
|
|
30
|
+
* that is `ratified` (a superseded decision has a superseder). The work is
|
|
31
|
+
* then settled, and the flag that hid it is dead weight.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
import { dirname } from "node:path";
|
|
35
|
+
import type { ReasonCode } from "./reason-codes";
|
|
36
|
+
import { loadRecordKind, readRecords, RecordReadError, type LoadedRecordKind, type ReadRecordsOptions, type RecordEntry } from "./records";
|
|
37
|
+
import type { WorkLink } from "./work";
|
|
38
|
+
|
|
39
|
+
/** Why a flag record carries a warning. Closed, like the record warning codes. */
|
|
40
|
+
export const FLAG_WARNING_CODES = [
|
|
41
|
+
/** The flag names a work item no record of its kind's work kind has. */
|
|
42
|
+
"flag-work-unknown",
|
|
43
|
+
/** Every decision the flag's work item implements is confirmed, so the flag is due for removal. `check` reports it (WSP118). */
|
|
44
|
+
"flag-stale",
|
|
45
|
+
] as const satisfies readonly ReasonCode[];
|
|
46
|
+
export type FlagWarningCode = (typeof FLAG_WARNING_CODES)[number];
|
|
47
|
+
|
|
48
|
+
/** A flag's value in one environment: what a build-time parameter can hold. */
|
|
49
|
+
export type FlagValue = string | number | boolean;
|
|
50
|
+
|
|
51
|
+
/** A decision a flag's work item implements, with whether it is confirmed. */
|
|
52
|
+
export interface FlagDecision extends WorkLink {
|
|
53
|
+
/** In one of its kind's closed states and superseded by nothing. */
|
|
54
|
+
confirmed: boolean;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** What a read of a flag kind adds to each record (#4024). */
|
|
58
|
+
export interface FlagState {
|
|
59
|
+
/** The work item the flag hides, with its state, or null when the record names none. The state is null when no record has the id. */
|
|
60
|
+
work: WorkLink | null;
|
|
61
|
+
/** Each decision the work item implements, with its state. Empty when the item is unknown or implements none. */
|
|
62
|
+
implements: FlagDecision[];
|
|
63
|
+
/** True when the work item implements at least one decision and every one is confirmed: the flag is due for removal. */
|
|
64
|
+
stale: boolean;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** A flag as `workspace status` and the build read it. */
|
|
68
|
+
export interface WorkspaceFlag {
|
|
69
|
+
/** The flag's name, its record's id. */
|
|
70
|
+
id: string;
|
|
71
|
+
/** The kind file from the workspace root. */
|
|
72
|
+
kind: string;
|
|
73
|
+
/** The record file from the workspace root, with / separators. */
|
|
74
|
+
path: string;
|
|
75
|
+
member: string;
|
|
76
|
+
param: string;
|
|
77
|
+
owner: string;
|
|
78
|
+
/** The value per environment, as the record writes it, in key order. */
|
|
79
|
+
values: Record<string, FlagValue>;
|
|
80
|
+
work: WorkLink | null;
|
|
81
|
+
implements: FlagDecision[];
|
|
82
|
+
stale: boolean;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** A flag record that is not valid, so no build trusts it. */
|
|
86
|
+
export interface InvalidFlag {
|
|
87
|
+
kind: string;
|
|
88
|
+
path: string;
|
|
89
|
+
/** The member the record names, or null when it names none that can be read. */
|
|
90
|
+
member: string | null;
|
|
91
|
+
message: string;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Every flag a workspace's declared flag kinds hold. */
|
|
95
|
+
export interface WorkspaceFlags {
|
|
96
|
+
flags: WorkspaceFlag[];
|
|
97
|
+
invalid: InvalidFlag[];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const str = (v: unknown): string | null => (typeof v === "string" && v !== "" ? v : null);
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Whether a decision is confirmed: in one of its kind's `closed` states and
|
|
104
|
+
* superseded by nothing. An unknown decision is not. The overlap report
|
|
105
|
+
* (#4025) shares it: a work item is in flight while any decision it
|
|
106
|
+
* implements is not confirmed.
|
|
107
|
+
*/
|
|
108
|
+
export function decisionConfirmed(decision: { state: string | null; supersededBy: string | null } | undefined, closed: ReadonlySet<string>): boolean {
|
|
109
|
+
return decision !== undefined && decision.state !== null && closed.has(decision.state) && decision.supersededBy === null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Give each record of a flag kind its {@link FlagState} and warnings, in
|
|
114
|
+
* place. Throws a `RecordReadError` when the work kind it names can't be read
|
|
115
|
+
* or is not a work kind.
|
|
116
|
+
*/
|
|
117
|
+
export async function applyFlags(loaded: LoadedRecordKind, entries: RecordEntry[], options: ReadRecordsOptions): Promise<void> {
|
|
118
|
+
const flag = loaded.kind.flag!;
|
|
119
|
+
const workKind = await loadRecordKind(flag.work.kind, dirname(loaded.file));
|
|
120
|
+
if (!workKind.kind.work) {
|
|
121
|
+
throw new RecordReadError("kind-invalid", `flag kind ${loaded.kind.name} names ${flag.work.kind} as its work kind, and that kind has no work block`);
|
|
122
|
+
}
|
|
123
|
+
const decisionKind = await loadRecordKind(workKind.kind.work.decisions, dirname(workKind.file));
|
|
124
|
+
const read = await readRecords(workKind, { root: options.root, source: options.source });
|
|
125
|
+
const items = new Map<string, RecordEntry>();
|
|
126
|
+
for (const r of read.records) if (r.id !== null && !items.has(r.id)) items.set(r.id, r);
|
|
127
|
+
const decisions = new Map((read.decisions ?? []).map((d) => [d.id, d]));
|
|
128
|
+
const closed = new Set(decisionKind.kind.closedStates ?? []);
|
|
129
|
+
|
|
130
|
+
for (const e of entries) {
|
|
131
|
+
if (e.data === null) continue;
|
|
132
|
+
const named = str(e.data[flag.work.field]);
|
|
133
|
+
if (named === null) {
|
|
134
|
+
e.flag = { work: null, implements: [], stale: false };
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
const item = items.get(named);
|
|
138
|
+
if (!item) {
|
|
139
|
+
e.flag = { work: { id: named, state: null }, implements: [], stale: false };
|
|
140
|
+
e.warnings.push({ code: "flag-work-unknown", message: `hides the work item ${named}, which no record in ${flag.work.kind} has` });
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
const implementsList: FlagDecision[] = (item.implements ?? []).map((d) => {
|
|
144
|
+
const decision = decisions.get(d.id);
|
|
145
|
+
return { ...d, confirmed: decisionConfirmed(decision, closed) };
|
|
146
|
+
});
|
|
147
|
+
const stale = implementsList.length > 0 && implementsList.every((d) => d.confirmed);
|
|
148
|
+
e.flag = { work: { id: named, state: item.state }, implements: implementsList, stale };
|
|
149
|
+
if (stale) {
|
|
150
|
+
e.warnings.push({
|
|
151
|
+
code: "flag-stale",
|
|
152
|
+
message: `is due for removal: its work item ${named} implements ${implementsList.map((d) => `${d.id} (${d.state})`).join(", ")}, and every one is confirmed`,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
@@ -60,7 +60,7 @@ export type GraphErrorCode = (typeof GRAPH_ERROR_CODES)[number];
|
|
|
60
60
|
const LIVE_AT_REVISION = "--live reads the account as it stands now, and --at reads the source at a revision; chant workspace graph takes one of them";
|
|
61
61
|
|
|
62
62
|
const USAGE =
|
|
63
|
-
"chant workspace graph [dir] [--at <rev>] [--member <name>] [--kind <kind file>] [-o <file>] [--env <env>] [--live [--overlay] [--traffic <level>]] [--no-cache] [--dry-run] | chant workspace graph --composites [--at <rev>] [--member <name>] [-o <file>] | chant workspace graph --intent <path[:start-end]|path#symbol> [--at <rev>] [--kind <kind file>...] [--follow-squash] [--json]";
|
|
63
|
+
"chant workspace graph [dir] [--at <rev>] [--member <name>] [--kind <kind file>] [-o <file>] [--env <env>] [--live [--overlay] [--traffic <level>]] [--no-cache] [--dry-run] | chant workspace graph --composites [--at <rev>] [--member <name>] [-o <file>] | chant workspace graph --intent <path[:start-end]|path#symbol|member/node|sha> [--at <rev>] [--kind <kind file>...] [--follow-squash] [--json]";
|
|
64
64
|
|
|
65
65
|
interface Head {
|
|
66
66
|
$schema: string;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `chant workspace graph --intent <path[:start-end]|path#symbol> [--at <rev>] [--kind <kind file>...] [--follow-squash] [--json]`
|
|
2
|
+
* `chant workspace graph --intent <path[:start-end]|path#symbol|member/node|sha> [--at <rev>] [--kind <kind file>...] [--follow-squash] [--json]`
|
|
3
3
|
* (#2651, #3034): the intent graph over one region (`intent.ts`), printed as JSON
|
|
4
4
|
* with `--json` or as a walk, one line per node, in the order of #2650
|
|
5
5
|
* section B: the region, its decisions, their artifacts, the commits, and the
|
|
@@ -7,16 +7,19 @@
|
|
|
7
7
|
* the decisions governing it, most relevant first. Under each decision come the commits made inside its window, and
|
|
8
8
|
* when any of them is not the decision's own work, the question #2650 B puts
|
|
9
9
|
* to the person about it (#2656). Without `--kind`, the walk reads every
|
|
10
|
-
* record kind the declaration names (#2680).
|
|
10
|
+
* record kind the declaration names (#2680). Last comes the why chain (#4023):
|
|
11
|
+
* each commit's work item, ci tag and release, and each release's gates and
|
|
12
|
+
* deploys.
|
|
11
13
|
*/
|
|
12
14
|
|
|
13
15
|
import { resolve } from "node:path";
|
|
14
16
|
import { formatError } from "../cli/format";
|
|
15
17
|
import type { CommandContext } from "../cli/registry";
|
|
16
18
|
import { intentRecord, type IntentRecordDocument } from "./intent-record";
|
|
19
|
+
import type { IntentTrace, TraceGate } from "./intent-trace";
|
|
17
20
|
import { intentGraph, type ArtifactNode, type CommitNode, type DecisionNode, type IntentDocument, type IntentEdge, type IntentNode, type RunNode, type WhyAnswer, type WorkNode } from "./intent";
|
|
18
21
|
|
|
19
|
-
const USAGE = "chant workspace graph --intent <path[:start-end]|path#symbol> [--at <rev>] [--kind <kind file>...] [--follow-squash] [--json]";
|
|
22
|
+
const USAGE = "chant workspace graph --intent <path[:start-end]|path#symbol|member/node|sha> [--at <rev>] [--kind <kind file>...] [--follow-squash] [--json]";
|
|
20
23
|
const RECORD_USAGE = "chant workspace graph --intent --record <id> [--at <rev>] [--kind <kind file>...] [--follow-squash] [--json]";
|
|
21
24
|
|
|
22
25
|
type Result = Exclude<IntentDocument, { error: unknown }>;
|
|
@@ -162,6 +165,42 @@ export function whyLines(doc: Result, why: WhyAnswer): string[] {
|
|
|
162
165
|
return out;
|
|
163
166
|
}
|
|
164
167
|
|
|
168
|
+
const gateLabel = (g: TraceGate) =>
|
|
169
|
+
`${g.op}/${g.gate}${g.environment ? ` ${g.environment}` : ""}: ${g.empty ? "passed over an empty change set" : `${g.resolvedBy}${g.relayedBy ? ` relayed by ${g.relayedBy}` : ""}`}${g.channel ? ` on ${g.channel}` : ""} at ${g.at}${g.planDigest ? `, plan ${g.planDigest.slice(0, 24)}${g.changeSet ? " (change set stored)" : ""}` : ""}${g.sealed ? ", sealed" : ""}`;
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* The why chain (#4023), in its order: for each commit that names a work item
|
|
173
|
+
* or was tagged, released or deployed, a line; then each release with its
|
|
174
|
+
* cuts, gates and deploys, and the deploys of no release.
|
|
175
|
+
*/
|
|
176
|
+
export function traceLines(trace: IntentTrace): string[] {
|
|
177
|
+
const out: string[] = [];
|
|
178
|
+
const label = (digest: string) => trace.releases.find((r) => r.releaseDigest === digest)?.label ?? digest.slice(0, 24);
|
|
179
|
+
for (const c of trace.commits) {
|
|
180
|
+
const parts: string[] = [];
|
|
181
|
+
if (c.work && c.work.state !== "unlinked") parts.push(c.work.state === "linked" ? `work ${c.work.items.map((w) => `${w.id} (${w.source.field})`).join(", ") || "by a lease not checked"}` : `work ${c.work.state}`);
|
|
182
|
+
if (c.windowMatches.length > 0) parts.push(`in the window of ${c.windowMatches.map((w) => w.id).join(", ")}`);
|
|
183
|
+
if (c.tag.state !== "none") parts.push(c.tag.state);
|
|
184
|
+
for (const r of c.releases) parts.push(`${r.link === "cut-from" ? "released as" : "shipped in"} ${label(r.release)}`);
|
|
185
|
+
for (const d of c.deploys) parts.push(`${d.env} since ${trace.deploys.find((x) => x.id === d.deploy)?.timestamp ?? "?"}`);
|
|
186
|
+
if (parts.length > 0) out.push(`trace ${short(c.sha)} ${parts.join("; ")}`);
|
|
187
|
+
}
|
|
188
|
+
for (const r of trace.releases) {
|
|
189
|
+
const cut = r.cuts[0];
|
|
190
|
+
out.push(`release ${r.label ?? r.releaseDigest.slice(0, 24)} from ${short(r.commit)}, ${r.tag.state}${cut ? `; cut by ${cut.actor} at ${cut.timestamp}${cut.signed ? ", signed" : ""}` : ""}`);
|
|
191
|
+
for (const g of r.gates) out.push(` gate ${gateLabel(g)}`);
|
|
192
|
+
for (const id of r.deploys) {
|
|
193
|
+
const d = trace.deploys.find((x) => x.id === id);
|
|
194
|
+
if (d) out.push(` deploy ${d.env} ${d.component} at ${d.timestamp} by ${d.actor}${d.admittedBy ? `, admitted by ${d.admittedBy.op}/${d.admittedBy.gate} (${d.admittedBy.resolvedBy})` : ""}`);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
for (const d of trace.deploys.filter((x) => x.release === null)) {
|
|
198
|
+
out.push(`deploy ${d.env} ${d.component} at ${d.timestamp} by ${d.actor} from ${short(d.gitSha)}, no release${d.admittedBy ? `; admitted by ${gateLabel(d.admittedBy)}` : ""}${d.plan ? `; plan ${d.plan.document.digest.slice(0, 24)}` : ""}`);
|
|
199
|
+
}
|
|
200
|
+
for (const x of trace.reasons) out.push(`reason ${x.code}: ${x.message}`);
|
|
201
|
+
return out;
|
|
202
|
+
}
|
|
203
|
+
|
|
165
204
|
/** The walk as text, one line each, in the order of #2650 section B. */
|
|
166
205
|
export function formatIntent(doc: Result): string {
|
|
167
206
|
const out: string[] = [];
|
|
@@ -170,7 +209,8 @@ export function formatIntent(doc: Result): string {
|
|
|
170
209
|
const lines = region.lines ? `:${region.lines.start}${region.lines.end === region.lines.start ? "" : `-${region.lines.end}`}` : "";
|
|
171
210
|
const where = doc.at ? `at ${short(doc.at)}` : "in the working tree";
|
|
172
211
|
const symbol = region.symbol ? `#${region.symbol.name} (${region.symbol.kind} ${region.symbol.qualified}, lines ${region.lines ? span(region.lines) : "none"})` : lines;
|
|
173
|
-
out.push(`region ${region.
|
|
212
|
+
if (region.commit) out.push(`region commit ${short(region.commit)} (member ${region.member ?? "none"}) ${doc.at ? `at ${short(doc.at)}` : "in the working tree"}`);
|
|
213
|
+
else out.push(`region ${region.path}${symbol} (${region.type}, member ${region.member ?? "none"}${region.generated ? ", generated" : ""}) ${where}${region.node ? `, from node ${region.node}` : ""}`);
|
|
174
214
|
}
|
|
175
215
|
const files = ofKind(doc, "file");
|
|
176
216
|
if (files.length > 0) out.push(`files ${files.length} under the region, ${files.filter((f) => f.generated).length} generated`);
|
|
@@ -188,6 +228,7 @@ export function formatIntent(doc: Result): string {
|
|
|
188
228
|
}
|
|
189
229
|
for (const r of doc.reasons) out.push(`reason ${r.code}: ${r.message}`);
|
|
190
230
|
if (doc.why) out.push(...whyLines(doc, doc.why));
|
|
231
|
+
if (doc.trace) out.push(...traceLines(doc.trace));
|
|
191
232
|
const kinds = doc.kinds.length === 0 ? "; no --kind, so no decisions were read" : "";
|
|
192
233
|
const work = ofKind(doc, "work").length;
|
|
193
234
|
out.push(`${doc.summary.commits} commits, ${doc.summary.decisions} decisions, ${work > 0 ? `${work} work ${work === 1 ? "item" : "items"}, ` : ""}${doc.summary.artifacts} artifacts, ${doc.summary.findings} findings${kinds}`);
|
|
@@ -243,7 +284,7 @@ export async function runWorkspaceIntent(ctx: CommandContext, cwd: string): Prom
|
|
|
243
284
|
return runRecord(ctx, cwd, args.kinds ?? (args.kind !== undefined ? [args.kind] : undefined));
|
|
244
285
|
}
|
|
245
286
|
if (!args.intent) {
|
|
246
|
-
console.error(formatError({ message: "--intent needs a region: a path, path:line, path:start-end
|
|
287
|
+
console.error(formatError({ message: "--intent needs a region: a path, path:line, path:start-end, path#symbol, a graph node id or a commit sha", hint: USAGE }));
|
|
247
288
|
return 1;
|
|
248
289
|
}
|
|
249
290
|
// No --kind: undefined, so the walk reads the kinds the declaration names (#2680).
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The why chain of `graph --intent` (#4023), over a workspace in a throwaway
|
|
3
|
+
* git repository whose `chant/lifecycle` ledgers are written through the
|
|
4
|
+
* library, so the test needs no deploy. The end-to-end chain, with a real
|
|
5
|
+
* cut, a gated `components promote --release` and an approval, is in
|
|
6
|
+
* cli/handlers/intent-trace.e2e.test.ts.
|
|
7
|
+
*
|
|
8
|
+
* - c1 adds the workspace, the work kind, W-001 (constrains app/server.mjs)
|
|
9
|
+
* and the server.
|
|
10
|
+
* - c2 changes the server with `Chant-Record: work:W-001`: linked, green, and
|
|
11
|
+
* release v1 is cut from it and deployed to prod through the release gate.
|
|
12
|
+
* - c3 changes the server with no trailer: unlinked, in W-001's window, and
|
|
13
|
+
* revoked. A plain deploy of it reaches dev through a component gate, with
|
|
14
|
+
* a release plan in `_plans/`.
|
|
15
|
+
* - c4 changes the README: green, release v2 is cut from it, so v2 contains
|
|
16
|
+
* c2 and c3. Its staging deploy names no releaseDigest and joins v2
|
|
17
|
+
* through its release gate admission.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { readFileSync } from "node:fs";
|
|
21
|
+
import { join } from "node:path";
|
|
22
|
+
import { afterAll, beforeAll, describe, expect, test } from "vitest";
|
|
23
|
+
import { appendGateResolution } from "../lifecycle/gate-ledger";
|
|
24
|
+
import { persistReleasePlan } from "../lifecycle/plan-ledger";
|
|
25
|
+
import { appendReleaseRecord } from "../lifecycle/release-ledger";
|
|
26
|
+
import { appendCut, makeRelease, persistRelease, type Release } from "../lifecycle/release-store";
|
|
27
|
+
import { cleanScratch, contract, git, REPO, repo, writeFiles } from "./__fixtures__/contract-repo";
|
|
28
|
+
import { intentGraph, type IntentDocument } from "./intent";
|
|
29
|
+
import intentSchema from "./intent.schema.json";
|
|
30
|
+
import { traceLines } from "./intent-cli";
|
|
31
|
+
|
|
32
|
+
const REF = join(REPO, "reference-workspace");
|
|
33
|
+
const KIND_FILES = {
|
|
34
|
+
"work/work.kind.mjs": readFileSync(join(REF, "work", "work.kind.mjs"), "utf-8"),
|
|
35
|
+
"work/work.schema.json": readFileSync(join(REF, "work", "work.schema.json"), "utf-8"),
|
|
36
|
+
"decisions/decision.kind.mjs": readFileSync(join(REF, "decisions", "decision.kind.mjs"), "utf-8"),
|
|
37
|
+
"decisions/decision.schema.json": readFileSync(join(REF, "decisions", "decision.schema.json"), "utf-8"),
|
|
38
|
+
};
|
|
39
|
+
const W001 = `---\n${JSON.stringify({ schema: 1, id: "W-001", title: "Serve on 8081", state: "open", implements: [], needs: [], constrains: ["path:app/server.mjs"], evidence: [], opened_on: "2026-10-16", supersedes: [] }, null, 2)}\n---\n\n# W-001\n`;
|
|
40
|
+
const DECLARATION = JSON.stringify({ name: "shop", schema: 1, members: [{ name: "app", dir: "app", kind: "other", because: "a plain Node server" }] });
|
|
41
|
+
const PLAN = `jcs1-sha256:${"b".repeat(64)}`;
|
|
42
|
+
const DEV_PLAN = `sha256:${"0123456789abcdef".repeat(4)}`;
|
|
43
|
+
|
|
44
|
+
let root: string;
|
|
45
|
+
let app: string;
|
|
46
|
+
const sha: Record<string, string> = {};
|
|
47
|
+
let v1: Release;
|
|
48
|
+
let v2: Release;
|
|
49
|
+
|
|
50
|
+
function commit(name: string, message: string): void {
|
|
51
|
+
git(root, "add", "-A");
|
|
52
|
+
git(root, "commit", "-q", "-m", message);
|
|
53
|
+
sha[name] = git(root, "rev-parse", "HEAD");
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const subject = (commitSha: string) => ({ member: "app", commit: commitSha, artifactDigests: [{ component: "api", kind: "image" as const, path: "api.tar", digest: `sha256:${commitSha.slice(0, 8).padEnd(64, "0")}` }], targets: [{ env: "prod", templates: [], params: [] }] });
|
|
57
|
+
|
|
58
|
+
beforeAll(async () => {
|
|
59
|
+
root = repo({ "chant.workspace.json": DECLARATION, "app/server.mjs": "export const port = 8080;\n", "README.md": "shop\n", "work/W-001-port.md": W001, ...KIND_FILES });
|
|
60
|
+
app = join(root, "app");
|
|
61
|
+
commit("c1", "the shop and W-001");
|
|
62
|
+
writeFiles(root, { "app/server.mjs": "export const port = 8081;\n" });
|
|
63
|
+
commit("c2", "serve on 8081\n\nChant-Record: work:W-001");
|
|
64
|
+
writeFiles(root, { "app/server.mjs": "export const port = 8081;\nexport const host = '0.0.0.0';\n" });
|
|
65
|
+
commit("c3", "listen on every interface");
|
|
66
|
+
writeFiles(root, { "README.md": "shop, on 8081\n" });
|
|
67
|
+
commit("c4", "say the port");
|
|
68
|
+
git(root, "tag", `ci/green/${sha.c2}`, sha.c2);
|
|
69
|
+
git(root, "tag", `ci/revoked/${sha.c3}`, sha.c3);
|
|
70
|
+
git(root, "tag", `ci/green/${sha.c4}`, sha.c4);
|
|
71
|
+
|
|
72
|
+
const loc = { cwd: app };
|
|
73
|
+
v1 = makeRelease(subject(sha.c2));
|
|
74
|
+
v2 = makeRelease(subject(sha.c4));
|
|
75
|
+
for (const [r, label, at] of [[v1, "v1", "2026-10-16T10:00:00.000Z"], [v2, "v2", "2026-10-16T12:00:00.000Z"]] as const) {
|
|
76
|
+
await persistRelease(r, [], loc);
|
|
77
|
+
await appendCut({ releaseDigest: r.releaseDigest, label, actor: "alice", timestamp: at, runOrigin: null }, loc);
|
|
78
|
+
}
|
|
79
|
+
await appendGateResolution({ op: "release", gate: "prod-release", resolvedBy: "bob", origin: "cli", timestamp: "2026-10-16T10:30:00.000Z", planDigest: PLAN, environment: "prod", releaseDigest: v1.releaseDigest }, loc);
|
|
80
|
+
await appendGateResolution({ op: "release", gate: "staging-release", resolvedBy: "carol", origin: "mcp", timestamp: "2026-10-16T12:30:00.000Z", planDigest: PLAN, environment: "staging", releaseDigest: v2.releaseDigest }, loc);
|
|
81
|
+
await appendGateResolution({ op: "api", gate: "dev-gate", resolvedBy: "dave", timestamp: "2026-10-16T11:00:00.000Z", planDigest: DEV_PLAN, environment: "dev" }, loc);
|
|
82
|
+
await persistReleasePlan({ digest: DEV_PLAN, work: ["W-001"] }, loc);
|
|
83
|
+
const deploy = (env: string, gitSha: string, runId: string, timestamp: string, extra: Record<string, unknown>) =>
|
|
84
|
+
appendReleaseRecord({ component: "api", env, digest: DEV_PLAN, gitSha, runId, timestamp, actor: "ci", ...extra }, loc);
|
|
85
|
+
await deploy("prod", sha.c2, "run-1", "2026-10-16T10:40:00.000Z", { releaseDigest: v1.releaseDigest, approver: "bob", admission: { op: "release", gate: "prod-release", resolvedAt: "2026-10-16T10:30:00.000Z" } });
|
|
86
|
+
await deploy("dev", sha.c3, "run-2", "2026-10-16T11:10:00.000Z", { approver: "dave", admission: { op: "api", gate: "dev-gate", resolvedAt: "2026-10-16T11:00:00.000Z" } });
|
|
87
|
+
await deploy("staging", sha.c4, "run-3", "2026-10-16T12:40:00.000Z", { approver: "carol", admission: { op: "release", gate: "staging-release", resolvedAt: "2026-10-16T12:30:00.000Z" } });
|
|
88
|
+
}, 30_000);
|
|
89
|
+
afterAll(cleanScratch);
|
|
90
|
+
|
|
91
|
+
const intent = contract(intentSchema);
|
|
92
|
+
type Result = Exclude<IntentDocument, { error: unknown }>;
|
|
93
|
+
|
|
94
|
+
async function walk(region: string): Promise<Result> {
|
|
95
|
+
const { doc } = await intentGraph({ cwd: root, region, kinds: ["work/work.kind.mjs", "decisions/decision.kind.mjs"].map((k) => join(root, k)) });
|
|
96
|
+
intent.expectValid(doc);
|
|
97
|
+
if ("error" in doc) throw new Error(`${doc.error.code}: ${doc.error.message}`);
|
|
98
|
+
return doc;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
describe("the why chain (#4023)", () => {
|
|
102
|
+
test("a file region walks each commit to its work item, tag, release, gate and deploys, each link with its source", async () => {
|
|
103
|
+
const doc = await walk("app/server.mjs");
|
|
104
|
+
const trace = doc.trace!;
|
|
105
|
+
expect(trace.reasons).toEqual([]);
|
|
106
|
+
expect(trace.commits.map((c) => c.sha)).toEqual([sha.c3, sha.c2, sha.c1]);
|
|
107
|
+
const [c3, c2] = trace.commits;
|
|
108
|
+
|
|
109
|
+
// Who asked for it: the trailer links c2 to W-001; c3 only falls in its window.
|
|
110
|
+
expect(c2.work).toEqual({ state: "linked", items: [{ id: "work:W-001", node: "record:work/W-001", source: { ref: sha.c2, field: "Chant-Record", value: "work:W-001" } }], problems: [] });
|
|
111
|
+
expect(c3.work).toMatchObject({ state: "unlinked", items: [] });
|
|
112
|
+
expect(c3.windowMatches).toEqual([{ id: "work:W-001", node: "record:work/W-001", source: { ref: "work/W-001-port.md", field: "window", value: "worked" } }]);
|
|
113
|
+
|
|
114
|
+
// The ci tags.
|
|
115
|
+
expect(c2.tag).toMatchObject({ state: "green", green: { tag: `ci/green/${sha.c2}`, source: { ref: `refs/tags/ci/green/${sha.c2}` } }, revoked: null });
|
|
116
|
+
expect(c3.tag).toMatchObject({ state: "revoked", revoked: { tag: `ci/revoked/${sha.c3}` } });
|
|
117
|
+
|
|
118
|
+
// The release cut from c2, and the first release that contains c3.
|
|
119
|
+
expect(c2.releases).toEqual([{ release: v1.releaseDigest, link: "cut-from", source: { ref: `chant/lifecycle:_members/app/_releases/${v1.releaseDigest.replace(/:/g, "_")}.json`, field: "commit", value: sha.c2 } }]);
|
|
120
|
+
expect(c3.releases).toEqual([expect.objectContaining({ release: v2.releaseDigest, link: "contains", source: expect.objectContaining({ value: sha.c4 }) })]);
|
|
121
|
+
|
|
122
|
+
// The first deploy per environment that shipped each commit.
|
|
123
|
+
expect(c2.deploys.map((d) => [d.env, d.link])).toEqual([["dev", "contains"], ["prod", "built-from"], ["staging", "contains"]]);
|
|
124
|
+
expect(c3.deploys.map((d) => [d.env, d.link])).toEqual([["dev", "built-from"], ["staging", "contains"]]);
|
|
125
|
+
|
|
126
|
+
// Who let it ship: the release gate's resolution, with resolver, channel, time and plan.
|
|
127
|
+
const r1 = trace.releases.find((r) => r.releaseDigest === v1.releaseDigest)!;
|
|
128
|
+
expect(r1).toMatchObject({ label: "v1", commit: sha.c2, tag: { state: "green" }, cuts: [{ label: "v1", actor: "alice", signed: false }] });
|
|
129
|
+
expect(r1.gates).toEqual([
|
|
130
|
+
expect.objectContaining({ op: "release", gate: "prod-release", environment: "prod", resolvedBy: "bob", channel: "cli", at: "2026-10-16T10:30:00.000Z", planDigest: PLAN, changeSet: null, source: { ref: "chant/lifecycle:_members/app/_gates/release.jsonl", field: "releaseDigest", value: v1.releaseDigest } }),
|
|
131
|
+
]);
|
|
132
|
+
const prod = trace.deploys.find((d) => d.env === "prod")!;
|
|
133
|
+
expect(r1.deploys).toEqual([prod.id]);
|
|
134
|
+
expect(prod).toMatchObject({ release: v1.releaseDigest, releaseSource: { field: "releaseDigest" }, admittedBy: { gate: "prod-release", resolvedBy: "bob" }, source: { ref: "chant/lifecycle:_members/app/prod/releases.jsonl", field: "runId", value: "run-1" } });
|
|
135
|
+
|
|
136
|
+
// A deploy with no releaseDigest joins its release through its release gate admission (#4043).
|
|
137
|
+
const staging = trace.deploys.find((d) => d.env === "staging")!;
|
|
138
|
+
expect(staging).toMatchObject({ release: v2.releaseDigest, releaseSource: { field: "admission", value: "staging-release@2026-10-16T12:30:00.000Z" }, admittedBy: { resolvedBy: "carol", channel: "mcp" } });
|
|
139
|
+
expect(trace.releases.find((r) => r.releaseDigest === v2.releaseDigest)!.deploys).toEqual([staging.id]);
|
|
140
|
+
|
|
141
|
+
// A plain deploy names no release, and its component gate and release plan (ws-055) are read back.
|
|
142
|
+
const dev = trace.deploys.find((d) => d.env === "dev")!;
|
|
143
|
+
expect(dev).toMatchObject({ release: null, admittedBy: { op: "api", gate: "dev-gate", resolvedBy: "dave", planDigest: DEV_PLAN }, plan: { document: { digest: DEV_PLAN, work: ["W-001"] }, source: { field: "digest" } } });
|
|
144
|
+
|
|
145
|
+
expect(traceLines(trace)).toEqual(
|
|
146
|
+
expect.arrayContaining([
|
|
147
|
+
expect.stringMatching(new RegExp(`^trace ${sha.c2.slice(0, 8)} work work:W-001 \\(Chant-Record\\); in the window of work:W-001; green; released as v1; dev since`)),
|
|
148
|
+
expect.stringMatching(/^release v1 from .*green; cut by alice/),
|
|
149
|
+
expect.stringMatching(/^ gate release\/prod-release prod: bob on cli at 2026-10-16T10:30:00.000Z/),
|
|
150
|
+
expect.stringMatching(/^deploy dev api .* no release; admitted by api\/dev-gate dev: dave/),
|
|
151
|
+
]),
|
|
152
|
+
);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test("a commit sha is a region: the chain of that one commit, from the files it changed", async () => {
|
|
156
|
+
const doc = await walk(sha.c2.slice(0, 10));
|
|
157
|
+
expect(doc.region).toBe(`region:commit:${sha.c2}`);
|
|
158
|
+
expect(doc.nodes[0]).toMatchObject({ kind: "region", type: "commit", path: ".", commit: sha.c2, member: "app" });
|
|
159
|
+
expect(doc.history.follows).toBe("commit");
|
|
160
|
+
expect(doc.trace!.commits.map((c) => [c.sha, c.work?.state, c.tag.state, c.releases.map((r) => r.link)])).toEqual([[sha.c2, "linked", "green", ["cut-from"]]]);
|
|
161
|
+
// W-001 constrains a file the commit changed, so the work node is there and the commit falls in its window.
|
|
162
|
+
expect(doc.trace!.commits[0].windowMatches.map((w) => w.id)).toEqual(["work:W-001"]);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test("a commit no release was cut from is shipped by the first release that contains it", async () => {
|
|
166
|
+
const doc = await walk("README.md");
|
|
167
|
+
expect(doc.trace!.commits.map((c) => [c.sha, c.work?.state, c.tag.state, c.releases.map((r) => [r.release, r.link])])).toEqual([
|
|
168
|
+
[sha.c4, "unlinked", "green", [[v2.releaseDigest, "cut-from"]]],
|
|
169
|
+
[sha.c1, "unlinked", "none", [[v1.releaseDigest, "contains"]]],
|
|
170
|
+
]);
|
|
171
|
+
expect(doc.trace!.commits[1].deploys.map((d) => [d.env, d.link])).toEqual([["dev", "contains"], ["prod", "contains"], ["staging", "contains"]]);
|
|
172
|
+
});
|
|
173
|
+
});
|