@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
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Concurrent writers in one working tree (#3173, ws-089): two people and an
|
|
3
|
+
* agent amend one record at once, each from the digest they read. Exactly one
|
|
4
|
+
* write wins per digest; the others are refused with record-conflict, naming
|
|
5
|
+
* the digest the record has now and who wrote it, and win when they retry
|
|
6
|
+
* from it. Nothing anyone wrote is lost. The same through the command line,
|
|
7
|
+
* in separate processes, is in records-concurrent.e2e.test.ts.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { readFileSync } from "node:fs";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
import { afterAll, beforeAll, describe, expect, test } from "vitest";
|
|
13
|
+
import { cleanScratch, contract, git, REPO, repo } from "./__fixtures__/contract-repo";
|
|
14
|
+
import { parseFrontMatter } from "./records";
|
|
15
|
+
import { queryRecords } from "./records-cli";
|
|
16
|
+
import { amendRecord, newRecord, renderRecord, reviewRecord, type AmendDocument } from "./records-write";
|
|
17
|
+
import amendSchema from "./records-amend.schema.json";
|
|
18
|
+
import recordsSchema from "./records.schema.json";
|
|
19
|
+
|
|
20
|
+
const DECISIONS = join(REPO, "docs", "design", "decisions");
|
|
21
|
+
const KIND = "decisions/decision.kind.mjs";
|
|
22
|
+
type Data = Record<string, unknown>;
|
|
23
|
+
|
|
24
|
+
const SAMPLE = (() => {
|
|
25
|
+
const fm = parseFrontMatter(readFileSync(join(DECISIONS, "ws-003-seal-scope.md"), "utf-8"));
|
|
26
|
+
if (!fm.ok) throw new Error(fm.message);
|
|
27
|
+
return fm.value;
|
|
28
|
+
})();
|
|
29
|
+
const proposal = (over: Data): Data => ({ ...structuredClone(SAMPLE), state: "proposed", choice: null, decided_by: null, decided_on: null, ...over });
|
|
30
|
+
const record = (d: Data) => renderRecord(d, `\n# ${String(d.title)}\n`);
|
|
31
|
+
|
|
32
|
+
const amendDoc = contract(amendSchema);
|
|
33
|
+
const recordsDoc = contract(recordsSchema);
|
|
34
|
+
|
|
35
|
+
let root: string;
|
|
36
|
+
beforeAll(() => {
|
|
37
|
+
root = repo(
|
|
38
|
+
{
|
|
39
|
+
"chant.workspace.json": JSON.stringify({
|
|
40
|
+
name: "box",
|
|
41
|
+
schema: 1,
|
|
42
|
+
members: [{ name: "app", dir: "app", kind: "other", because: "a plain Node server" }],
|
|
43
|
+
records: [{ kind: KIND }],
|
|
44
|
+
writeScope: { agent: { records: { decision: ["new", "amend", "review"] } } },
|
|
45
|
+
agents: [{ name: "app-agent", member: "app" }],
|
|
46
|
+
}),
|
|
47
|
+
[KIND]: readFileSync(join(DECISIONS, "decision.kind.mjs"), "utf-8"),
|
|
48
|
+
"decisions/decision.schema.json": readFileSync(join(DECISIONS, "decision.schema.json"), "utf-8"),
|
|
49
|
+
"decisions/ws-001-one.md": record(proposal({ id: "ws-001", title: "One" })),
|
|
50
|
+
"app/server.mjs": "export const port = 8080;\n",
|
|
51
|
+
},
|
|
52
|
+
true,
|
|
53
|
+
);
|
|
54
|
+
git(root, "branch", "-M", "main");
|
|
55
|
+
});
|
|
56
|
+
afterAll(cleanScratch);
|
|
57
|
+
|
|
58
|
+
async function read(id: string): Promise<{ digest: string; lastWrite: unknown; data: Data }> {
|
|
59
|
+
const doc = await queryRecords({ kind: KIND, cwd: root });
|
|
60
|
+
recordsDoc.expectValid(doc);
|
|
61
|
+
const r = (doc as { records: { id: string; digest: string; lastWrite: unknown; data: Data }[] }).records.find((x) => x.id === id)!;
|
|
62
|
+
return { digest: r.digest, lastWrite: r.lastWrite, data: r.data };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const failed = (d: AmendDocument) => ("error" in d ? d : null);
|
|
66
|
+
|
|
67
|
+
describe("two people and an agent amend one record at once (#3173)", () => {
|
|
68
|
+
test("exactly one wins per digest, and the others get a conflict they can retry", async () => {
|
|
69
|
+
const start = await read("ws-001");
|
|
70
|
+
expect(start.lastWrite).toBeNull();
|
|
71
|
+
|
|
72
|
+
const writes = {
|
|
73
|
+
alice: { fields: { title: "One, as alice has it" } },
|
|
74
|
+
bob: { fields: { question: "What does bob ask?" } },
|
|
75
|
+
agent: { fields: { constrains: ["member:app"] }, agent: "app-agent" },
|
|
76
|
+
} as const;
|
|
77
|
+
const amend = (who: keyof typeof writes, expect: string) =>
|
|
78
|
+
amendRecord({ kind: KIND, id: "ws-001", fields: JSON.stringify(writes[who].fields), cwd: root, expect, agent: who === "agent" ? "app-agent" : undefined });
|
|
79
|
+
|
|
80
|
+
// All three from the digest they read, at once.
|
|
81
|
+
const first = await Promise.all((["alice", "bob", "agent"] as const).map(async (who) => [who, await amend(who, start.digest)] as const));
|
|
82
|
+
for (const [, doc] of first) amendDoc.expectValid(doc);
|
|
83
|
+
const won = first.filter(([, d]) => !failed(d));
|
|
84
|
+
const lost = first.filter(([, d]) => failed(d));
|
|
85
|
+
expect(won).toHaveLength(1);
|
|
86
|
+
expect(lost).toHaveLength(2);
|
|
87
|
+
const [winner, winDoc] = won[0];
|
|
88
|
+
const d1 = (winDoc as { digest: string }).digest;
|
|
89
|
+
expect(d1).not.toBe(start.digest);
|
|
90
|
+
for (const [, doc] of lost) {
|
|
91
|
+
const f = failed(doc)!;
|
|
92
|
+
expect(f.error.code).toBe("record-conflict");
|
|
93
|
+
expect(f.error.message).toContain(d1);
|
|
94
|
+
expect(f.conflict).toMatchObject({ id: "ws-001", path: "decisions/ws-001-one.md", expected: start.digest, digest: d1 });
|
|
95
|
+
expect(f.conflict!.lastWrite).toMatchObject({ verb: "records amend", agent: winner === "agent" ? "app-agent" : null });
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// The read contract shows the same digest and who wrote it.
|
|
99
|
+
const after = await read("ws-001");
|
|
100
|
+
expect(after.digest).toBe(d1);
|
|
101
|
+
expect(after.lastWrite).toMatchObject({ verb: "records amend" });
|
|
102
|
+
|
|
103
|
+
// Each loser retries from the digest its conflict named; the first retry wins, the second conflicts again, then wins.
|
|
104
|
+
const [[l1], [l2]] = lost;
|
|
105
|
+
const r1 = await amend(l1, d1);
|
|
106
|
+
expect(failed(r1)).toBeNull();
|
|
107
|
+
const d2 = (r1 as { digest: string }).digest;
|
|
108
|
+
const r2 = await amend(l2, d1);
|
|
109
|
+
expect(failed(r2)?.conflict?.digest).toBe(d2);
|
|
110
|
+
const r3 = await amend(l2, d2);
|
|
111
|
+
expect(failed(r3)).toBeNull();
|
|
112
|
+
|
|
113
|
+
// Nothing anyone wrote was lost.
|
|
114
|
+
const end = await read("ws-001");
|
|
115
|
+
expect(end.data).toMatchObject({ title: "One, as alice has it", question: "What does bob ask?", constrains: ["member:app"] });
|
|
116
|
+
expect(end.digest).toBe((r3 as { digest: string }).digest);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
test("a verdict on a text the reviewer did not see is refused, and one on the current text is taken", async () => {
|
|
120
|
+
const now = await read("ws-001");
|
|
121
|
+
const stale = await reviewRecord({ kind: KIND, id: "ws-001", verdict: "agree", by: "carol", cwd: root, expect: "0".repeat(64) });
|
|
122
|
+
expect(stale).toMatchObject({ error: { code: "record-conflict" }, conflict: { digest: now.digest } });
|
|
123
|
+
const fresh = await reviewRecord({ kind: KIND, id: "ws-001", verdict: "agree", by: "carol", cwd: root, expect: now.digest });
|
|
124
|
+
expect(fresh).toMatchObject({ digest: now.digest, review: { reviewer: "carol", digest: now.digest } });
|
|
125
|
+
expect((await read("ws-001")).lastWrite).toMatchObject({ verb: "records review", by: "carol" });
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test("writes without --expect are serialised, so none is lost and no id is handed out twice", async () => {
|
|
129
|
+
const titles = ["Two", "Three", "Four"];
|
|
130
|
+
const made = await Promise.all(titles.map((title) => newRecord({ kind: KIND, fields: JSON.stringify(proposal({ id: undefined, title })), cwd: root })));
|
|
131
|
+
const ids = made.map((d) => ("error" in d ? d.error.code : d.id));
|
|
132
|
+
expect(new Set(ids).size).toBe(3);
|
|
133
|
+
for (const id of ids) expect(id).toMatch(/^ws-00[2-4]$/);
|
|
134
|
+
const amended = await Promise.all([
|
|
135
|
+
amendRecord({ kind: KIND, id: "ws-002", fields: JSON.stringify({ title: "Two, retitled" }), cwd: root }),
|
|
136
|
+
amendRecord({ kind: KIND, id: "ws-002", fields: JSON.stringify({ question: "Asked again?" }), cwd: root }),
|
|
137
|
+
]);
|
|
138
|
+
expect(amended.every((d) => !("error" in d))).toBe(true);
|
|
139
|
+
expect((await read("ws-002")).data).toMatchObject({ title: "Two, retitled", question: "Asked again?" });
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test("--expect that is not a digest is a usage error, and nothing is read", async () => {
|
|
143
|
+
expect(await amendRecord({ kind: KIND, id: "ws-001", fields: "{}", cwd: root, expect: "abc" })).toMatchObject({ error: { code: "write-usage-invalid" } });
|
|
144
|
+
});
|
|
145
|
+
});
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
import { execFileSync } from "node:child_process";
|
|
14
14
|
import { createHash } from "node:crypto";
|
|
15
|
-
import { mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
15
|
+
import { mkdirSync, mkdtempSync, readdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
16
16
|
import { tmpdir } from "node:os";
|
|
17
17
|
import { dirname, join } from "node:path";
|
|
18
18
|
import Ajv2020 from "ajv/dist/2020";
|
|
@@ -22,9 +22,11 @@ import { queryRecords, type RecordsDocument } from "./records-cli";
|
|
|
22
22
|
import { newRecord, runRecordsWrite } from "./records-write";
|
|
23
23
|
import { loadRecordKind, parseJsonRecord, RECORD_FORMATS, recordTextDigest } from "./records";
|
|
24
24
|
import schema from "./records.schema.json";
|
|
25
|
+
import newSchema from "./records-new.schema.json";
|
|
25
26
|
|
|
26
27
|
const REPO = join(import.meta.dirname, "..", "..", "..", "..");
|
|
27
28
|
const validateOutput = new Ajv2020({ strict: true, allErrors: true }).compile(schema);
|
|
29
|
+
const validateNew = new Ajv2020({ strict: true, allErrors: true }).compile(newSchema);
|
|
28
30
|
|
|
29
31
|
let root: string;
|
|
30
32
|
beforeEach(() => {
|
|
@@ -398,19 +400,51 @@ describe("a JSON session kind (#2673)", () => {
|
|
|
398
400
|
});
|
|
399
401
|
|
|
400
402
|
describe("the write commands", () => {
|
|
401
|
-
test("refuse a JSON kind
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
403
|
+
test("refuse a JSON kind with an id field, which they cannot write", async () => {
|
|
404
|
+
const doc = await newRecord({ kind: "design/unit.kind.mjs", fields: "{}", cwd: root, dryRun: true });
|
|
405
|
+
expect("error" in doc && doc.error.code).toBe("write-usage-invalid");
|
|
406
|
+
expect("error" in doc && doc.error.message).toMatch(/write only Markdown front matter records with an idField/);
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
test("records new writes a content-addressed record as JSON named for its bytes, and reads it back valid (ws-082)", async () => {
|
|
410
|
+
// The fields in another order: the file holds them in the schema's.
|
|
411
|
+
const fields = JSON.stringify(Object.fromEntries(Object.entries(EVIDENCE).reverse()));
|
|
412
|
+
const dry = await newRecord({ kind: "design/evidence.kind.mjs", fields, cwd: root, dryRun: true });
|
|
413
|
+
if ("error" in dry) throw new Error(dry.error.message);
|
|
414
|
+
const h = sha256(json(EVIDENCE));
|
|
415
|
+
expect(dry).toMatchObject({ id: h, path: `design/evidence/${h}.json`, dryRun: true, text: json(EVIDENCE), warnings: [] });
|
|
416
|
+
expect(validateNew(dry), JSON.stringify(validateNew.errors)).toBe(true);
|
|
417
|
+
expect(readdirSync(join(root, "design", "evidence"))).toEqual([]);
|
|
418
|
+
|
|
419
|
+
const doc = await newRecord({ kind: "design/evidence.kind.mjs", fields, cwd: root });
|
|
420
|
+
expect(doc).toMatchObject({ id: h, path: `design/evidence/${h}.json`, dryRun: false });
|
|
421
|
+
expect(readFileSync(join(root, "design", "evidence", `${h}.json`), "utf-8")).toBe(json(EVIDENCE));
|
|
422
|
+
const [read] = (await query("evidence")).records;
|
|
423
|
+
expect([read.id, read.valid, warned(read)]).toEqual([h, true, []]);
|
|
424
|
+
|
|
425
|
+
// The same bytes again are the same record, already written.
|
|
426
|
+
const again = await newRecord({ kind: "design/evidence.kind.mjs", fields, cwd: root });
|
|
427
|
+
expect("error" in again && again.error.code).toBe("record-id-taken");
|
|
428
|
+
});
|
|
429
|
+
|
|
430
|
+
test("records new refuses content-addressed fields the schema refuses, an id prefix and a seal", async () => {
|
|
431
|
+
const bad = await newRecord({ kind: "design/evidence.kind.mjs", fields: JSON.stringify({ ...EVIDENCE, tree: "not-a-tree" }), cwd: root });
|
|
432
|
+
expect("error" in bad && bad.error.code).toBe("record-schema-invalid");
|
|
433
|
+
const prefixed = await newRecord({ kind: "design/evidence.kind.mjs", fields: JSON.stringify(EVIDENCE), prefix: "ev", cwd: root });
|
|
434
|
+
expect("error" in prefixed && prefixed.error.message).toMatch(/--prefix allocates nothing/);
|
|
435
|
+
const signed = await newRecord({ kind: "design/evidence.kind.mjs", fields: JSON.stringify(EVIDENCE), sign: true, cwd: root });
|
|
436
|
+
expect("error" in signed && signed.error.message).toMatch(/names no author to seal/);
|
|
437
|
+
expect(readdirSync(join(root, "design", "evidence"))).toEqual([]);
|
|
407
438
|
});
|
|
408
439
|
});
|
|
409
440
|
|
|
410
441
|
describe("the write commands through a declared kind (#2680)", () => {
|
|
411
442
|
afterEach(() => vi.restoreAllMocks());
|
|
412
443
|
|
|
413
|
-
test.each([
|
|
444
|
+
test.each([
|
|
445
|
+
["unit", ["new", "amend", "review"], /write only Markdown front matter records with an idField/],
|
|
446
|
+
["evidence", ["amend", "review"], /a content-addressed record never changes once records new writes it/],
|
|
447
|
+
] as const)("refuse the declared %s kind with write-usage-invalid", async (kind, verbs, why) => {
|
|
414
448
|
write(
|
|
415
449
|
"chant.workspace.json",
|
|
416
450
|
json({ name: "acme", schema: 1, members: [{ name: "design", dir: "design", kind: "other", because: "acme's records" }], records: [{ kind: `design/${kind}.kind.mjs` }] }),
|
|
@@ -425,13 +459,13 @@ describe("the write commands through a declared kind (#2680)", () => {
|
|
|
425
459
|
["amend", "U-0001", "--set", join(root, "fields.json"), "--dry-run"],
|
|
426
460
|
["review", "U-0001", "--verdict", "agree", "--by", "bo", "--dry-run"],
|
|
427
461
|
];
|
|
428
|
-
for (const argv of runs) {
|
|
462
|
+
for (const argv of runs.filter((r) => (verbs as readonly string[]).includes(r[0]))) {
|
|
429
463
|
out.length = 0;
|
|
430
464
|
const status = await runRecordsWrite({ args: parseArgs(["workspace", "records", ...argv]), plugins: [] } as never);
|
|
431
465
|
const doc = JSON.parse(out.join("\n")) as { error?: { code: string; message: string } };
|
|
432
466
|
expect(status, argv[0]).toBe(1);
|
|
433
467
|
expect(doc.error?.code, argv[0]).toBe("write-usage-invalid");
|
|
434
|
-
expect(doc.error?.message, argv[0]).toMatch(
|
|
468
|
+
expect(doc.error?.message, argv[0]).toMatch(why);
|
|
435
469
|
}
|
|
436
470
|
});
|
|
437
471
|
});
|
|
@@ -554,6 +588,8 @@ describe("kinds without a lifecycle", () => {
|
|
|
554
588
|
["an unknown format", { ...KINDS.closure, format: "yaml" }, /format/],
|
|
555
589
|
["a session block without states", { ...KINDS.closure, session: { verdicts: "approvals", seal: "seal", subjects: { kind: "contract.kind.mjs" } } }, /session: a session kind must have states/],
|
|
556
590
|
["a seal without states (#2546)", { ...KINDS.closure, seal: { field: "seal" } }, /seal: a kind without states cannot have seal/],
|
|
591
|
+
["session subjects listing a kind twice (ws-082)", { ...KINDS.session, session: { verdicts: "approvals", seal: "seal", subjects: { kinds: ["contract.kind.mjs", "contract.kind.mjs"] } } }, /lists each kind file once/],
|
|
592
|
+
["session subjects with both kind and kinds (ws-082)", { ...KINDS.session, session: { verdicts: "approvals", seal: "seal", subjects: { kind: "contract.kind.mjs", kinds: ["contract.kind.mjs"] } } }, /subjects/],
|
|
557
593
|
["a seal naming another field than session.seal (#2546)", { ...KINDS.session, session: { verdicts: "approvals", seal: "seal", subjects: { kind: "contract.kind.mjs" } }, seal: { field: "closed_digest" } }, /seal: seal.field and session.seal name the field/],
|
|
558
594
|
])("a kind with %s is kind-invalid, naming the field", async (_label, kind, why) => {
|
|
559
595
|
write("design/closure.kind.mjs", kindFile(kind));
|
|
@@ -96,6 +96,11 @@
|
|
|
96
96
|
"$ref": "#/$defs/warning"
|
|
97
97
|
}
|
|
98
98
|
},
|
|
99
|
+
"digest": {
|
|
100
|
+
"type": "string",
|
|
101
|
+
"pattern": "^[0-9a-f]{64}$",
|
|
102
|
+
"description": "Added in contract 1 by #3173. The record's digest after the write, as chant workspace records --json reports it (with --dry-run, the digest it would have). Pass it as --expect to the next amend, review or close of the record, so that write is refused with record-conflict if someone else wrote the record in between."
|
|
103
|
+
},
|
|
99
104
|
"text": {
|
|
100
105
|
"type": "string",
|
|
101
106
|
"description": "With --dry-run only: the whole text of the file the command would write."
|
|
@@ -165,6 +170,8 @@
|
|
|
165
170
|
"source-harvest-not-proposed",
|
|
166
171
|
"record-state-not-initial",
|
|
167
172
|
"ratify-quorum-not-met",
|
|
173
|
+
"write-lock-timeout",
|
|
174
|
+
"write-lock-not-held",
|
|
168
175
|
"write-scope-member",
|
|
169
176
|
"write-scope-kind",
|
|
170
177
|
"write-scope-class-unknown",
|
|
@@ -140,6 +140,11 @@
|
|
|
140
140
|
"$ref": "#/$defs/warning"
|
|
141
141
|
}
|
|
142
142
|
},
|
|
143
|
+
"digest": {
|
|
144
|
+
"type": "string",
|
|
145
|
+
"pattern": "^[0-9a-f]{64}$",
|
|
146
|
+
"description": "Added in contract 1 by #3173. The record's digest after the write, as chant workspace records --json reports it (with --dry-run, the digest it would have). Pass it as --expect to the next amend, review or close of the record, so that write is refused with record-conflict if someone else wrote the record in between."
|
|
147
|
+
},
|
|
143
148
|
"text": {
|
|
144
149
|
"type": "string",
|
|
145
150
|
"description": "With --dry-run only: the whole text of the file the command would write."
|
|
@@ -259,6 +264,9 @@
|
|
|
259
264
|
"review-sign-failed",
|
|
260
265
|
"session-unknown",
|
|
261
266
|
"session-not-open",
|
|
267
|
+
"record-conflict",
|
|
268
|
+
"write-lock-timeout",
|
|
269
|
+
"write-lock-not-held",
|
|
262
270
|
"write-scope-member",
|
|
263
271
|
"write-scope-kind",
|
|
264
272
|
"write-scope-class-unknown",
|
|
@@ -282,6 +290,76 @@
|
|
|
282
290
|
}
|
|
283
291
|
}
|
|
284
292
|
},
|
|
293
|
+
"conflict": {
|
|
294
|
+
"type": "object",
|
|
295
|
+
"description": "Added in contract 1 by #3173. With record-conflict only: the record as it is now. Re-read it, and write again with --expect set to digest if the change still stands.",
|
|
296
|
+
"required": [
|
|
297
|
+
"id",
|
|
298
|
+
"path",
|
|
299
|
+
"expected",
|
|
300
|
+
"digest",
|
|
301
|
+
"lastWrite"
|
|
302
|
+
],
|
|
303
|
+
"properties": {
|
|
304
|
+
"id": {
|
|
305
|
+
"type": "string"
|
|
306
|
+
},
|
|
307
|
+
"path": {
|
|
308
|
+
"type": "string",
|
|
309
|
+
"description": "The record file, from the repository root."
|
|
310
|
+
},
|
|
311
|
+
"expected": {
|
|
312
|
+
"type": "string",
|
|
313
|
+
"description": "The digest given with --expect."
|
|
314
|
+
},
|
|
315
|
+
"digest": {
|
|
316
|
+
"type": "string",
|
|
317
|
+
"pattern": "^[0-9a-f]{64}$",
|
|
318
|
+
"description": "The record's digest now."
|
|
319
|
+
},
|
|
320
|
+
"lastWrite": {
|
|
321
|
+
"description": "The last chant write of the record file, from the write journal in the git directory, while the file still holds the text it left; null when the journal does not know it.",
|
|
322
|
+
"oneOf": [
|
|
323
|
+
{
|
|
324
|
+
"type": "null"
|
|
325
|
+
},
|
|
326
|
+
{
|
|
327
|
+
"type": "object",
|
|
328
|
+
"required": [
|
|
329
|
+
"verb",
|
|
330
|
+
"by",
|
|
331
|
+
"agent",
|
|
332
|
+
"at"
|
|
333
|
+
],
|
|
334
|
+
"properties": {
|
|
335
|
+
"verb": {
|
|
336
|
+
"type": "string",
|
|
337
|
+
"description": "The write command, such as records amend or points answer."
|
|
338
|
+
},
|
|
339
|
+
"by": {
|
|
340
|
+
"type": [
|
|
341
|
+
"string",
|
|
342
|
+
"null"
|
|
343
|
+
],
|
|
344
|
+
"description": "The principal the write named, or null when it named none."
|
|
345
|
+
},
|
|
346
|
+
"agent": {
|
|
347
|
+
"type": [
|
|
348
|
+
"string",
|
|
349
|
+
"null"
|
|
350
|
+
],
|
|
351
|
+
"description": "The agent session it was made in (CHANT_AGENT), or null."
|
|
352
|
+
},
|
|
353
|
+
"at": {
|
|
354
|
+
"type": "string",
|
|
355
|
+
"description": "When, as an ISO 8601 time."
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
]
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
},
|
|
285
363
|
"path": false
|
|
286
364
|
}
|
|
287
365
|
}
|
|
@@ -241,3 +241,72 @@ describe("records --since <session id> (#2693)", () => {
|
|
|
241
241
|
expect((await sinceDoc({ kind: DECISIONS_KIND, since: "v-1", cwd: root })).session).toBeUndefined();
|
|
242
242
|
});
|
|
243
243
|
});
|
|
244
|
+
|
|
245
|
+
describe("a UI review batch as a session (#3350, ws-083)", () => {
|
|
246
|
+
const anchor = { route: "/notes?page=2#top", elements: [{ selector: "[data-hud-id=status]", tag: "p", text: "Running.", state: "anchored", basis: "authored" }] };
|
|
247
|
+
|
|
248
|
+
test("a batch is written through records new, answered and replied to through amend, and closed with each comment's exit", async () => {
|
|
249
|
+
const root = declared();
|
|
250
|
+
const sent = await newRecord({
|
|
251
|
+
kind: SESSIONS_KIND,
|
|
252
|
+
fields: newSessionFields({
|
|
253
|
+
title: "Review of /notes",
|
|
254
|
+
agenda: [{ text: "Comment-mode review of /notes" }],
|
|
255
|
+
attendance: [{ principal: "alice", class: "person", roles: ["reviewer"] }, { principal: "hud-chat", class: "agent" }],
|
|
256
|
+
comments: [
|
|
257
|
+
{ text: "Say which build is running.", by: "alice", anchor },
|
|
258
|
+
{ text: "", anchor: { route: "/notes?page=2#top", elements: [{ selector: "main > ul", state: "ambiguous" }] } },
|
|
259
|
+
],
|
|
260
|
+
rounds: [{ round: 0, note: "The list's second page, mostly.", by: "alice", at: "2026-09-25T09:00:00Z", ended: null }],
|
|
261
|
+
}),
|
|
262
|
+
cwd: root,
|
|
263
|
+
});
|
|
264
|
+
created.expectValid(sent);
|
|
265
|
+
if ("error" in sent) throw new Error(`${sent.error.code}: ${sent.error.message}`);
|
|
266
|
+
|
|
267
|
+
const answered = (round: number, extra: Record<string, unknown>[]) => [
|
|
268
|
+
{ text: "Say which build is running.", by: "alice", anchor, answers: [{ round: 0, disposition: "needs-discussion", note: "The commit, or the release tag?", by: "hud-chat", at: "2026-09-25T09:02:00Z" }, ...extra] },
|
|
269
|
+
{ text: "", anchor: { route: "/notes?page=2#top", elements: [{ selector: "main > ul", state: "ambiguous" }] }, answers: [{ round, disposition: "handled", note: "Paged the list by 20.", at: "2026-09-25T09:03:00Z" }] },
|
|
270
|
+
];
|
|
271
|
+
const first = await amendRecord({
|
|
272
|
+
kind: SESSIONS_KIND,
|
|
273
|
+
id: sent.id,
|
|
274
|
+
fields: JSON.stringify({ comments: answered(0, []), rounds: [{ round: 0, note: "The list's second page, mostly.", by: "alice", at: "2026-09-25T09:00:00Z", ended: "2026-09-25T09:04:00Z" }] }),
|
|
275
|
+
cwd: root,
|
|
276
|
+
});
|
|
277
|
+
expect(code(first)).toBe("ok");
|
|
278
|
+
|
|
279
|
+
const comments = answered(0, [{ round: 1, disposition: "handled", note: "Shows the release tag now.", by: "hud-chat", at: "2026-09-25T09:11:00Z" }]);
|
|
280
|
+
(comments[0] as Record<string, unknown>).replies = [{ round: 1, text: "The release tag.", by: "alice", at: "2026-09-25T09:10:00Z" }];
|
|
281
|
+
(comments[0] as Record<string, unknown>).exit = "kept";
|
|
282
|
+
(comments[0] as Record<string, unknown>).follow_ups = ["work:W-007"];
|
|
283
|
+
(comments[1] as Record<string, unknown>).exit = "sent";
|
|
284
|
+
const rounds = [
|
|
285
|
+
{ round: 0, note: "The list's second page, mostly.", by: "alice", at: "2026-09-25T09:00:00Z", ended: "2026-09-25T09:04:00Z" },
|
|
286
|
+
{ round: 1, note: null, by: "alice", at: "2026-09-25T09:10:00Z", ended: "2026-09-25T09:12:00Z" },
|
|
287
|
+
];
|
|
288
|
+
expect(code(await amendRecord({ kind: SESSIONS_KIND, id: sent.id, fields: JSON.stringify({ comments, rounds, closed_by: "alice" }), cwd: root }))).toBe("ok");
|
|
289
|
+
const closed = await closeRecord({ kind: SESSIONS_KIND, id: sent.id, cwd: root, now: new Date("2026-09-25T09:15:00Z") });
|
|
290
|
+
close.expectValid(closed);
|
|
291
|
+
expect(code(closed)).toBe("ok");
|
|
292
|
+
|
|
293
|
+
const doc = await queryRecords({ kind: SESSIONS_KIND, cwd: root });
|
|
294
|
+
if ("error" in doc) throw new Error(doc.error.message);
|
|
295
|
+
const s2 = doc.records.find((r) => r.id === sent.id)!;
|
|
296
|
+
expect([s2.state, s2.valid, s2.reasons]).toEqual(["closed", true, []]);
|
|
297
|
+
expect((s2.data!.comments as { exit?: string }[]).map((c) => c.exit)).toEqual(["kept", "sent"]);
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
test.each([
|
|
301
|
+
["an answer with another disposition", { text: "x", answers: [{ round: 0, disposition: "kept", note: "n" }] }],
|
|
302
|
+
["an answer with no note", { text: "x", answers: [{ round: 0, disposition: "skipped", note: " " }] }],
|
|
303
|
+
["a reply in round 0", { text: "x", replies: [{ round: 0, text: "r" }] }],
|
|
304
|
+
["an anchor with no element", { text: "x", anchor: { route: "/", elements: [] } }],
|
|
305
|
+
["an element in an unknown state", { text: "x", anchor: { route: "/", elements: [{ selector: "p", state: "moved" }] } }],
|
|
306
|
+
["an exit that is not kept, sent or dismissed", { text: "x", exit: "handled" }],
|
|
307
|
+
])("records new refuses %s", async (_label, comment) => {
|
|
308
|
+
const root = declared();
|
|
309
|
+
const doc = await newRecord({ kind: SESSIONS_KIND, fields: newSessionFields({ comments: [comment] }), cwd: root, dryRun: true });
|
|
310
|
+
expect(code(doc)).toBe("record-schema-invalid");
|
|
311
|
+
});
|
|
312
|
+
});
|
|
@@ -114,6 +114,28 @@ describe("the session kind (#2673)", () => {
|
|
|
114
114
|
expect(then.records.find((r) => r.id === "S-0002")).toMatchObject({ state: "open", valid: true, citedBy: [] });
|
|
115
115
|
});
|
|
116
116
|
|
|
117
|
+
test("a verdict may name a contract or a driver, the session kind's other subjects, and a contract review cites the session (#3148, ws-082)", async () => {
|
|
118
|
+
const root = fixture();
|
|
119
|
+
const contractFile = join(root, "design", "contracts", "C-001-the-home-page-follows-its-screen-spec.md");
|
|
120
|
+
writeFileSync(contractFile, readFileSync(contractFile, "utf-8").replace("reviews: []", 'reviews:\n - reviewer: "alice"\n verdict: "agree"\n on: "2026-09-24"\n session: "S-0002"'));
|
|
121
|
+
writeFileSync(
|
|
122
|
+
join(root, "design", "sessions", "S-0002-walk.md"),
|
|
123
|
+
sessionText({
|
|
124
|
+
id: "S-0002",
|
|
125
|
+
state: "closed",
|
|
126
|
+
agenda: ["C-001", "D-001"],
|
|
127
|
+
verdicts: [
|
|
128
|
+
{ record: "C-001", principal: "alice", verdict: "agree" },
|
|
129
|
+
{ record: "D-001", principal: "alice", verdict: "agree" },
|
|
130
|
+
{ record: "C-999", principal: "alice", verdict: "agree" },
|
|
131
|
+
],
|
|
132
|
+
}),
|
|
133
|
+
);
|
|
134
|
+
const s2 = (await read(root)).records.find((r) => r.id === "S-0002")!;
|
|
135
|
+
expect(s2.reasons).toEqual([{ code: "session-verdict-unknown-record", message: expect.stringMatching(/verdicts\[2\] names C-999, which no .*decision\.kind\.mjs or .*contract\.kind\.mjs or .*driver\.kind\.mjs record has/) }]);
|
|
136
|
+
expect(s2.citedBy).toEqual([{ id: "C-001", path: "design/contracts/C-001-the-home-page-follows-its-screen-spec.md", index: 0, reviewer: "alice", verdict: "agree" }]);
|
|
137
|
+
});
|
|
138
|
+
|
|
117
139
|
test("the decision kind accepts a review entry naming its session", async () => {
|
|
118
140
|
const root = fixture();
|
|
119
141
|
reviewed(root, ["alice"], "S-0001");
|