@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
|
@@ -136,10 +136,14 @@ export const REASONS = {
|
|
|
136
136
|
"source-harvest-not-proposed": "A harvested record (source.via harvest) was written in a state other than the kind's first: a harvest proposes, and a person decides.",
|
|
137
137
|
"ratify-quorum-not-met": "The write puts a record in its kind's ratified state (reviews.ratified), and the record's quorum is not met: too few agreeing verdicts count.",
|
|
138
138
|
"record-sign-failed": "--sign was given and no author seal could be made: the record names no author, the key can't be read or used, git names no ssh signing key, or ssh-keygen is not installed.",
|
|
139
|
+
// Concurrent writers in one working tree (#3173, ws-089): a write that is refused rather than overwrite another's.
|
|
140
|
+
"record-conflict": "--expect named a digest the record no longer has: another write changed it after the caller read it. The refusal names the digest it has now; re-read the record and write again.",
|
|
141
|
+
"write-lock-timeout": "Another write held the working tree's write lock for longer than the write waits (CHANT_WRITE_LOCK_WAIT_MS); the refusal names the holder. Run the write again.",
|
|
142
|
+
"write-lock-not-held": "CHANT_WRITE_LOCK names a batch's lock token that no longer holds the working tree's write lock: the batch released it, or it expired and another writer took it.",
|
|
139
143
|
// Write scope (#2548): a records write that is refused, and a commit check --changes reports, outside the writer's scope.
|
|
140
144
|
"write-scope-member": "The write is to a file, or to a record kind, of a member outside the writer's scope: an agent session writes only its own member, and writeScope.<class>.members leaves the member out.",
|
|
141
145
|
"write-scope-kind": "The write is to a record kind writeScope.<class>.records does not list, with a verb it does not list for the kind, or deletes a record.",
|
|
142
|
-
"write-scope-protected": "The write is to a file writeScope.<class>.protected lists, or one under a directory it lists, outside
|
|
146
|
+
"write-scope-protected": "The write is to a file writeScope.<class>.protected lists, or one under a directory it lists, outside the top-level keys or JSON Pointers the entry's except allows (#3146, #3308).",
|
|
143
147
|
"write-scope-class-unknown": "The declaration's writeScope at base names a principal class no pinned package supplies, and the writer is judged human, so it may be in that class; the write is refused until the package is installed at the pinned version or the entry removed (#3080).",
|
|
144
148
|
"agent-unknown": "CHANT_AGENT, or a commit's Chant-Agent trailer, names an agent session the declaration at base does not declare.",
|
|
145
149
|
"principal-unidentified": "chant.workspace.json at base sets identity.attribution to identified, and the write names a person by a bare name: --by, an author field or a decision point's answerer must be a forge identity (github:<login>, gitlab:<login>, <forge>@<host>:<login>), a principal the signers file at base lists, or an agent, runner or service principal (#3163).",
|
|
@@ -155,6 +159,9 @@ export const REASONS = {
|
|
|
155
159
|
"answer-not-candidate": "The people's answer is not one of the question's candidates.",
|
|
156
160
|
"quorum-not-met": "Too few of the people who answered count toward the point's quorum: distinct, not holding the agent role, not the steward that asked, and holding one of its roles when it names any.",
|
|
157
161
|
"answer-in-steward-turn": "The answer was given during a steward's turn, or by a process it started: a steward never answers a decision point, and a person answers it through hud or at a shell.",
|
|
162
|
+
"answer-not-answered": "points retract names a question that has no answer to retract: it is escalated or proposed, and people answer it instead (#3351).",
|
|
163
|
+
"answer-field-unsupported": "The answer kind's copy of point-answer.schema.json predates a field the write needs, a note, a relay, a retraction or an ad-hoc question's asked: copy the schema anew to turn it on (#3351, #3402, #3403).",
|
|
164
|
+
"point-candidates-invalid": "points ask names an ad-hoc point without --candidates, a declared point with them, or candidates that are not a question and criteria of the point's question type (#3403).",
|
|
158
165
|
// records --since that fails (#2673).
|
|
159
166
|
"since-rev-unknown": "--since names no commit, or a session with no opening revision and no commit that added it.",
|
|
160
167
|
"since-session-unknown": "--since has the shape of a session id and names no commit, and no session the kind or the declaration reads has that id.",
|
|
@@ -168,6 +175,7 @@ export const REASONS = {
|
|
|
168
175
|
// The intent graph: part of the walk that can't be read. The document is still printed.
|
|
169
176
|
"intent-history-shallow": "The repository is a shallow clone, so the region's history stops at the clone's boundary.",
|
|
170
177
|
"intent-plugin-failed": "A kind file's commitJoins, which joins commits to units, contracts and evidence, failed for a commit.",
|
|
178
|
+
"squash-unfollowed": "A squash commit's pull request ref (refs/pull/<n>/head) is not in the clone and could not be fetched from origin, so its original commits are not followed; the read keeps its answer without them.",
|
|
171
179
|
// The intent graph: findings, each a node in the graph.
|
|
172
180
|
"intent-commit-undecided": "A commit changed the region when no decision constrained it at path granularity.",
|
|
173
181
|
"intent-commit-bare": "A commit names no unit, carries no record through its Chant-Record or Chant-Lease trailer, and has no pull request and no decision covering the region at its time.",
|
|
@@ -186,6 +194,7 @@ export const REASONS = {
|
|
|
186
194
|
"intent-decision-unimplemented": "A decided decision constrains the region, no work item that is not dropped implements it, and no commit falls in its window.",
|
|
187
195
|
"intent-work-blocked": "A work item constraining the region has commits in its window while a work item it needs is not done.",
|
|
188
196
|
"intent-work-open-decided-code": "Commits in the region are a decision's own work while the work item implementing that decision is still open.",
|
|
197
|
+
"intent-commit-join-conflict": "A commit joined to an agent run by its Chant-Run trailer or the run's record has the patch-id of a commit another run recorded, so it is not joined to that run by content.",
|
|
189
198
|
// The intent graph's answer to why the region is like this (#3034): what it can't account for.
|
|
190
199
|
"intent-why-no-decision": "No current decision governs the region at any granularity, and none is carried out by the commits or runs that made its current lines.",
|
|
191
200
|
"intent-why-no-run": "None of the region's current lines was made by a commit an agent run is joined to.",
|
|
@@ -207,6 +216,7 @@ export const REASONS = {
|
|
|
207
216
|
"environments-none-declared": "The member's chant.config.ts declares no environments, so only local and the environments in its ledger are listed.",
|
|
208
217
|
"environments-ledger-undeclared": "The member's ledger has releases in an environment its config's environments don't cover, so chant run --env would refuse it and it is not listed.",
|
|
209
218
|
"environments-ledger-unreadable": "The chant/lifecycle branch exists and the member's ledger environments could not be listed.",
|
|
219
|
+
"environments-component-undeclared": "A component declares an environment its member's chant.config.ts environments don't cover, so chant run --env would refuse it and it is not listed (#3153).",
|
|
210
220
|
// A declared diagram's source or render (check, #2764). Each is also a WSP finding.
|
|
211
221
|
"diagram-source-missing": "A declared diagram's source file does not exist in the tree read.",
|
|
212
222
|
"diagram-render-missing": "A declared diagram's render file does not exist in the tree read.",
|
|
@@ -241,6 +251,30 @@ export const REASONS = {
|
|
|
241
251
|
"run-ended": "runs end names a run whose end is already recorded.",
|
|
242
252
|
"runs-no-ledger": "The checkout has no chant/lifecycle branch, so there are no agent runs to read.",
|
|
243
253
|
"runs-ledger-malformed": "Some lines of the agent run ledger aren't run events; the rest are read.",
|
|
254
|
+
// The agent-run statement (runs sign, statement and verify, #3192): why a statement is refused or does not verify.
|
|
255
|
+
"run-not-ended": "runs sign or runs statement names a run with no end recorded. A statement is signed over the run's whole record.",
|
|
256
|
+
"run-statement-invalid": "The envelope's payload is not an in-toto Statement v1 with chant's agent-run predicate, or has a field the predicate does not define.",
|
|
257
|
+
"run-statement-signer-mismatch": "The statement names a signer other than the runner whose key signed it.",
|
|
258
|
+
"run-statement-mismatch": "A listed runner key signed the statement, and it does not match the run's record: another run, a record that hashes differently, or another unit, harness, model, provider or principal.",
|
|
259
|
+
// A box's listing written through chant (box listing set, #3308): why the write was refused. Nothing is written.
|
|
260
|
+
"listing-member-unknown": "box listing set names a member the declaration does not declare.",
|
|
261
|
+
"listing-box-missing": "box listing set names a member whose entry declares no box block, so it has no listing.",
|
|
262
|
+
"listing-cover-invalid": "The cover can't be read, is not a PNG, JPEG or WebP picture, is larger than 5 MiB, has a path outside the workspace, or has an extension other than its picture format's.",
|
|
263
|
+
// A box's work published through chant (box publish, #3165, ws-088): why the call printed no result.
|
|
264
|
+
"publish-member-unknown": "box publish names a member the declaration does not declare.",
|
|
265
|
+
"publish-none": "box publish names a member whose box block names no publisher, or which declares no box block.",
|
|
266
|
+
"publish-refused": "The box's publisher refused (it exited 2): nothing was published, and its message says why.",
|
|
267
|
+
"publish-failed": "The box's publisher could not be run, failed (a nonzero exit other than 2) or ran out of time; its message says what it had done.",
|
|
268
|
+
"publish-answer-invalid": "The box's publisher exited 0 and printed no JSON object, or one box-publish.schema.json does not allow.",
|
|
269
|
+
"publish-unrecorded": "The commit the publisher named is not in the repository, or lacks the apply record of ws-075: Chant-Applied-By naming --by, Chant-Applied-At, Chant-Applied-Commit and a Chant-Record for the item, or a Chant-Record for each record sent.",
|
|
270
|
+
// Work in progress under refs/chant/wip/<branch> and its replication (wip save|restore|push|fetch, #3172): why the write was refused. Nothing is written.
|
|
271
|
+
"wip-no-branch": "HEAD is detached, or names a branch with no commit yet, so there is no branch to keep work in progress for.",
|
|
272
|
+
"wip-none": "wip restore was given no snapshot, and the branch has none under refs/chant/wip/<branch>.",
|
|
273
|
+
"wip-snapshot-unknown": "wip restore names something that is not a work-in-progress snapshot chant took.",
|
|
274
|
+
"wip-branch-other": "wip restore names a snapshot taken on another branch than the one checked out.",
|
|
275
|
+
"wip-race": "Another writer moved refs/chant/wip/<branch> between this command's read and its write.",
|
|
276
|
+
"wip-policy-none": "wip push or wip fetch was run, and no box block declares replicate, so there is no remote.",
|
|
277
|
+
"wip-remote-unknown": "The replicate policy names a git remote the checkout does not have; the host adds it, with its credential, before chant pushes.",
|
|
244
278
|
// A box whose isolation fails (check, #2727). Each is also a WSP finding.
|
|
245
279
|
"box-isolation-collision": "Two boxes on one host resolve to the same port, state path or cookie name, or two ports in one box share an offset.",
|
|
246
280
|
"box-isolation-literal": "A host's stateRoot or a box's state entry is a literal machine path instead of one derived from an environment reference and the box's name.",
|
|
@@ -263,9 +297,9 @@ export const REASONS = {
|
|
|
263
297
|
"evidence-payload-type": "The envelope's payload type is not application/vnd.in-toto+json.",
|
|
264
298
|
"evidence-statement-invalid": "The payload is not an in-toto Statement v1 with chant's runner-evidence predicate, or has a field the predicate does not define.",
|
|
265
299
|
"evidence-runner-mismatch": "The statement names a runner other than the one whose key signed it.",
|
|
266
|
-
"runner-key-invalid": "The key given to evidence sign is not an Ed25519 private key in PEM.",
|
|
267
|
-
"runner-key-is-signer": "The key given to evidence sign is a person's key in the signers file. Evidence
|
|
268
|
-
"runner-key-unlisted": "The policy at base lists no runner with the key given to evidence sign.",
|
|
300
|
+
"runner-key-invalid": "The key given to evidence sign or runs sign is not an Ed25519 private key in PEM.",
|
|
301
|
+
"runner-key-is-signer": "The key given to evidence sign or runs sign is a person's key in the signers file. Evidence and run statements are signed by a service or CI identity.",
|
|
302
|
+
"runner-key-unlisted": "The policy at base lists no runner with the key given to evidence sign or runs sign.",
|
|
269
303
|
// The lineage lock (check).
|
|
270
304
|
"lock-invalid": "The lineage lock can't be read.",
|
|
271
305
|
"manual-step-open": "A scope in the lineage lock has an open manual step.",
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* `citedBy`. This module only reads; nothing here writes a seal into a file.
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
-
import { recordSeal, type RecordEntry, type RecordFormat, type RecordKind } from "./records";
|
|
15
|
+
import { recordSeal, sessionSubjectKinds, type RecordEntry, type RecordFormat, type RecordKind, type SessionSubjects } from "./records";
|
|
16
16
|
|
|
17
17
|
/** A subject record's review entry that names a session in its `session` field. */
|
|
18
18
|
export interface SessionCitation {
|
|
@@ -43,18 +43,19 @@ export function sessionSeal(text: string, field: string, format: RecordFormat =
|
|
|
43
43
|
/**
|
|
44
44
|
* Check each session in `entries` against its seal and the subject records,
|
|
45
45
|
* and fill in `citedBy`. `texts` holds each entry's file text by path.
|
|
46
|
-
* `subjects` holds the subject
|
|
47
|
-
* it is null when they were not read, and then no verdict is
|
|
46
|
+
* `subjects` holds the records of each subject kind and the name of their
|
|
47
|
+
* reviews list; it is null when they were not read, and then no verdict is
|
|
48
|
+
* checked.
|
|
48
49
|
*/
|
|
49
|
-
export function joinSessions(kind: RecordKind, entries: RecordEntry[], texts: Map<string, string>, subjects:
|
|
50
|
+
export function joinSessions(kind: RecordKind, entries: RecordEntry[], texts: Map<string, string>, subjects: SessionSubjects[] | null): void {
|
|
50
51
|
const decl = kind.session;
|
|
51
52
|
if (!decl) return;
|
|
52
53
|
const closed = new Set(kind.closedStates);
|
|
53
|
-
const subjectIds = new Set(subjects
|
|
54
|
+
const subjectIds = new Set((subjects ?? []).flatMap((k) => k.records.map((s) => s.id)).filter((id): id is string => id !== null));
|
|
54
55
|
const citations = new Map<string, SessionCitation[]>();
|
|
55
|
-
for (const s of subjects
|
|
56
|
+
for (const { s, reviewsField } of (subjects ?? []).flatMap((k) => k.records.map((s) => ({ s, reviewsField: k.reviews })))) {
|
|
56
57
|
if (s.id === null) continue;
|
|
57
|
-
const reviews = s.data?.[
|
|
58
|
+
const reviews = s.data?.[reviewsField];
|
|
58
59
|
if (!Array.isArray(reviews)) continue;
|
|
59
60
|
reviews.forEach((r, index) => {
|
|
60
61
|
if (r === null || typeof r !== "object") return;
|
|
@@ -96,7 +97,7 @@ export function joinSessions(kind: RecordKind, entries: RecordEntry[], texts: Ma
|
|
|
96
97
|
if (v === null || typeof v !== "object") return;
|
|
97
98
|
const record = (v as Record<string, unknown>).record;
|
|
98
99
|
if (typeof record === "string" && !subjectIds.has(record)) {
|
|
99
|
-
e.reasons.push({ code: "session-verdict-unknown-record", message: `${decl.verdicts}[${i}] names ${record}, which no ${decl.
|
|
100
|
+
e.reasons.push({ code: "session-verdict-unknown-record", message: `${decl.verdicts}[${i}] names ${record}, which no ${sessionSubjectKinds(decl).join(" or ")} record has` });
|
|
100
101
|
}
|
|
101
102
|
});
|
|
102
103
|
}
|
|
@@ -108,6 +108,11 @@
|
|
|
108
108
|
"$ref": "#/$defs/warning"
|
|
109
109
|
}
|
|
110
110
|
},
|
|
111
|
+
"digest": {
|
|
112
|
+
"type": "string",
|
|
113
|
+
"pattern": "^[0-9a-f]{64}$",
|
|
114
|
+
"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."
|
|
115
|
+
},
|
|
111
116
|
"text": {
|
|
112
117
|
"type": "string",
|
|
113
118
|
"description": "With --dry-run only: the whole text of the file the command would write."
|
|
@@ -176,6 +181,9 @@
|
|
|
176
181
|
"amend-supersede-instead",
|
|
177
182
|
"record-sign-failed",
|
|
178
183
|
"ratify-quorum-not-met",
|
|
184
|
+
"record-conflict",
|
|
185
|
+
"write-lock-timeout",
|
|
186
|
+
"write-lock-not-held",
|
|
179
187
|
"write-scope-member",
|
|
180
188
|
"write-scope-kind",
|
|
181
189
|
"write-scope-class-unknown",
|
|
@@ -199,6 +207,76 @@
|
|
|
199
207
|
}
|
|
200
208
|
}
|
|
201
209
|
},
|
|
210
|
+
"conflict": {
|
|
211
|
+
"type": "object",
|
|
212
|
+
"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.",
|
|
213
|
+
"required": [
|
|
214
|
+
"id",
|
|
215
|
+
"path",
|
|
216
|
+
"expected",
|
|
217
|
+
"digest",
|
|
218
|
+
"lastWrite"
|
|
219
|
+
],
|
|
220
|
+
"properties": {
|
|
221
|
+
"id": {
|
|
222
|
+
"type": "string"
|
|
223
|
+
},
|
|
224
|
+
"path": {
|
|
225
|
+
"type": "string",
|
|
226
|
+
"description": "The record file, from the repository root."
|
|
227
|
+
},
|
|
228
|
+
"expected": {
|
|
229
|
+
"type": "string",
|
|
230
|
+
"description": "The digest given with --expect."
|
|
231
|
+
},
|
|
232
|
+
"digest": {
|
|
233
|
+
"type": "string",
|
|
234
|
+
"pattern": "^[0-9a-f]{64}$",
|
|
235
|
+
"description": "The record's digest now."
|
|
236
|
+
},
|
|
237
|
+
"lastWrite": {
|
|
238
|
+
"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.",
|
|
239
|
+
"oneOf": [
|
|
240
|
+
{
|
|
241
|
+
"type": "null"
|
|
242
|
+
},
|
|
243
|
+
{
|
|
244
|
+
"type": "object",
|
|
245
|
+
"required": [
|
|
246
|
+
"verb",
|
|
247
|
+
"by",
|
|
248
|
+
"agent",
|
|
249
|
+
"at"
|
|
250
|
+
],
|
|
251
|
+
"properties": {
|
|
252
|
+
"verb": {
|
|
253
|
+
"type": "string",
|
|
254
|
+
"description": "The write command, such as records amend or points answer."
|
|
255
|
+
},
|
|
256
|
+
"by": {
|
|
257
|
+
"type": [
|
|
258
|
+
"string",
|
|
259
|
+
"null"
|
|
260
|
+
],
|
|
261
|
+
"description": "The principal the write named, or null when it named none."
|
|
262
|
+
},
|
|
263
|
+
"agent": {
|
|
264
|
+
"type": [
|
|
265
|
+
"string",
|
|
266
|
+
"null"
|
|
267
|
+
],
|
|
268
|
+
"description": "The agent session it was made in (CHANT_AGENT), or null."
|
|
269
|
+
},
|
|
270
|
+
"at": {
|
|
271
|
+
"type": "string",
|
|
272
|
+
"description": "When, as an ISO 8601 time."
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
]
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
},
|
|
202
280
|
"path": false
|
|
203
281
|
}
|
|
204
282
|
}
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
*/
|
|
24
24
|
|
|
25
25
|
import { execFileSync } from "node:child_process";
|
|
26
|
-
import { realpathSync } from "node:fs";
|
|
26
|
+
import { readFileSync, realpathSync } from "node:fs";
|
|
27
27
|
import { dirname, join, relative, resolve, sep } from "node:path";
|
|
28
28
|
import { formatError } from "../cli/format";
|
|
29
29
|
import type { CommandContext } from "../cli/registry";
|
|
@@ -40,6 +40,7 @@ import {
|
|
|
40
40
|
loadRecordKind,
|
|
41
41
|
normalisePrincipal,
|
|
42
42
|
readRecords,
|
|
43
|
+
readSessionSubjects,
|
|
43
44
|
RECORD_SEAL_FIELD,
|
|
44
45
|
RecordReadError,
|
|
45
46
|
type LoadedRecordKind,
|
|
@@ -54,6 +55,15 @@ import {
|
|
|
54
55
|
type VerdictAttestation,
|
|
55
56
|
} from "./records";
|
|
56
57
|
import { readCheckoutHead, worktreeStates, type CheckoutView, type WorktreeState } from "./records-checkout";
|
|
58
|
+
import { lastWriteReader, type LastWrite } from "./write-lock";
|
|
59
|
+
|
|
60
|
+
function readTextOrNull(file: string): string | null {
|
|
61
|
+
try {
|
|
62
|
+
return readFileSync(file, "utf-8");
|
|
63
|
+
} catch {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
57
67
|
import { gitTree, workingTree, type WorkspaceTree } from "./tree";
|
|
58
68
|
import type { DecisionWork, WorkAnswer } from "./work";
|
|
59
69
|
import { activeAttestors, type CommitAttestor, type ProvenanceLevel } from "./trust/attestor";
|
|
@@ -122,6 +132,12 @@ export type RecordView = RecordEntry & {
|
|
|
122
132
|
* (`modified`), or not in it (`new`), staged or not. Absent under `--at`.
|
|
123
133
|
*/
|
|
124
134
|
worktree?: WorktreeState;
|
|
135
|
+
/**
|
|
136
|
+
* Beside `worktree` (#3173): the last chant write of the record's file, from
|
|
137
|
+
* the write journal (`write-lock.ts`), while the file holds the text it
|
|
138
|
+
* left, or null.
|
|
139
|
+
*/
|
|
140
|
+
lastWrite?: LastWrite | null;
|
|
125
141
|
/**
|
|
126
142
|
* For a work kind whose work block names an answer kind (#3147): the
|
|
127
143
|
* decision-point answers about the item, the answer records whose
|
|
@@ -372,11 +388,7 @@ export async function readRecordsFor(
|
|
|
372
388
|
if (at === null && query.overlay) source = await query.overlay(source, { loaded, root });
|
|
373
389
|
const history = top ? gitHistory(top, at ?? "HEAD", workspaceRoot) : undefined;
|
|
374
390
|
// A session kind's verdicts name records of another kind, read from the same tree (#2673).
|
|
375
|
-
|
|
376
|
-
if (loaded.kind.session) {
|
|
377
|
-
const subjectKind = await loadRecordKind(resolve(dirname(loaded.file), loaded.kind.session.subjects.kind), cwd);
|
|
378
|
-
subjects = { records: (await readRecords(subjectKind, { root, source })).records, reviews: subjectKind.kind.reviews?.field ?? "reviews" };
|
|
379
|
-
}
|
|
391
|
+
const subjects = await readSessionSubjects(loaded, root, source);
|
|
380
392
|
const result = await readRecords(loaded, { root, source, current: !!query.current, assets, workspaceRoot, ...(history ? { history } : {}), ...(subjects ? { subjects } : {}) });
|
|
381
393
|
return { loaded, root, top, at, workspaceRoot, tree: assets, result };
|
|
382
394
|
}
|
|
@@ -451,6 +463,8 @@ export async function queryRecords(query: RecordsQuery): Promise<RecordsDocument
|
|
|
451
463
|
const dir = relative(root, loaded.dir).split(sep).join("/") || ".";
|
|
452
464
|
const { states, deleted } = worktreeStates(top, head, dir, records.map((r) => r.path), new RegExp(loaded.kind.location.match));
|
|
453
465
|
for (const r of records) r.worktree = states.get(r.path) ?? "new";
|
|
466
|
+
const lastWrite = lastWriteReader(top);
|
|
467
|
+
for (const r of records) r.lastWrite = lastWrite(r.path, readTextOrNull(join(top, ...r.path.split("/"))));
|
|
454
468
|
checkout = { branch: rest.branch, head, base: rest.base, baseFrom: rest.baseFrom, deleted };
|
|
455
469
|
if (query.uncommitted) {
|
|
456
470
|
listed = records.filter((r) => r.worktree !== "committed");
|
|
@@ -108,6 +108,11 @@
|
|
|
108
108
|
"$ref": "#/$defs/warning"
|
|
109
109
|
}
|
|
110
110
|
},
|
|
111
|
+
"digest": {
|
|
112
|
+
"type": "string",
|
|
113
|
+
"pattern": "^[0-9a-f]{64}$",
|
|
114
|
+
"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."
|
|
115
|
+
},
|
|
111
116
|
"text": {
|
|
112
117
|
"type": "string",
|
|
113
118
|
"description": "With --dry-run only: the whole text of the file the command would write."
|
|
@@ -171,6 +176,9 @@
|
|
|
171
176
|
"write-usage-invalid",
|
|
172
177
|
"record-not-found",
|
|
173
178
|
"record-closed",
|
|
179
|
+
"record-conflict",
|
|
180
|
+
"write-lock-timeout",
|
|
181
|
+
"write-lock-not-held",
|
|
174
182
|
"write-scope-member",
|
|
175
183
|
"write-scope-kind",
|
|
176
184
|
"write-scope-class-unknown",
|
|
@@ -193,6 +201,76 @@
|
|
|
193
201
|
}
|
|
194
202
|
}
|
|
195
203
|
},
|
|
204
|
+
"conflict": {
|
|
205
|
+
"type": "object",
|
|
206
|
+
"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.",
|
|
207
|
+
"required": [
|
|
208
|
+
"id",
|
|
209
|
+
"path",
|
|
210
|
+
"expected",
|
|
211
|
+
"digest",
|
|
212
|
+
"lastWrite"
|
|
213
|
+
],
|
|
214
|
+
"properties": {
|
|
215
|
+
"id": {
|
|
216
|
+
"type": "string"
|
|
217
|
+
},
|
|
218
|
+
"path": {
|
|
219
|
+
"type": "string",
|
|
220
|
+
"description": "The record file, from the repository root."
|
|
221
|
+
},
|
|
222
|
+
"expected": {
|
|
223
|
+
"type": "string",
|
|
224
|
+
"description": "The digest given with --expect."
|
|
225
|
+
},
|
|
226
|
+
"digest": {
|
|
227
|
+
"type": "string",
|
|
228
|
+
"pattern": "^[0-9a-f]{64}$",
|
|
229
|
+
"description": "The record's digest now."
|
|
230
|
+
},
|
|
231
|
+
"lastWrite": {
|
|
232
|
+
"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.",
|
|
233
|
+
"oneOf": [
|
|
234
|
+
{
|
|
235
|
+
"type": "null"
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
"type": "object",
|
|
239
|
+
"required": [
|
|
240
|
+
"verb",
|
|
241
|
+
"by",
|
|
242
|
+
"agent",
|
|
243
|
+
"at"
|
|
244
|
+
],
|
|
245
|
+
"properties": {
|
|
246
|
+
"verb": {
|
|
247
|
+
"type": "string",
|
|
248
|
+
"description": "The write command, such as records amend or points answer."
|
|
249
|
+
},
|
|
250
|
+
"by": {
|
|
251
|
+
"type": [
|
|
252
|
+
"string",
|
|
253
|
+
"null"
|
|
254
|
+
],
|
|
255
|
+
"description": "The principal the write named, or null when it named none."
|
|
256
|
+
},
|
|
257
|
+
"agent": {
|
|
258
|
+
"type": [
|
|
259
|
+
"string",
|
|
260
|
+
"null"
|
|
261
|
+
],
|
|
262
|
+
"description": "The agent session it was made in (CHANT_AGENT), or null."
|
|
263
|
+
},
|
|
264
|
+
"at": {
|
|
265
|
+
"type": "string",
|
|
266
|
+
"description": "When, as an ISO 8601 time."
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
]
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
},
|
|
196
274
|
"path": false
|
|
197
275
|
}
|
|
198
276
|
}
|
|
@@ -10,10 +10,11 @@
|
|
|
10
10
|
* in place and writes only when the session comes back valid: a verdict
|
|
11
11
|
* naming a record the subjects lack is refused with
|
|
12
12
|
* `session-verdict-unknown-record`. A session already closed is
|
|
13
|
-
* `record-closed`. It writes one file or none and never commits.
|
|
13
|
+
* `record-closed`. It writes one file or none and never commits. Like them
|
|
14
|
+
* it holds the working tree's write lock, and takes `--expect <digest>`
|
|
15
|
+
* (#3173).
|
|
14
16
|
*/
|
|
15
17
|
|
|
16
|
-
import { writeFileSync } from "node:fs";
|
|
17
18
|
import type { ReasonCode } from "./reason-codes";
|
|
18
19
|
import { sessionSeal } from "./record-sessions";
|
|
19
20
|
import { RECORD_REASON_CODES } from "./records";
|
|
@@ -28,12 +29,15 @@ import {
|
|
|
28
29
|
RECORDS_WRITE_CONTRACT_VERSION,
|
|
29
30
|
RecordWriteError,
|
|
30
31
|
refuseOutOfScope,
|
|
32
|
+
refuseStale,
|
|
31
33
|
replaceFields,
|
|
32
34
|
stableJson,
|
|
33
|
-
|
|
35
|
+
validatedEntry,
|
|
36
|
+
writeWho,
|
|
34
37
|
type WriteFailure,
|
|
35
38
|
type WriteResult,
|
|
36
39
|
} from "./records-write";
|
|
40
|
+
import { noteWrites, withWriteLock, writeFileAtomic, WRITE_LOCK_CODES } from "./write-lock";
|
|
37
41
|
import { headCommit } from "./session-kinds";
|
|
38
42
|
import { WRITE_SCOPE_CODES } from "./write-scope";
|
|
39
43
|
|
|
@@ -45,6 +49,8 @@ export const CLOSE_ERROR_CODES = [
|
|
|
45
49
|
"write-usage-invalid",
|
|
46
50
|
"record-not-found",
|
|
47
51
|
"record-closed",
|
|
52
|
+
"record-conflict",
|
|
53
|
+
...WRITE_LOCK_CODES,
|
|
48
54
|
...WRITE_SCOPE_CODES,
|
|
49
55
|
...RECORD_REASON_CODES,
|
|
50
56
|
] as const satisfies readonly ReasonCode[];
|
|
@@ -71,6 +77,8 @@ export interface CloseRecordOptions {
|
|
|
71
77
|
now?: Date;
|
|
72
78
|
/** The agent session the write is made in (#2548). */
|
|
73
79
|
agent?: string;
|
|
80
|
+
/** The session's digest the caller last read (#3173): the close is refused with record-conflict when it has another. */
|
|
81
|
+
expect?: string;
|
|
74
82
|
}
|
|
75
83
|
|
|
76
84
|
/** An ISO 8601 time in UTC to the second, as the reference session schema's dateTime takes it. */
|
|
@@ -78,8 +86,19 @@ function isoSeconds(d: Date): string {
|
|
|
78
86
|
return d.toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
79
87
|
}
|
|
80
88
|
|
|
81
|
-
/** `records close`: close one open session and seal it. */
|
|
89
|
+
/** `records close`: close one open session and seal it, holding the write lock (#3173). */
|
|
82
90
|
export async function closeRecord(opts: CloseRecordOptions): Promise<CloseDocument> {
|
|
91
|
+
try {
|
|
92
|
+
if (opts.expect !== undefined && !/^[0-9a-f]{64}$/.test(opts.expect)) {
|
|
93
|
+
throw new RecordWriteError("write-usage-invalid", `--expect takes a record's digest, 64 lowercase hex digits as records --json prints it, not ${JSON.stringify(opts.expect)}`);
|
|
94
|
+
}
|
|
95
|
+
return await withWriteLock(opts.cwd, writeWho("records close", opts), opts.dryRun, () => closeRecordLocked(opts));
|
|
96
|
+
} catch (err) {
|
|
97
|
+
return failure<CloseErrorCode>(RECORDS_CLOSE_SCHEMA_ID, err);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function closeRecordLocked(opts: CloseRecordOptions): Promise<CloseDocument> {
|
|
83
102
|
try {
|
|
84
103
|
const o = await open(opts.kind, opts.cwd);
|
|
85
104
|
refuseOutOfScope(o, "close", opts.cwd, opts);
|
|
@@ -92,6 +111,7 @@ export async function closeRecord(opts: CloseRecordOptions): Promise<CloseDocume
|
|
|
92
111
|
if (closedState === undefined) throw new RecordWriteError("write-usage-invalid", `the ${kind.name} kind lists no closed state, so a session of it can't close`);
|
|
93
112
|
const before = await readAll(o, o.source);
|
|
94
113
|
const target = findRecord(before, opts.id, kind.name);
|
|
114
|
+
refuseStale(o, target, opts.id, opts.expect);
|
|
95
115
|
if (target.state !== null && (kind.closedStates ?? []).includes(target.state)) {
|
|
96
116
|
throw new RecordWriteError("record-closed", `${opts.id} is already ${target.state}, and a closed session is sealed and stays as it is`);
|
|
97
117
|
}
|
|
@@ -115,8 +135,11 @@ export async function closeRecord(opts: CloseRecordOptions): Promise<CloseDocume
|
|
|
115
135
|
if (text === undefined || sessionSeal(text, decl.seal, kind.format) !== digest) {
|
|
116
136
|
throw new RecordWriteError("record-unparseable", `${target.path}: the ${decl.seal} line can't be added without changing the text it seals`);
|
|
117
137
|
}
|
|
118
|
-
const
|
|
119
|
-
if (!opts.dryRun)
|
|
138
|
+
const entry = await validatedEntry(o, before, target.path, text);
|
|
139
|
+
if (!opts.dryRun) {
|
|
140
|
+
writeFileAtomic(abs(o, target.path), text);
|
|
141
|
+
noteWrites(o.root, [{ path: target.path, text }], writeWho("records close", opts));
|
|
142
|
+
}
|
|
120
143
|
const written = { ...merged, [decl.seal]: digest };
|
|
121
144
|
return {
|
|
122
145
|
$schema: RECORDS_CLOSE_SCHEMA_ID,
|
|
@@ -128,7 +151,8 @@ export async function closeRecord(opts: CloseRecordOptions): Promise<CloseDocume
|
|
|
128
151
|
seal: { field: decl.seal, digest },
|
|
129
152
|
closedRev,
|
|
130
153
|
dryRun: !!opts.dryRun,
|
|
131
|
-
warnings,
|
|
154
|
+
warnings: entry.warnings,
|
|
155
|
+
digest: entry.digest,
|
|
132
156
|
...(opts.dryRun ? { text } : {}),
|
|
133
157
|
};
|
|
134
158
|
} catch (err) {
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Concurrent writers in one working tree through the command line (#3173,
|
|
3
|
+
* ws-089): three `records amend --expect` processes started at once against
|
|
4
|
+
* one record, two people's and an agent's (CHANT_AGENT). Exactly one wins;
|
|
5
|
+
* the others exit 1 with record-conflict and the digest to retry from, and the
|
|
6
|
+
* retry wins. Then a batch: `lock acquire` holds the lock, a write run with
|
|
7
|
+
* CHANT_WRITE_LOCK goes ahead, and one without it is refused with
|
|
8
|
+
* write-lock-timeout until `lock release`. The in-process cases are in
|
|
9
|
+
* records-concurrent.test.ts.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { execFile } from "node:child_process";
|
|
13
|
+
import { readFileSync } from "node:fs";
|
|
14
|
+
import { join } from "node:path";
|
|
15
|
+
import { pathToFileURL } from "node:url";
|
|
16
|
+
import { afterAll, beforeAll, describe, expect, test } from "vitest";
|
|
17
|
+
import { cleanScratch, git, REPO, repo } from "./__fixtures__/contract-repo";
|
|
18
|
+
import { parseFrontMatter } from "./records";
|
|
19
|
+
import { renderRecord } from "./records-write";
|
|
20
|
+
|
|
21
|
+
const DECISIONS = join(REPO, "docs", "design", "decisions");
|
|
22
|
+
const KIND = "decisions/decision.kind.mjs";
|
|
23
|
+
type Data = Record<string, unknown>;
|
|
24
|
+
|
|
25
|
+
const SAMPLE = (() => {
|
|
26
|
+
const fm = parseFrontMatter(readFileSync(join(DECISIONS, "ws-003-seal-scope.md"), "utf-8"));
|
|
27
|
+
if (!fm.ok) throw new Error(fm.message);
|
|
28
|
+
return fm.value;
|
|
29
|
+
})();
|
|
30
|
+
const proposal = (over: Data): Data => ({ ...structuredClone(SAMPLE), state: "proposed", choice: null, decided_by: null, decided_on: null, ...over });
|
|
31
|
+
|
|
32
|
+
let root: string;
|
|
33
|
+
beforeAll(() => {
|
|
34
|
+
root = repo(
|
|
35
|
+
{
|
|
36
|
+
"chant.workspace.json": JSON.stringify({
|
|
37
|
+
name: "box",
|
|
38
|
+
schema: 1,
|
|
39
|
+
members: [{ name: "app", dir: "app", kind: "other", because: "a plain Node server" }],
|
|
40
|
+
records: [{ kind: KIND }],
|
|
41
|
+
writeScope: { agent: { records: { decision: ["new", "amend", "review"] } } },
|
|
42
|
+
agents: [{ name: "app-agent", member: "app" }],
|
|
43
|
+
}),
|
|
44
|
+
[KIND]: readFileSync(join(DECISIONS, "decision.kind.mjs"), "utf-8"),
|
|
45
|
+
"decisions/decision.schema.json": readFileSync(join(DECISIONS, "decision.schema.json"), "utf-8"),
|
|
46
|
+
"decisions/ws-001-one.md": renderRecord(proposal({ id: "ws-001", title: "One" }), "\n# One\n"),
|
|
47
|
+
"app/server.mjs": "export const port = 8080;\n",
|
|
48
|
+
},
|
|
49
|
+
true,
|
|
50
|
+
);
|
|
51
|
+
git(root, "branch", "-M", "main");
|
|
52
|
+
});
|
|
53
|
+
afterAll(cleanScratch);
|
|
54
|
+
|
|
55
|
+
/** Run chant in the repository, without waiting on the others. */
|
|
56
|
+
function chant(args: string[], opts: { input?: string; env?: Record<string, string> } = {}): Promise<{ status: number; doc: Record<string, any> }> {
|
|
57
|
+
const loader = pathToFileURL(join(REPO, "node_modules", "tsx", "dist", "loader.mjs")).href;
|
|
58
|
+
return new Promise((resolve, reject) => {
|
|
59
|
+
const child = execFile(
|
|
60
|
+
process.execPath,
|
|
61
|
+
["--import", loader, join(REPO, "packages", "core", "src", "cli", "main.ts"), "workspace", ...args],
|
|
62
|
+
{ cwd: root, encoding: "utf-8", env: { ...process.env, TSX_DISABLE_CACHE: "1", ...opts.env } },
|
|
63
|
+
(err, stdout, stderr) => {
|
|
64
|
+
try {
|
|
65
|
+
resolve({ status: err ? Number((err as { code?: number }).code ?? 1) : 0, doc: JSON.parse(stdout) });
|
|
66
|
+
} catch {
|
|
67
|
+
reject(new Error(`chant ${args.join(" ")} printed no JSON: ${stderr}`));
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
);
|
|
71
|
+
child.stdin!.end(opts.input ?? "");
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const amend = (fields: Data, expect: string, env?: Record<string, string>) => chant(["records", "amend", "ws-001", "--kind", KIND, "--set", "-", "--expect", expect], { input: JSON.stringify(fields), env });
|
|
76
|
+
|
|
77
|
+
describe("concurrent writers through the command line (#3173)", () => {
|
|
78
|
+
test(
|
|
79
|
+
"three amends from one digest in three processes: one wins, two conflict, and their retries win",
|
|
80
|
+
async () => {
|
|
81
|
+
const read = await chant(["records", "--kind", KIND, "--json"]);
|
|
82
|
+
const d0 = read.doc.records.find((r: { id: string }) => r.id === "ws-001").digest as string;
|
|
83
|
+
const runs = await Promise.all([
|
|
84
|
+
amend({ title: "One, as alice has it" }, d0),
|
|
85
|
+
amend({ question: "What does bob ask?" }, d0),
|
|
86
|
+
amend({ constrains: ["member:app"] }, d0, { CHANT_AGENT: "app-agent" }),
|
|
87
|
+
]);
|
|
88
|
+
const won = runs.filter((r) => r.status === 0);
|
|
89
|
+
const lost = runs.filter((r) => r.status !== 0);
|
|
90
|
+
expect(won).toHaveLength(1);
|
|
91
|
+
expect(lost.map((r) => r.doc.error.code)).toEqual(["record-conflict", "record-conflict"]);
|
|
92
|
+
const d1 = won[0].doc.digest as string;
|
|
93
|
+
for (const r of lost) expect(r.doc.conflict).toMatchObject({ expected: d0, digest: d1, lastWrite: { verb: "records amend" } });
|
|
94
|
+
|
|
95
|
+
// Each loser retries from what it was told, one after the other.
|
|
96
|
+
const fieldsOf = (i: number) => [{ title: "One, as alice has it" }, { question: "What does bob ask?" }, { constrains: ["member:app"] }][i];
|
|
97
|
+
let digest = d1;
|
|
98
|
+
for (const r of lost) {
|
|
99
|
+
const i = runs.indexOf(r);
|
|
100
|
+
const retry = await amend(fieldsOf(i), digest, i === 2 ? { CHANT_AGENT: "app-agent" } : undefined);
|
|
101
|
+
expect(retry.status).toBe(0);
|
|
102
|
+
digest = retry.doc.digest;
|
|
103
|
+
}
|
|
104
|
+
const end = await chant(["records", "--kind", KIND, "--json"]);
|
|
105
|
+
const rec = end.doc.records.find((r: { id: string }) => r.id === "ws-001");
|
|
106
|
+
expect(rec.digest).toBe(digest);
|
|
107
|
+
expect(rec.data).toMatchObject({ title: "One, as alice has it", question: "What does bob ask?", constrains: ["member:app"] });
|
|
108
|
+
expect(rec.lastWrite).toMatchObject({ verb: "records amend" });
|
|
109
|
+
},
|
|
110
|
+
120_000,
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
test(
|
|
114
|
+
"a batch holds the lock across calls: its own writes go ahead, another writer's waits and is refused",
|
|
115
|
+
async () => {
|
|
116
|
+
const taken = await chant(["lock", "acquire", "--holder", "hud:alice", "--ttl", "60s"]);
|
|
117
|
+
expect(taken).toMatchObject({ status: 0, doc: { verb: "acquire", holder: { verb: "batch", by: "hud:alice" } } });
|
|
118
|
+
const token = taken.doc.token as string;
|
|
119
|
+
const status = await chant(["lock"]);
|
|
120
|
+
expect(status.doc.holder).toMatchObject({ by: "hud:alice" });
|
|
121
|
+
expect(status.doc.token).toBeUndefined();
|
|
122
|
+
|
|
123
|
+
const outsider = await chant(["records", "review", "ws-001", "--kind", KIND, "--verdict", "agree", "--by", "bob"], { env: { CHANT_WRITE_LOCK_WAIT_MS: "300" } });
|
|
124
|
+
expect(outsider).toMatchObject({ status: 1, doc: { error: { code: "write-lock-timeout" } } });
|
|
125
|
+
expect(outsider.doc.error.message).toContain("hud:alice");
|
|
126
|
+
const inside = await chant(["records", "review", "ws-001", "--kind", KIND, "--verdict", "agree", "--by", "alice"], { env: { CHANT_WRITE_LOCK: token } });
|
|
127
|
+
expect(inside.status).toBe(0);
|
|
128
|
+
|
|
129
|
+
expect((await chant(["lock", "release", "--token", token])).status).toBe(0);
|
|
130
|
+
const after = await chant(["records", "review", "ws-001", "--kind", KIND, "--verdict", "agree", "--by", "bob"]);
|
|
131
|
+
expect(after.status).toBe(0);
|
|
132
|
+
},
|
|
133
|
+
120_000,
|
|
134
|
+
);
|
|
135
|
+
});
|