@intentius/chant 0.101.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/agents/checks.d.ts.map +1 -1
- package/dist/attrref.d.ts.map +1 -1
- 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/audit/nginx.d.ts.map +1 -1
- package/dist/audit/prior-art.d.ts +48 -0
- package/dist/audit/prior-art.d.ts.map +1 -1
- package/dist/behaviour-kinds.d.ts.map +1 -1
- package/dist/builder.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/commands/carve-emit.d.ts.map +1 -1
- package/dist/cli/commands/check-lexicon.d.ts +16 -0
- package/dist/cli/commands/check-lexicon.d.ts.map +1 -1
- package/dist/cli/commands/import.d.ts +8 -0
- package/dist/cli/commands/import.d.ts.map +1 -1
- package/dist/cli/commands/init-lexicon.d.ts.map +1 -1
- package/dist/cli/commands/init.d.ts +19 -0
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/onboard.d.ts +9 -0
- package/dist/cli/commands/onboard.d.ts.map +1 -1
- package/dist/cli/commands/pinned-upgrade.d.ts +19 -5
- package/dist/cli/commands/pinned-upgrade.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/dev.d.ts.map +1 -1
- 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/lifecycle.d.ts.map +1 -1
- package/dist/cli/handlers/misc.d.ts.map +1 -1
- package/dist/cli/handlers/operator.d.ts +16 -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/lsp/server.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/server.d.ts.map +1 -1
- package/dist/cli/mcp/tools/build.d.ts.map +1 -1
- package/dist/cli/mcp/tools/composites.d.ts.map +1 -1
- package/dist/cli/mcp/tools/explain.d.ts.map +1 -1
- package/dist/cli/mcp/tools/import.d.ts.map +1 -1
- package/dist/cli/mcp/tools/lint.d.ts.map +1 -1
- package/dist/cli/mcp/tools/scaffold.d.ts.map +1 -1
- package/dist/cli/mcp/tools/search.d.ts.map +1 -1
- package/dist/cli/mcp/workspace-tools.d.ts +4 -3
- package/dist/cli/mcp/workspace-tools.d.ts.map +1 -1
- package/dist/cli/registry.d.ts +74 -3
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/codegen/fetch.d.ts.map +1 -1
- package/dist/codegen/naming.d.ts.map +1 -1
- package/dist/codegen/pinned-upgrade.d.ts +67 -3
- package/dist/codegen/pinned-upgrade.d.ts.map +1 -1
- package/dist/codegen/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-plugin-loader.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/starter-plugin.d.ts.map +1 -1
- package/dist/components/verbs/process-runner.d.ts.map +1 -1
- package/dist/components/verbs/r2-sync.d.ts.map +1 -1
- package/dist/components/verbs/run-agent.d.ts.map +1 -1
- package/dist/components/verbs/sbom-generator.d.ts.map +1 -1
- package/dist/components/verbs/shell.d.ts.map +1 -1
- package/dist/components/verbs/sign.d.ts.map +1 -1
- package/dist/components/verbs/verify.d.ts.map +1 -1
- package/dist/components/verbs/vuln-gate.d.ts.map +1 -1
- package/dist/components/verbs/vuln-scan.d.ts.map +1 -1
- package/dist/components/verbs/wrangler.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/composite.d.ts.map +1 -1
- package/dist/config.d.ts +19 -7
- package/dist/config.d.ts.map +1 -1
- package/dist/declarable.d.ts +16 -0
- package/dist/declarable.d.ts.map +1 -1
- package/dist/discovery/cache.d.ts.map +1 -1
- package/dist/discovery/fold-import.d.ts +15 -5
- package/dist/discovery/fold-import.d.ts.map +1 -1
- package/dist/discovery/param-deps.d.ts +16 -0
- package/dist/discovery/param-deps.d.ts.map +1 -1
- package/dist/discovery/sandbox/policy-wire.d.ts.map +1 -1
- package/dist/discovery/specifier-resolve.d.ts +37 -0
- package/dist/discovery/specifier-resolve.d.ts.map +1 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/fold/fold.d.ts +3 -1
- package/dist/fold/fold.d.ts.map +1 -1
- package/dist/gated-waves.d.ts +147 -0
- package/dist/gated-waves.d.ts.map +1 -0
- package/dist/graph-detail.d.ts.map +1 -1
- package/dist/graph-ir.d.ts.map +1 -1
- package/dist/graph-layout.d.ts.map +1 -1
- package/dist/import/base-parser.d.ts.map +1 -1
- package/dist/import/embedded.d.ts +7 -0
- package/dist/import/embedded.d.ts.map +1 -1
- package/dist/import/parser-options.d.ts +14 -0
- package/dist/import/parser-options.d.ts.map +1 -0
- package/dist/import/parser.d.ts +17 -0
- package/dist/import/parser.d.ts.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/kubectl-context.d.ts.map +1 -1
- package/dist/lexicon-output.d.ts.map +1 -1
- package/dist/lexicon-schema.d.ts +3 -3
- package/dist/lexicon.d.ts +103 -3
- package/dist/lexicon.d.ts.map +1 -1
- package/dist/lifecycle/assert-live.d.ts.map +1 -1
- package/dist/lifecycle/gate-ledger.d.ts +22 -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/lifecycle/member-ledger.d.ts.map +1 -1
- package/dist/lifecycle/observe.d.ts.map +1 -1
- package/dist/lifecycle/plan-ledger.d.ts.map +1 -1
- package/dist/lifecycle/release-ledger.d.ts.map +1 -1
- package/dist/lifecycle/scenario.d.ts.map +1 -1
- package/dist/lifecycle/status.d.ts.map +1 -1
- package/dist/lifecycle/work-lease.d.ts +61 -2
- package/dist/lifecycle/work-lease.d.ts.map +1 -1
- package/dist/lint/config.d.ts +8 -8
- package/dist/lint/engine.d.ts +6 -0
- package/dist/lint/engine.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/lsp/lexicon-providers.d.ts.map +1 -1
- package/dist/op/activities/activity-contracts.d.ts +137 -11
- package/dist/op/activities/activity-contracts.d.ts.map +1 -1
- package/dist/op/activities/apply.d.ts +79 -9
- package/dist/op/activities/apply.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 +5 -1
- package/dist/op/activities/index.d.ts.map +1 -1
- package/dist/op/activities/lexicon-upgrade.d.ts +44 -12
- package/dist/op/activities/lexicon-upgrade.d.ts.map +1 -1
- package/dist/op/activity-contract.d.ts +1 -1
- package/dist/op/activity-contract.d.ts.map +1 -1
- package/dist/op/activity-failure.d.ts.map +1 -1
- package/dist/op/activity-profiles.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/composites/apply-op.d.ts +12 -5
- package/dist/op/composites/apply-op.d.ts.map +1 -1
- package/dist/op/composites/lexicon-upgrade-op.d.ts +22 -6
- package/dist/op/composites/lexicon-upgrade-op.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/emulator-lifecycle.d.ts +21 -2
- package/dist/op/emulator-lifecycle.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/gate-approval.d.ts +5 -1
- package/dist/op/gate-approval.d.ts.map +1 -1
- package/dist/op/gate-wait.d.ts.map +1 -1
- package/dist/op/gate.d.ts +19 -3
- package/dist/op/gate.d.ts.map +1 -1
- package/dist/op/index.d.ts +6 -1
- package/dist/op/index.d.ts.map +1 -1
- package/dist/op/local-executor.d.ts +6 -0
- package/dist/op/local-executor.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/operator.d.ts.map +1 -1
- package/dist/op/run-context.d.ts +39 -0
- package/dist/op/run-context.d.ts.map +1 -0
- package/dist/op/run-live.d.ts.map +1 -1
- package/dist/op/runtimes/local.d.ts.map +1 -1
- package/dist/op/source-archive.d.ts.map +1 -1
- package/dist/op/steward-beside.d.ts.map +1 -1
- package/dist/op/steward-points.d.ts.map +1 -1
- package/dist/op/steward.d.ts.map +1 -1
- package/dist/op/types.d.ts +16 -3
- package/dist/op/types.d.ts.map +1 -1
- package/dist/op/work-lease-run.d.ts +24 -4
- 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/project-root.d.ts.map +1 -1
- package/dist/provenance.d.ts +21 -0
- package/dist/provenance.d.ts.map +1 -1
- package/dist/pseudo-parameter.d.ts.map +1 -1
- package/dist/reconcile.d.ts.map +1 -1
- package/dist/secret-materialization.d.ts.map +1 -1
- package/dist/telemetry-attribution.d.ts +31 -0
- package/dist/telemetry-attribution.d.ts.map +1 -1
- package/dist/terraform/emit-source.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/testing.d.ts.map +1 -1
- package/dist/workspace/__fixtures__/contract-repo.d.ts.map +1 -1
- package/dist/workspace/__fixtures__/decide-workspace.d.ts +1 -1
- package/dist/workspace/__fixtures__/decide-workspace.d.ts.map +1 -1
- package/dist/workspace/agent-cli.d.ts +5 -0
- package/dist/workspace/agent-cli.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 +97 -0
- package/dist/workspace/box-factory.d.ts.map +1 -0
- 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/changes.d.ts.map +1 -1
- package/dist/workspace/chant-migrations/chud-lexicon-exit.d.ts.map +1 -1
- package/dist/workspace/checks/boxes.d.ts.map +1 -1
- package/dist/workspace/checks.d.ts +5 -0
- package/dist/workspace/checks.d.ts.map +1 -1
- package/dist/workspace/compose-graph.d.ts +10 -0
- package/dist/workspace/compose-graph.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 +68 -6
- package/dist/workspace/conformance/index.d.ts.map +1 -1
- package/dist/workspace/conformance/vitest.d.ts +19 -3
- package/dist/workspace/conformance/vitest.d.ts.map +1 -1
- package/dist/workspace/conformance/writer.d.ts +462 -0
- package/dist/workspace/conformance/writer.d.ts.map +1 -0
- 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 +156 -4
- package/dist/workspace/declaration.d.ts.map +1 -1
- package/dist/workspace/declaration.schema.json +383 -5
- 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/generated-files.d.ts.map +1 -1
- package/dist/workspace/graph-cache.d.ts.map +1 -1
- package/dist/workspace/graph-cli.d.ts +2 -0
- package/dist/workspace/graph-cli.d.ts.map +1 -1
- package/dist/workspace/identity.d.ts +111 -0
- package/dist/workspace/identity.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 +7 -4
- package/dist/workspace/intent-cli.d.ts.map +1 -1
- package/dist/workspace/intent-record.d.ts +36 -2
- package/dist/workspace/intent-record.d.ts.map +1 -1
- package/dist/workspace/intent.d.ts +249 -8
- 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 +66 -4
- 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-hash-index.d.ts.map +1 -1
- package/dist/workspace/lineage-lock.d.ts +2 -2
- package/dist/workspace/lineage-lock.d.ts.map +1 -1
- package/dist/workspace/lineage-upgrade.d.ts +31 -2
- package/dist/workspace/lineage-upgrade.d.ts.map +1 -1
- package/dist/workspace/links.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/principal-classes.d.ts +114 -0
- package/dist/workspace/principal-classes.d.ts.map +1 -0
- package/dist/workspace/reason-codes.d.ts +53 -4
- package/dist/workspace/reason-codes.d.ts.map +1 -1
- package/dist/workspace/record-assets.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-checkout.d.ts +54 -0
- package/dist/workspace/records-checkout.d.ts.map +1 -0
- package/dist/workspace/records-cli.d.ts +34 -1
- 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-since.d.ts.map +1 -1
- package/dist/workspace/records-write.d.ts +68 -7
- package/dist/workspace/records-write.d.ts.map +1 -1
- package/dist/workspace/records.d.ts +43 -9
- package/dist/workspace/records.d.ts.map +1 -1
- package/dist/workspace/runs-cli.d.ts +296 -0
- package/dist/workspace/runs-cli.d.ts.map +1 -0
- package/dist/workspace/runs.d.ts +544 -0
- package/dist/workspace/runs.d.ts.map +1 -0
- package/dist/workspace/session-kinds.d.ts +1 -1
- package/dist/workspace/session-kinds.d.ts.map +1 -1
- package/dist/workspace/source-block.d.ts +1 -1
- package/dist/workspace/source-block.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 +17 -3
- package/dist/workspace/status-gates.d.ts.map +1 -1
- package/dist/workspace/status.d.ts +64 -0
- package/dist/workspace/status.d.ts.map +1 -1
- package/dist/workspace/symbols.d.ts +63 -0
- package/dist/workspace/symbols.d.ts.map +1 -0
- package/dist/workspace/trailer-joins.d.ts +46 -0
- package/dist/workspace/trailer-joins.d.ts.map +1 -0
- package/dist/workspace/trailers.d.ts +89 -0
- package/dist/workspace/trailers.d.ts.map +1 -0
- package/dist/workspace/trust/attestor.d.ts.map +1 -1
- package/dist/workspace/trust/rotation.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 +38 -0
- package/dist/workspace/trust/seal.d.ts.map +1 -1
- package/dist/workspace/trust/test-repo.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 +39 -3
- 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/work.d.ts +26 -1
- package/dist/workspace/work.d.ts.map +1 -1
- package/dist/workspace/workspace-kinds.schema.json +122 -0
- package/dist/workspace/workspace-principals.schema.json +60 -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 +83 -19
- package/dist/workspace/write-scope.d.ts.map +1 -1
- package/dist/yaml.d.ts +21 -39
- package/dist/yaml.d.ts.map +1 -1
- package/package.json +7 -2
- 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/audit/prior-art.ts +15 -0
- 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/__fixtures__/init-lexicon-output/package.json +1 -1
- 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/commands/carve-emit-state.test.ts +4 -0
- package/src/cli/commands/carve-emit.ts +5 -19
- package/src/cli/commands/check-lexicon.test.ts +62 -1
- package/src/cli/commands/check-lexicon.ts +36 -6
- package/src/cli/commands/import-live.test.ts +18 -0
- package/src/cli/commands/import-parser-options.test.ts +133 -0
- package/src/cli/commands/import.ts +21 -3
- package/src/cli/commands/init-lexicon/templates/project.ts +1 -1
- package/src/cli/commands/init.test.ts +8 -1
- package/src/cli/commands/init.ts +23 -8
- package/src/cli/commands/lint.test.ts +37 -0
- package/src/cli/commands/onboard.test.ts +53 -3
- package/src/cli/commands/onboard.ts +52 -4
- package/src/cli/commands/pinned-upgrade.ts +73 -33
- package/src/cli/handlers/__snapshots__/build-member.test.ts.snap +2 -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/components.ts +2 -2
- package/src/cli/handlers/dev.ts +8 -6
- 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/lifecycle.test.ts +67 -0
- package/src/cli/handlers/lifecycle.ts +6 -5
- package/src/cli/handlers/misc.ts +2 -0
- package/src/cli/handlers/operator.test.ts +21 -0
- package/src/cli/handlers/operator.ts +121 -6
- package/src/cli/handlers/pr.ts +373 -0
- package/src/cli/handlers/run.test.ts +111 -0
- package/src/cli/handlers/run.ts +86 -24
- package/src/cli/handlers/scenario.ts +2 -2
- package/src/cli/main.test.ts +21 -0
- package/src/cli/main.ts +246 -30
- 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 +5 -2
- package/src/cli/mcp/workspace-tools.ts +62 -7
- package/src/cli/registry.ts +74 -3
- package/src/codegen/pinned-upgrade-multi.test.ts +157 -0
- package/src/codegen/pinned-upgrade-tags.test.ts +112 -0
- package/src/codegen/pinned-upgrade.test.ts +104 -0
- package/src/codegen/pinned-upgrade.ts +250 -56
- package/src/codegen/testdata/postgres-tags.json +242 -0
- 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/config.test.ts +21 -0
- package/src/config.ts +15 -0
- package/src/declarable.test.ts +21 -0
- package/src/declarable.ts +19 -0
- package/src/discovery/fold-composite.test.ts +41 -5
- package/src/discovery/fold-import.test.ts +129 -0
- package/src/discovery/fold-import.ts +643 -131
- package/src/discovery/fold-package-composite.test.ts +264 -0
- package/src/discovery/fold-specifiers.test.ts +191 -0
- package/src/discovery/param-deps.test.ts +66 -1
- package/src/discovery/param-deps.ts +45 -0
- package/src/discovery/sandbox/bundle.ts +1 -1
- package/src/discovery/specifier-resolve.ts +344 -0
- package/src/fold/fold.test.ts +39 -1
- package/src/fold/fold.ts +83 -9
- package/src/fold-provenance.test.ts +140 -0
- 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/graph-ir.test.ts +83 -0
- package/src/graph-ir.ts +29 -0
- package/src/import/embedded.test.ts +20 -0
- package/src/import/embedded.ts +13 -0
- package/src/import/parser-options.ts +46 -0
- package/src/import/parser.ts +20 -0
- package/src/index.ts +1 -0
- package/src/lexicon.ts +106 -3
- package/src/lifecycle/gate-ledger.ts +23 -0
- package/src/lifecycle/git.ts +102 -16
- package/src/lifecycle/observe.test.ts +18 -0
- package/src/lifecycle/observe.ts +6 -4
- package/src/lifecycle/snapshot.ts +2 -2
- package/src/lifecycle/work-lease.test.ts +1 -1
- package/src/lifecycle/work-lease.ts +93 -2
- package/src/lint/engine.test.ts +55 -0
- package/src/lint/engine.ts +30 -4
- package/src/lint/output-docs.test.ts +59 -56
- 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/apply.test.ts +189 -5
- package/src/op/activities/apply.ts +230 -8
- 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 +9 -0
- package/src/op/activities/lexicon-upgrade.test.ts +160 -6
- package/src/op/activities/lexicon-upgrade.ts +154 -51
- 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/composites/apply-op.ts +12 -5
- package/src/op/composites/composites.test.ts +30 -3
- package/src/op/composites/lexicon-upgrade-op.test.ts +7 -2
- package/src/op/composites/lexicon-upgrade-op.ts +29 -8
- package/src/op/decide-backend.ts +5 -1
- package/src/op/decide-read-inputs.ts +33 -5
- package/src/op/emulator-lifecycle.test.ts +23 -0
- package/src/op/emulator-lifecycle.ts +52 -14
- package/src/op/factory.e2e.test.ts +232 -0
- package/src/op/factory.ts +118 -0
- package/src/op/gate-approval.test.ts +74 -0
- package/src/op/gate-approval.ts +5 -1
- package/src/op/gate.ts +31 -5
- package/src/op/index.ts +11 -1
- package/src/op/local-executor.test.ts +15 -0
- package/src/op/local-executor.ts +40 -12
- package/src/op/local-output.ts +1 -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/operator.ts +4 -0
- package/src/op/run-context.test.ts +141 -0
- package/src/op/run-context.ts +51 -0
- package/src/op/runtimes/local.test.ts +14 -0
- package/src/op/runtimes/local.ts +2 -0
- package/src/op/steward-beside.ts +2 -0
- package/src/op/types.ts +16 -3
- package/src/op/work-lease-run.test.ts +47 -0
- package/src/op/work-lease-run.ts +76 -11
- 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/provenance.ts +35 -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/agent-cli.ts +7 -3
- package/src/workspace/agent.schema.json +22 -0
- package/src/workspace/app-kind.test.ts +196 -0
- package/src/workspace/box-cli.ts +113 -0
- package/src/workspace/box-factory.test.ts +323 -0
- package/src/workspace/box-factory.ts +136 -0
- 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/changes.schema.json +12 -20
- package/src/workspace/changes.ts +4 -1
- package/src/workspace/chant-migrations.e2e.test.ts +17 -5
- package/src/workspace/checks/boxes.test.ts +5 -0
- package/src/workspace/checks.ts +39 -8
- package/src/workspace/component-environments.test.ts +105 -0
- package/src/workspace/compose-graph.ts +7 -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 +32 -0
- package/src/workspace/conformance/__writer_fixture__/answers/answer.schema.json +135 -0
- 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 +228 -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 +54 -0
- package/src/workspace/conformance/__writer_fixture__/sessions/session.schema.json +289 -0
- package/src/workspace/conformance/__writer_fixture__/work/W-001-the-writer-suite-works-this-item.md +25 -0
- package/src/workspace/conformance/__writer_fixture__/work/work.kind.mjs +60 -0
- package/src/workspace/conformance/__writer_fixture__/work/work.schema.json +603 -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 +36 -3
- package/src/workspace/conformance/index.mjs +51 -3
- package/src/workspace/conformance/index.ts +107 -15
- package/src/workspace/conformance/vitest.ts +76 -2
- package/src/workspace/conformance/writer-conformance.e2e.test.ts +177 -0
- package/src/workspace/conformance/writer.test.ts +186 -0
- package/src/workspace/conformance/writer.ts +1191 -0
- package/src/workspace/decide.test.ts +208 -4
- package/src/workspace/decide.ts +433 -73
- package/src/workspace/decision-points.schema.json +11 -4
- package/src/workspace/declaration.schema.json +383 -5
- package/src/workspace/declaration.ts +315 -10
- 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 +6 -3
- package/src/workspace/graph.schema.json +314 -0
- package/src/workspace/identity.test.ts +219 -0
- package/src/workspace/identity.ts +186 -0
- package/src/workspace/init.ts +82 -2
- package/src/workspace/intent-cli.ts +84 -13
- package/src/workspace/intent-patch-id.test.ts +162 -0
- package/src/workspace/intent-record.schema.json +336 -1
- package/src/workspace/intent-record.ts +96 -16
- 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 +274 -0
- package/src/workspace/intent.schema.json +950 -9
- package/src/workspace/intent.test.ts +11 -2
- package/src/workspace/intent.ts +722 -65
- 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 +201 -39
- 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 +163 -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/principal-classes.test.ts +175 -0
- package/src/workspace/principal-classes.ts +307 -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 +23 -3
- package/src/workspace/reason-codes.ts +61 -4
- package/src/workspace/record-sessions.ts +9 -8
- package/src/workspace/records-amend.schema.json +80 -0
- package/src/workspace/records-checkout.ts +106 -0
- package/src/workspace/records-cli.ts +123 -17
- package/src/workspace/records-close.schema.json +79 -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 +9 -0
- package/src/workspace/records-review.schema.json +80 -0
- package/src/workspace/records-sessions-write.test.ts +69 -0
- package/src/workspace/records-sessions.test.ts +22 -0
- package/src/workspace/records-uncommitted.test.ts +171 -0
- package/src/workspace/records-write.ts +219 -30
- package/src/workspace/records.schema.json +88 -1
- package/src/workspace/records.ts +76 -8
- 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 +710 -0
- package/src/workspace/runs-statement.test.ts +239 -0
- package/src/workspace/runs-write.schema.json +994 -0
- package/src/workspace/runs.schema.json +1257 -0
- package/src/workspace/runs.test.ts +281 -0
- package/src/workspace/runs.ts +970 -0
- package/src/workspace/session-kinds.ts +3 -3
- package/src/workspace/squash.ts +198 -0
- package/src/workspace/status-contract.test.ts +10 -2
- package/src/workspace/status-gates.ts +25 -9
- package/src/workspace/status.schema.json +252 -4
- package/src/workspace/status.ts +110 -1
- package/src/workspace/symbols.ts +191 -0
- package/src/workspace/trailer-joins.test.ts +171 -0
- package/src/workspace/trailer-joins.ts +82 -0
- package/src/workspace/trailers.ts +148 -0
- package/src/workspace/trust/run-commits.ts +115 -0
- package/src/workspace/trust/run-statement.ts +226 -0
- package/src/workspace/trust/seal.ts +58 -1
- 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.e2e.test.ts +28 -1
- package/src/workspace/work-cli.ts +136 -8
- package/src/workspace/work-evidence.schema.json +5 -0
- package/src/workspace/work-history.schema.json +73 -1
- package/src/workspace/work-history.test.ts +48 -5
- package/src/workspace/work-kind-fields.test.ts +154 -0
- package/src/workspace/work-lease.schema.json +14 -1
- package/src/workspace/work-size.test.ts +77 -0
- package/src/workspace/work-size.ts +93 -0
- package/src/workspace/work.ts +82 -0
- package/src/workspace/workspace-kinds.schema.json +122 -0
- package/src/workspace/workspace-principals.schema.json +60 -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 +144 -3
- package/src/workspace/write-scope.ts +192 -45
- package/src/yaml.test.ts +111 -1
- package/src/yaml.ts +141 -549
|
@@ -0,0 +1,1191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workspace writer conformance (#3159, ws-074).
|
|
3
|
+
*
|
|
4
|
+
* The repo is the database (ws-074): every durable fact about a workspace is
|
|
5
|
+
* a file in the repository or a line on its ledger branch, written only
|
|
6
|
+
* through chant's write contract, and a tool keeps nothing but secrets,
|
|
7
|
+
* telemetry, caches and its substrate's runtime state outside it. The reader
|
|
8
|
+
* suite beside this module holds a reader to the read contract. This one
|
|
9
|
+
* holds a writer, such as hud or studio's factory, to the write contract:
|
|
10
|
+
*
|
|
11
|
+
* 1. Writes go through chant. The suite drives a script of writes
|
|
12
|
+
* ({@link WRITER_SCRIPT}) through the writer's `write(step)`. Each step
|
|
13
|
+
* must make exactly one chant call, the step's write-contract command
|
|
14
|
+
* with its arguments (and its JSON flag, where the command has one),
|
|
15
|
+
* giving chant the step's fields on stdin. The document the writer
|
|
16
|
+
* returns is the one chant printed, it validates against the command's
|
|
17
|
+
* output schema, and it is not a refusal.
|
|
18
|
+
* 2. Every change is chant's. The workspace's files (outside `.git`) and its
|
|
19
|
+
* git refs are read before and after each step. A file may change only
|
|
20
|
+
* when it is the path the command reports writing, and a ref only when
|
|
21
|
+
* it is the lease ref or the `chant/lifecycle` commit the command
|
|
22
|
+
* reports.
|
|
23
|
+
* 3. Amnesia. The writer is given a state directory of its own, outside
|
|
24
|
+
* the workspace, and declares what it keeps there (`privateState`). After
|
|
25
|
+
* the script the suite asks the writer for the facts it shows
|
|
26
|
+
* (`facts()`), closes it, deletes everything in the state directory,
|
|
27
|
+
* builds the writer again and asks again: the two answers must be equal.
|
|
28
|
+
* The suite also reads every fact the script produced back through the
|
|
29
|
+
* read contract, the uncommitted records included.
|
|
30
|
+
* 4. No facts outside the repo. A file left in the state directory that
|
|
31
|
+
* `privateState` does not declare is a problem, and so is an entry of
|
|
32
|
+
* the writer's optional `holds()` that names a record, run or lease the
|
|
33
|
+
* repository does not have, unless it names itself one of the four
|
|
34
|
+
* exceptions.
|
|
35
|
+
*
|
|
36
|
+
* 5. Concurrent writes (#3173, ws-089). When the writer performs `records
|
|
37
|
+
* amend`, the suite has it make three amendments of the script's
|
|
38
|
+
* decision at once, each from the digest the suite read
|
|
39
|
+
* (`--expect <digest>`), as two people and an agent would. Exactly one
|
|
40
|
+
* must be written; the other two must come back as chant's
|
|
41
|
+
* record-conflict refusal, naming the winner's digest, returned to the
|
|
42
|
+
* suite unchanged rather than retried blindly or hidden. The suite then
|
|
43
|
+
* has the writer retry each from the digest its refusal named, and each
|
|
44
|
+
* retry must be written. See {@link CONCURRENT_AMENDS}.
|
|
45
|
+
*
|
|
46
|
+
* `facts()` may only read: every chant call it makes must be a read-contract
|
|
47
|
+
* command, and it must leave the workspace as it was.
|
|
48
|
+
*
|
|
49
|
+
* The workspace is generated for each run, from the reader suite's fixture
|
|
50
|
+
* and the `__writer_fixture__/` overlay this package ships beside this module:
|
|
51
|
+
* the reference workspace's work, answer and session kinds (with the
|
|
52
|
+
* contract and driver kinds a session's verdicts may also name), its
|
|
53
|
+
* decision points, and one open work item, W-001. Nothing is written
|
|
54
|
+
* anywhere else.
|
|
55
|
+
*
|
|
56
|
+
* Actions a writer does not list in `actions` are not applicable to it. The
|
|
57
|
+
* suite still performs those steps itself, directly through chant, so a later
|
|
58
|
+
* step that depends on one (an amend on the decision a `records new` made,
|
|
59
|
+
* an evidence entry under a claimed lease) has what it needs.
|
|
60
|
+
*/
|
|
61
|
+
|
|
62
|
+
import { execFileSync } from "node:child_process";
|
|
63
|
+
import { createHash } from "node:crypto";
|
|
64
|
+
import { cpSync, existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
65
|
+
import { createRequire } from "node:module";
|
|
66
|
+
import { tmpdir } from "node:os";
|
|
67
|
+
import { dirname, join, resolve } from "node:path";
|
|
68
|
+
import { fileURLToPath } from "node:url";
|
|
69
|
+
import { isDeepStrictEqual } from "node:util";
|
|
70
|
+
import { createConformanceWorkspace, defaultChantCommand, runChant, treeChanges, type ChantRun, type ChantTransport } from "./index";
|
|
71
|
+
|
|
72
|
+
// Nothing at the top level of this module may use a binding imported from
|
|
73
|
+
// ./index: index re-exports this module, so this module is evaluated first.
|
|
74
|
+
|
|
75
|
+
/** The write-contract commands a writer performs, as the suite names them. */
|
|
76
|
+
export const WRITE_CONTRACT_ACTIONS = [
|
|
77
|
+
"records new",
|
|
78
|
+
"records amend",
|
|
79
|
+
"records review",
|
|
80
|
+
"records close",
|
|
81
|
+
"points ask",
|
|
82
|
+
"points answer",
|
|
83
|
+
"points retract",
|
|
84
|
+
"work claim",
|
|
85
|
+
"work renew",
|
|
86
|
+
"work evidence",
|
|
87
|
+
"work release",
|
|
88
|
+
"runs start",
|
|
89
|
+
"runs end",
|
|
90
|
+
"runs record",
|
|
91
|
+
"box listing set",
|
|
92
|
+
"wip save",
|
|
93
|
+
"wip restore",
|
|
94
|
+
] as const;
|
|
95
|
+
export type WriteContractAction = (typeof WRITE_CONTRACT_ACTIONS)[number];
|
|
96
|
+
|
|
97
|
+
/** Each action's output schema, in `src/workspace/` beside this module's directory. */
|
|
98
|
+
export const WRITE_CONTRACT_SCHEMAS: Record<WriteContractAction, string> = {
|
|
99
|
+
"records new": "records-new.schema.json",
|
|
100
|
+
"records amend": "records-amend.schema.json",
|
|
101
|
+
"records review": "records-review.schema.json",
|
|
102
|
+
"records close": "records-close.schema.json",
|
|
103
|
+
"points ask": "points-write.schema.json",
|
|
104
|
+
"points answer": "points-write.schema.json",
|
|
105
|
+
"points retract": "points-write.schema.json",
|
|
106
|
+
"work claim": "work-lease.schema.json",
|
|
107
|
+
"work renew": "work-lease.schema.json",
|
|
108
|
+
"work evidence": "work-evidence.schema.json",
|
|
109
|
+
"work release": "work-lease.schema.json",
|
|
110
|
+
"runs start": "runs-write.schema.json",
|
|
111
|
+
"runs end": "runs-write.schema.json",
|
|
112
|
+
"runs record": "runs-write.schema.json",
|
|
113
|
+
"box listing set": "box-listing-write.schema.json",
|
|
114
|
+
"wip save": "wip-write.schema.json",
|
|
115
|
+
"wip restore": "wip-write.schema.json",
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* The flags a writer may add to each command. The record, points, evidence,
|
|
120
|
+
* run and listing writes always print their document, so a writer adds
|
|
121
|
+
* nothing or `--json`; a lease prints its document only with `--json`.
|
|
122
|
+
*/
|
|
123
|
+
export const WRITE_CONTRACT_JSON_FLAGS: Record<WriteContractAction, readonly (readonly string[])[]> = {
|
|
124
|
+
"records new": [[], ["--json"]],
|
|
125
|
+
"records amend": [[], ["--json"]],
|
|
126
|
+
"records review": [[], ["--json"]],
|
|
127
|
+
"records close": [[], ["--json"]],
|
|
128
|
+
"points ask": [[], ["--json"]],
|
|
129
|
+
"points answer": [[], ["--json"]],
|
|
130
|
+
"points retract": [[], ["--json"]],
|
|
131
|
+
"work claim": [["--json"]],
|
|
132
|
+
"work renew": [["--json"]],
|
|
133
|
+
"work evidence": [[], ["--json"]],
|
|
134
|
+
"work release": [["--json"]],
|
|
135
|
+
"runs start": [[], ["--json"]],
|
|
136
|
+
"runs end": [[], ["--json"]],
|
|
137
|
+
"runs record": [[], ["--json"]],
|
|
138
|
+
"box listing set": [[], ["--json"]],
|
|
139
|
+
"wip save": [[], ["--json"]],
|
|
140
|
+
"wip restore": [[], ["--json"]],
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
/** What each action is given. Field documents are JSON values; the command reads them from stdin. */
|
|
144
|
+
export interface WriteParams {
|
|
145
|
+
"records new": { kind: string; fields: Record<string, unknown> };
|
|
146
|
+
/** `expect` (#3173): the record's digest the write builds on, passed as `--expect`. */
|
|
147
|
+
"records amend": { id: string; kind: string; fields: Record<string, unknown>; expect?: string };
|
|
148
|
+
"records review": { id: string; kind: string; verdict: "agree" | "dissent" | "abstain"; by: string; note?: string };
|
|
149
|
+
"records close": { id: string; kind: string };
|
|
150
|
+
/** `candidates` is an ad-hoc point's question and candidates (#3403), `{ question, criteria }`: they go on stdin, and the inputs as JSON on the command line. */
|
|
151
|
+
"points ask": { point: string; kind: string; inputs: Record<string, unknown>; subject?: string; candidates?: { question: string; criteria: Record<string, string> | string[] } };
|
|
152
|
+
"points answer": { id: string; kind: string; answer: string; by: string; note?: string };
|
|
153
|
+
"points retract": { id: string; kind: string; by: string; note?: string };
|
|
154
|
+
"work claim": { id: string; kind: string; holder: string };
|
|
155
|
+
"work renew": { id: string; kind: string; holder: string; token: string };
|
|
156
|
+
"work evidence": { id: string; kind: string; holder: string; token: string; entry: Record<string, unknown> };
|
|
157
|
+
"work release": { id: string; kind: string; holder: string; token: string; outcome: string };
|
|
158
|
+
"runs start": { run: Record<string, unknown> };
|
|
159
|
+
"runs end": { id: string; fields: Record<string, unknown> };
|
|
160
|
+
"runs record": { run: Record<string, unknown> };
|
|
161
|
+
/** A box's listing (#3308): the member, the listing fields, and an image to copy in as its cover. */
|
|
162
|
+
"box listing set": { member: string; fields: Record<string, unknown>; cover?: string };
|
|
163
|
+
/** A snapshot of the working tree, uncommitted records included, onto refs/chant/wip/<branch> (#3172): what the caller calls it, and who took it. */
|
|
164
|
+
"wip save": { label?: string; by?: string };
|
|
165
|
+
/** Put the working tree back as a snapshot holds it (#3172). */
|
|
166
|
+
"wip restore": { snapshot: string; by?: string };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** One write the suite asks of the writer: the action, what it is given, and the command that performs it. */
|
|
170
|
+
export type WriteStep = {
|
|
171
|
+
[A in WriteContractAction]: {
|
|
172
|
+
/** The step's name in the script, such as `review`. */
|
|
173
|
+
id: string;
|
|
174
|
+
action: A;
|
|
175
|
+
params: WriteParams[A];
|
|
176
|
+
/** The arguments after `chant workspace <action>`, as {@link writeArgv} builds them from `params`. */
|
|
177
|
+
args: string[];
|
|
178
|
+
/** What the command reads on stdin (`--from -`, `--set -`, `--inputs -`, `--candidates -`), when it reads anything. */
|
|
179
|
+
input?: string;
|
|
180
|
+
};
|
|
181
|
+
}[WriteContractAction];
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* The arguments that perform `action` with `params`, after `chant workspace
|
|
185
|
+
* <action>`, and the JSON the command reads on stdin. A writer can build its
|
|
186
|
+
* calls with this, or with its own code that makes the same call.
|
|
187
|
+
*/
|
|
188
|
+
export function writeArgv<A extends WriteContractAction>(action: A, params: WriteParams[A]): { args: string[]; input?: string } {
|
|
189
|
+
const p = params as WriteParams[WriteContractAction] & Record<string, unknown>;
|
|
190
|
+
const json = (v: unknown) => JSON.stringify(v);
|
|
191
|
+
switch (action) {
|
|
192
|
+
case "records new": {
|
|
193
|
+
const q = p as WriteParams["records new"];
|
|
194
|
+
return { args: [q.kind, "--from", "-"], input: json(q.fields) };
|
|
195
|
+
}
|
|
196
|
+
case "records amend": {
|
|
197
|
+
const q = p as WriteParams["records amend"];
|
|
198
|
+
return { args: [q.id, "--kind", q.kind, "--set", "-", ...(q.expect !== undefined ? ["--expect", q.expect] : [])], input: json(q.fields) };
|
|
199
|
+
}
|
|
200
|
+
case "records review": {
|
|
201
|
+
const q = p as WriteParams["records review"];
|
|
202
|
+
return { args: [q.id, "--kind", q.kind, "--verdict", q.verdict, "--by", q.by, ...(q.note !== undefined ? ["--note", q.note] : [])] };
|
|
203
|
+
}
|
|
204
|
+
case "records close": {
|
|
205
|
+
const q = p as WriteParams["records close"];
|
|
206
|
+
return { args: [q.id, "--kind", q.kind] };
|
|
207
|
+
}
|
|
208
|
+
case "points ask": {
|
|
209
|
+
const q = p as WriteParams["points ask"];
|
|
210
|
+
const subject = q.subject !== undefined ? ["--subject", q.subject] : [];
|
|
211
|
+
// An ad-hoc ask (#3403): the question and candidates read from stdin, and the inputs given as JSON.
|
|
212
|
+
if (q.candidates !== undefined) return { args: [q.point, "--inputs", json(q.inputs), "--candidates", "-", ...subject, "--kind", q.kind], input: json(q.candidates) };
|
|
213
|
+
return { args: [q.point, "--inputs", "-", ...subject, "--kind", q.kind], input: json(q.inputs) };
|
|
214
|
+
}
|
|
215
|
+
case "points answer": {
|
|
216
|
+
const q = p as WriteParams["points answer"];
|
|
217
|
+
return { args: [q.id, "--answer", q.answer, "--by", q.by, ...(q.note !== undefined ? ["--note", q.note] : []), "--kind", q.kind] };
|
|
218
|
+
}
|
|
219
|
+
case "points retract": {
|
|
220
|
+
const q = p as WriteParams["points retract"];
|
|
221
|
+
return { args: [q.id, "--by", q.by, ...(q.note !== undefined ? ["--note", q.note] : []), "--kind", q.kind] };
|
|
222
|
+
}
|
|
223
|
+
case "work claim": {
|
|
224
|
+
const q = p as WriteParams["work claim"];
|
|
225
|
+
return { args: [q.id, "--holder", q.holder, "--kind", q.kind] };
|
|
226
|
+
}
|
|
227
|
+
case "work renew": {
|
|
228
|
+
const q = p as WriteParams["work renew"];
|
|
229
|
+
return { args: [q.id, "--holder", q.holder, "--token", q.token, "--kind", q.kind] };
|
|
230
|
+
}
|
|
231
|
+
case "work evidence": {
|
|
232
|
+
const q = p as WriteParams["work evidence"];
|
|
233
|
+
return { args: [q.id, "--holder", q.holder, "--token", q.token, "--from", "-", "--kind", q.kind], input: json(q.entry) };
|
|
234
|
+
}
|
|
235
|
+
case "work release": {
|
|
236
|
+
const q = p as WriteParams["work release"];
|
|
237
|
+
return { args: [q.id, "--holder", q.holder, "--token", q.token, "--outcome", q.outcome, "--kind", q.kind] };
|
|
238
|
+
}
|
|
239
|
+
case "runs start":
|
|
240
|
+
case "runs record": {
|
|
241
|
+
const q = p as WriteParams["runs start"];
|
|
242
|
+
return { args: ["--from", "-"], input: json(q.run) };
|
|
243
|
+
}
|
|
244
|
+
case "runs end": {
|
|
245
|
+
const q = p as WriteParams["runs end"];
|
|
246
|
+
return { args: [q.id, "--from", "-"], input: json(q.fields) };
|
|
247
|
+
}
|
|
248
|
+
case "box listing set": {
|
|
249
|
+
const q = p as WriteParams["box listing set"];
|
|
250
|
+
return { args: [q.member, "--from", "-", ...(q.cover !== undefined ? ["--cover", q.cover] : [])], input: json(q.fields) };
|
|
251
|
+
}
|
|
252
|
+
case "wip save": {
|
|
253
|
+
const q = p as WriteParams["wip save"];
|
|
254
|
+
return { args: [...(q.label !== undefined ? ["--label", q.label] : []), ...(q.by !== undefined ? ["--by", q.by] : [])] };
|
|
255
|
+
}
|
|
256
|
+
case "wip restore": {
|
|
257
|
+
const q = p as WriteParams["wip restore"];
|
|
258
|
+
return { args: [q.snapshot, ...(q.by !== undefined ? ["--by", q.by] : [])] };
|
|
259
|
+
}
|
|
260
|
+
default:
|
|
261
|
+
throw new Error(`not a write-contract action: ${String(action)}`);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** The kind files of the writer workspace, from its root. */
|
|
266
|
+
export const WRITER_KINDS = {
|
|
267
|
+
decision: "decisions/decision.kind.mjs",
|
|
268
|
+
session: "sessions/session.kind.mjs",
|
|
269
|
+
answer: "answers/answer.kind.mjs",
|
|
270
|
+
work: "work/work.kind.mjs",
|
|
271
|
+
} as const;
|
|
272
|
+
|
|
273
|
+
/** The member of the writer workspace whose box block the script lists (#3308). */
|
|
274
|
+
export const WRITER_BOX = "app";
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* The files the suite makes for a run outside the workspace, for a step to
|
|
278
|
+
* hand chant: the cover image the listing step copies in (#3308), a PNG.
|
|
279
|
+
*/
|
|
280
|
+
export const WRITER_INPUTS = {
|
|
281
|
+
cover: {
|
|
282
|
+
name: "cover.png",
|
|
283
|
+
bytes: Buffer.from("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==", "base64"),
|
|
284
|
+
},
|
|
285
|
+
} as const;
|
|
286
|
+
|
|
287
|
+
/** Where the suite put {@link WRITER_INPUTS} for this run. */
|
|
288
|
+
export interface WriterInputs {
|
|
289
|
+
dir: string;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/** Write {@link WRITER_INPUTS} into `dir`. */
|
|
293
|
+
export function writeWriterInputs(dir: string): WriterInputs {
|
|
294
|
+
mkdirSync(dir, { recursive: true });
|
|
295
|
+
for (const f of Object.values(WRITER_INPUTS)) writeFileSync(join(dir, f.name), f.bytes);
|
|
296
|
+
return { dir };
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/** The principals the script writes as. */
|
|
300
|
+
export const WRITER_PRINCIPALS = { holder: "conformance-writer", reviewer: "conformance-reviewer", by: "conformance" } as const;
|
|
301
|
+
|
|
302
|
+
/** One step of the script: its name, its action, and its params from the documents of the steps before it. */
|
|
303
|
+
export interface WriterScriptStep<A extends WriteContractAction = WriteContractAction> {
|
|
304
|
+
id: string;
|
|
305
|
+
action: A;
|
|
306
|
+
/** `inputs` is where the suite put {@link WRITER_INPUTS}; a step that hands chant one of them leaves it out without. */
|
|
307
|
+
params(done: Record<string, Record<string, unknown>>, inputs?: WriterInputs): WriteParams[A];
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const step = <A extends WriteContractAction>(id: string, action: A, params: (done: Record<string, Record<string, unknown>>, inputs?: WriterInputs) => WriteParams[A]): WriterScriptStep => ({ id, action, params }) as WriterScriptStep;
|
|
311
|
+
|
|
312
|
+
const leaseToken = (done: Record<string, Record<string, unknown>>): string => String((done.claim?.lease as { token?: unknown } | undefined)?.token ?? "");
|
|
313
|
+
const RUN_STARTED = { harness: { name: "conformance", version: "1" }, model: "none", provider: "none", by: WRITER_PRINCIPALS.by, unit: "W-001" };
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* The writes the suite drives, in order, each action at least once: a
|
|
317
|
+
* decision made, amended and reviewed; a review session opened and closed; a
|
|
318
|
+
* decision point asked, answered with a note, the answer retracted and the
|
|
319
|
+
* question answered again (#3351); an ad-hoc question asked with its own
|
|
320
|
+
* question and candidates, as an agent asks the room, and answered (#3403); the work item's lease claimed, renewed,
|
|
321
|
+
* evidence attached under it and released; one run started and ended, and one
|
|
322
|
+
* recorded whole; the box's listing set, with a cover image copied in; and a
|
|
323
|
+
* snapshot of the working tree taken and restored (#3172).
|
|
324
|
+
*/
|
|
325
|
+
export const WRITER_SCRIPT: readonly WriterScriptStep[] = [
|
|
326
|
+
step("decision", "records new", () => ({
|
|
327
|
+
kind: WRITER_KINDS.decision,
|
|
328
|
+
fields: {
|
|
329
|
+
schema: 1,
|
|
330
|
+
title: "How a writer writes the workspace",
|
|
331
|
+
state: "proposed",
|
|
332
|
+
area: "delivery",
|
|
333
|
+
source: { kind: "workspace", member: "delivery" },
|
|
334
|
+
question: "How does a tool write a fact about this workspace?",
|
|
335
|
+
options: [{ id: "a", label: "through chant's write contract", how: "Each fact is one chant write command.", tradeoff: "One chant call per fact." }],
|
|
336
|
+
choice: null,
|
|
337
|
+
rejected: [],
|
|
338
|
+
supersedes: [],
|
|
339
|
+
evidence: [{ title: "INTENTIUS/chant#3159, the writer conformance suite", url: "https://github.com/INTENTIUS/chant/issues/3159" }],
|
|
340
|
+
decided_by: null,
|
|
341
|
+
decided_on: null,
|
|
342
|
+
reviews: [],
|
|
343
|
+
constrains: ["member:delivery"],
|
|
344
|
+
},
|
|
345
|
+
})),
|
|
346
|
+
step("amend", "records amend", (d) => ({ id: String(d.decision.id), kind: WRITER_KINDS.decision, fields: { title: "How a writer writes the workspace, through chant" } })),
|
|
347
|
+
step("review", "records review", (d) => ({ id: String(d.decision.id), kind: WRITER_KINDS.decision, verdict: "agree", by: WRITER_PRINCIPALS.reviewer, note: "Written through chant, read back after amnesia." })),
|
|
348
|
+
step("session", "records new", (d) => ({
|
|
349
|
+
kind: WRITER_KINDS.session,
|
|
350
|
+
fields: {
|
|
351
|
+
schema: 1,
|
|
352
|
+
id: "S-0001",
|
|
353
|
+
title: "The writer suite walks its decision",
|
|
354
|
+
state: "open",
|
|
355
|
+
agenda: [{ record: String(d.decision.id) }],
|
|
356
|
+
attendance: [{ principal: WRITER_PRINCIPALS.reviewer, class: "person" }],
|
|
357
|
+
opened: "2026-01-01T00:00:00Z",
|
|
358
|
+
closed: null,
|
|
359
|
+
verdicts: [],
|
|
360
|
+
// A review comment anchored in the app, as a UI review batch keeps it (#3350).
|
|
361
|
+
comments: [
|
|
362
|
+
{
|
|
363
|
+
text: "The status line should say which build is running.",
|
|
364
|
+
by: WRITER_PRINCIPALS.reviewer,
|
|
365
|
+
anchor: { route: "/", elements: [{ selector: "#status", tag: "p", text: "Running.", state: "anchored", basis: "id" }] },
|
|
366
|
+
answers: [{ round: 0, disposition: "needs-discussion", note: "Which build: the commit or the release?", by: WRITER_PRINCIPALS.holder, at: "2026-01-01T00:02:00Z" }],
|
|
367
|
+
},
|
|
368
|
+
],
|
|
369
|
+
rounds: [{ round: 0, note: null, by: WRITER_PRINCIPALS.reviewer, at: "2026-01-01T00:01:00Z", ended: "2026-01-01T00:03:00Z" }],
|
|
370
|
+
},
|
|
371
|
+
})),
|
|
372
|
+
step("close", "records close", (d) => ({ id: String(d.session.id), kind: WRITER_KINDS.session })),
|
|
373
|
+
step("ask", "points ask", () => ({
|
|
374
|
+
point: "slice-tier",
|
|
375
|
+
kind: WRITER_KINDS.answer,
|
|
376
|
+
subject: "W-001",
|
|
377
|
+
inputs: { "work-item.criteria": 1, "work-item.files": 1, "work-item.words": 40, "work-item.fits_small": false, "work-item.fits_medium": false },
|
|
378
|
+
})),
|
|
379
|
+
step("answer", "points answer", (d) => ({ id: String(d.ask.id), kind: WRITER_KINDS.answer, answer: "medium", by: WRITER_PRINCIPALS.reviewer, note: "Two criteria and one file, but past the small limits." })),
|
|
380
|
+
step("retract", "points retract", (d) => ({ id: String(d.ask.id), kind: WRITER_KINDS.answer, by: WRITER_PRINCIPALS.reviewer, note: "Answered for the wrong work item." })),
|
|
381
|
+
step("reanswer", "points answer", (d) => ({ id: String(d.ask.id), kind: WRITER_KINDS.answer, answer: "large", by: WRITER_PRINCIPALS.reviewer })),
|
|
382
|
+
// #3403: an agent's question to the room, its text and options arriving with the ask.
|
|
383
|
+
step("adhoc", "points ask", () => ({
|
|
384
|
+
point: "agent-question",
|
|
385
|
+
kind: WRITER_KINDS.answer,
|
|
386
|
+
subject: "W-001",
|
|
387
|
+
inputs: { "ask.id": "conformance-ask-1", "ask.by": WRITER_PRINCIPALS.holder },
|
|
388
|
+
candidates: {
|
|
389
|
+
question: "Should the writer suite's item ship behind a flag?",
|
|
390
|
+
criteria: { flag: "Behind a flag, off by default.", now: "On for everyone at once." },
|
|
391
|
+
},
|
|
392
|
+
})),
|
|
393
|
+
step("adhoc-answer", "points answer", (d) => ({ id: String(d.adhoc.id), kind: WRITER_KINDS.answer, answer: "flag", by: WRITER_PRINCIPALS.reviewer })),
|
|
394
|
+
step("claim", "work claim", () => ({ id: "W-001", kind: WRITER_KINDS.work, holder: WRITER_PRINCIPALS.holder })),
|
|
395
|
+
step("renew", "work renew", (d) => ({ id: "W-001", kind: WRITER_KINDS.work, holder: WRITER_PRINCIPALS.holder, token: leaseToken(d) })),
|
|
396
|
+
step("evidence", "work evidence", (d) => ({
|
|
397
|
+
id: "W-001",
|
|
398
|
+
kind: WRITER_KINDS.work,
|
|
399
|
+
holder: WRITER_PRINCIPALS.holder,
|
|
400
|
+
token: leaseToken(d),
|
|
401
|
+
entry: { criterion: "AC-1", result: "pass", title: "The writer conformance suite's evidence step", url: "https://github.com/INTENTIUS/chant/issues/3159" },
|
|
402
|
+
})),
|
|
403
|
+
step("release", "work release", (d) => ({ id: "W-001", kind: WRITER_KINDS.work, holder: WRITER_PRINCIPALS.holder, token: leaseToken(d), outcome: "done" })),
|
|
404
|
+
step("run-start", "runs start", () => ({ run: { id: "writer-run-1", startedAt: "2026-01-01T00:00:00Z", ...RUN_STARTED } })),
|
|
405
|
+
step("run-end", "runs end", (d) => ({
|
|
406
|
+
id: String((d["run-start"].run as { id?: unknown }).id),
|
|
407
|
+
fields: { endedAt: "2026-01-01T00:05:00Z", outcome: "done", usage: { turns: 2, inputTokens: 100, outputTokens: 50 }, cost: { amount: 0.02, currency: "USD", source: "conformance" }, commits: [] },
|
|
408
|
+
})),
|
|
409
|
+
step("run-record", "runs record", () => ({
|
|
410
|
+
run: {
|
|
411
|
+
id: "writer-run-2",
|
|
412
|
+
startedAt: "2026-01-01T01:00:00Z",
|
|
413
|
+
endedAt: "2026-01-01T01:01:00Z",
|
|
414
|
+
outcome: "done",
|
|
415
|
+
usage: { turns: 1, inputTokens: 10, outputTokens: 5 },
|
|
416
|
+
cost: { amount: 0.01, currency: "USD", source: "conformance" },
|
|
417
|
+
commits: [],
|
|
418
|
+
...RUN_STARTED,
|
|
419
|
+
},
|
|
420
|
+
})),
|
|
421
|
+
step("listing", "box listing set", (_d, inputs) => ({
|
|
422
|
+
member: WRITER_BOX,
|
|
423
|
+
fields: { published: true, title: "The writer suite's box", line: "Listed through chant, read back after amnesia." },
|
|
424
|
+
...(inputs ? { cover: join(inputs.dir, WRITER_INPUTS.cover.name) } : {}),
|
|
425
|
+
})),
|
|
426
|
+
// #3172: a checkpoint of everything the script left uncommitted, and putting it back.
|
|
427
|
+
step("checkpoint", "wip save", () => ({ label: "turn:1", by: WRITER_PRINCIPALS.by })),
|
|
428
|
+
step("undo", "wip restore", (done) => ({ snapshot: String((done.checkpoint?.snapshot as { commit?: unknown } | undefined)?.commit ?? ""), by: WRITER_PRINCIPALS.by })),
|
|
429
|
+
];
|
|
430
|
+
|
|
431
|
+
/**
|
|
432
|
+
* The concurrent case (#3173): three amendments of the script's decision the
|
|
433
|
+
* writer makes at once, each setting its question, as two people and an
|
|
434
|
+
* agent answering the same record would. The suite gives each the digest it
|
|
435
|
+
* read as `expect`.
|
|
436
|
+
*/
|
|
437
|
+
export const CONCURRENT_AMENDS: readonly { id: string; fields: Record<string, unknown> }[] = [
|
|
438
|
+
{ id: "concurrent-alice", fields: { question: "How does a tool write a fact about this workspace, as alice reads it?" } },
|
|
439
|
+
{ id: "concurrent-bob", fields: { question: "How does a tool write a fact about this workspace, as bob reads it?" } },
|
|
440
|
+
{ id: "concurrent-agent", fields: { question: "How does a tool write a fact about this workspace, as the agent reads it?" } },
|
|
441
|
+
];
|
|
442
|
+
|
|
443
|
+
/** The amend step of {@link CONCURRENT_AMENDS} entry `c`, on record `id`, from digest `expect`. */
|
|
444
|
+
export function concurrentAmendStep(c: { id: string; fields: Record<string, unknown> }, id: string, expect: string): WriteStep {
|
|
445
|
+
const params: WriteParams["records amend"] = { id, kind: WRITER_KINDS.decision, fields: c.fields, expect };
|
|
446
|
+
return { id: c.id, action: "records amend", params, ...writeArgv("records amend", params) } as WriteStep;
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/** The four things ws-074 lets a tool keep outside the repo. */
|
|
450
|
+
export const PRIVATE_STATE_CATEGORIES = ["cache", "telemetry", "secret", "runtime"] as const;
|
|
451
|
+
export type PrivateStateCategory = (typeof PRIVATE_STATE_CATEGORIES)[number];
|
|
452
|
+
|
|
453
|
+
/** A file or directory the writer keeps in its state directory, and which of the four it is. */
|
|
454
|
+
export interface PrivateStatePath {
|
|
455
|
+
/** From the state directory, with / separators. A directory covers everything under it. */
|
|
456
|
+
path: string;
|
|
457
|
+
is: PrivateStateCategory | readonly PrivateStateCategory[];
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/** One thing the writer holds, as its `holds()` reports it. */
|
|
461
|
+
export type HeldItem =
|
|
462
|
+
| { record: string; kind: string }
|
|
463
|
+
| { run: string }
|
|
464
|
+
| { lease: string; kind: string }
|
|
465
|
+
| { exempt: PrivateStateCategory; what: string };
|
|
466
|
+
|
|
467
|
+
export interface WorkspaceWriter {
|
|
468
|
+
/** Perform one step: run its write-contract command through the transport, and return the parsed document chant printed. */
|
|
469
|
+
write(step: WriteStep): Promise<unknown> | unknown;
|
|
470
|
+
/**
|
|
471
|
+
* The facts the tool shows, read the way the tool reads them, as a JSON
|
|
472
|
+
* value. The suite compares the answer before and after amnesia. It may make
|
|
473
|
+
* read-contract calls only.
|
|
474
|
+
*/
|
|
475
|
+
facts(): Promise<unknown> | unknown;
|
|
476
|
+
/** Optional: what the tool holds, each entry a record, run or lease in the repo, or one of the four exceptions. */
|
|
477
|
+
holds?(): Promise<readonly HeldItem[]> | readonly HeldItem[];
|
|
478
|
+
/** Optional: let go of the state directory (close a database) before the suite deletes it. */
|
|
479
|
+
close?(): Promise<void> | void;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
export interface WriterContext {
|
|
483
|
+
/**
|
|
484
|
+
* A directory the writer keeps its private state in, such as a database
|
|
485
|
+
* file. The suite makes it, outside the workspace, and the same one is given
|
|
486
|
+
* to the writer built again after amnesia.
|
|
487
|
+
*/
|
|
488
|
+
stateDir: string;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/** Build the writer over the transport the suite gives it. Called twice: before the script, and again after amnesia. */
|
|
492
|
+
export type WorkspaceWriterFactory = (chant: ChantTransport, context: WriterContext) => WorkspaceWriter;
|
|
493
|
+
|
|
494
|
+
export interface WorkspaceWriterConformanceOptions {
|
|
495
|
+
/** The actions the writer performs. The suite performs the other steps itself. Defaults to every action. */
|
|
496
|
+
actions?: readonly WriteContractAction[];
|
|
497
|
+
/** What the writer keeps in its state directory. Anything else left there is a problem. Defaults to nothing. */
|
|
498
|
+
privateState?: readonly PrivateStatePath[];
|
|
499
|
+
/** The chant to run, as a command and its leading arguments. The reader suite's default. */
|
|
500
|
+
chantCommand?: string[];
|
|
501
|
+
/** How long one chant run may take, in milliseconds. Default 120000. */
|
|
502
|
+
timeoutMs?: number;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
export interface WorkspaceWriterConformanceConfig extends WorkspaceWriterConformanceOptions {
|
|
506
|
+
/** Short label, used in the suite name. */
|
|
507
|
+
name: string;
|
|
508
|
+
writer: WorkspaceWriterFactory;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/** What one step found. */
|
|
512
|
+
export interface WriterStepResult {
|
|
513
|
+
id: string;
|
|
514
|
+
action: WriteContractAction;
|
|
515
|
+
/** Whether the writer performed it, or the suite did because the action is not the writer's. */
|
|
516
|
+
by: "writer" | "suite";
|
|
517
|
+
args: string[];
|
|
518
|
+
problems: string[];
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/** What {@link runWorkspaceWriterConformance} found. The writer conforms when `problems` is empty. */
|
|
522
|
+
export interface WorkspaceWriterConformanceReport {
|
|
523
|
+
/** Every problem, each starting with the step it concerns, or `amnesia:`, `facts:`, `state:`, `holds:` or `read back:`. */
|
|
524
|
+
problems: string[];
|
|
525
|
+
/** The actions the writer performed, in contract order. */
|
|
526
|
+
checked: WriteContractAction[];
|
|
527
|
+
/** The actions not in `actions`, so not applicable to this writer. */
|
|
528
|
+
skipped: WriteContractAction[];
|
|
529
|
+
results: WriterStepResult[];
|
|
530
|
+
/** The problems of the checks after the script, by check. `concurrent` is empty, and not run, when the writer does not perform records amend. */
|
|
531
|
+
after: { facts: string[]; state: string[]; amnesia: string[]; holds: string[]; readBack: string[]; concurrent: string[] };
|
|
532
|
+
/** What `facts()` returned before amnesia and after. */
|
|
533
|
+
facts: { before: unknown; after: unknown };
|
|
534
|
+
/** The workspace written. Removed before the report is returned. */
|
|
535
|
+
workspaceDir: string;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
539
|
+
const workspaceSrc = resolve(here, "..");
|
|
540
|
+
/** The overlay the writer workspace adds to the reader suite's fixture, shipped under `src/`. */
|
|
541
|
+
export const WRITER_FIXTURE_DIR = join(here, "__writer_fixture__");
|
|
542
|
+
|
|
543
|
+
type Validate = ((d: unknown) => boolean) & { errors?: unknown };
|
|
544
|
+
const writeValidators = new Map<string, { validate: Validate; schema: { $id: string } }>();
|
|
545
|
+
|
|
546
|
+
/** An action's output schema and a draft 2020-12 validator for it, from this package's own ajv 8. */
|
|
547
|
+
export function writeContractSchema(action: WriteContractAction): { schema: { $id: string }; validate: Validate } {
|
|
548
|
+
const file = WRITE_CONTRACT_SCHEMAS[action];
|
|
549
|
+
let v = writeValidators.get(file);
|
|
550
|
+
if (!v) {
|
|
551
|
+
const schema = JSON.parse(readFileSync(join(workspaceSrc, file), "utf-8")) as { $id: string };
|
|
552
|
+
const mod = createRequire(import.meta.url)("ajv/dist/2020") as { default?: unknown };
|
|
553
|
+
const Ajv = (mod.default ?? mod) as new (opts: object) => { compile(s: object): Validate };
|
|
554
|
+
v = { validate: new Ajv({ strict: true, allErrors: true }).compile(schema), schema };
|
|
555
|
+
writeValidators.set(file, v);
|
|
556
|
+
}
|
|
557
|
+
return v;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/** A digest of every file under `dir`, skipping `.git` and node_modules: the working tree. */
|
|
561
|
+
export function worktreeDigest(dir: string): Record<string, string> {
|
|
562
|
+
const out: Record<string, string> = {};
|
|
563
|
+
const walk = (at: string, prefix: string) => {
|
|
564
|
+
for (const e of readdirSync(at, { withFileTypes: true })) {
|
|
565
|
+
if (e.name === "node_modules" || (prefix === "" && e.name === ".git")) continue;
|
|
566
|
+
const rel = prefix ? `${prefix}/${e.name}` : e.name;
|
|
567
|
+
if (e.isDirectory()) walk(join(at, e.name), rel);
|
|
568
|
+
else if (e.isFile()) out[rel] = createHash("sha256").update(readFileSync(join(at, e.name))).digest("hex");
|
|
569
|
+
}
|
|
570
|
+
};
|
|
571
|
+
walk(dir, "");
|
|
572
|
+
return out;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/** Every git ref in `dir` and what it points at, with `HEAD` as the branch it names and its commit. */
|
|
576
|
+
export function gitRefs(dir: string): Record<string, string> {
|
|
577
|
+
const out: Record<string, string> = {};
|
|
578
|
+
const text = execFileSync("git", ["for-each-ref", "--format=%(refname) %(objectname)"], { cwd: dir, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] });
|
|
579
|
+
for (const line of text.split("\n")) {
|
|
580
|
+
const at = line.indexOf(" ");
|
|
581
|
+
if (at > 0) out[line.slice(0, at)] = line.slice(at + 1);
|
|
582
|
+
}
|
|
583
|
+
const head = (args: string[]) => {
|
|
584
|
+
try {
|
|
585
|
+
return execFileSync("git", args, { cwd: dir, encoding: "utf-8", stdio: ["ignore", "pipe", "pipe"] }).trim();
|
|
586
|
+
} catch {
|
|
587
|
+
return "";
|
|
588
|
+
}
|
|
589
|
+
};
|
|
590
|
+
out.HEAD = `${head(["symbolic-ref", "-q", "HEAD"])} ${head(["rev-parse", "-q", "--verify", "HEAD"])}`;
|
|
591
|
+
return out;
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
/** The refs that differ between two {@link gitRefs}, with what each now points at (empty when removed). */
|
|
595
|
+
export function refChanges(before: Record<string, string>, after: Record<string, string>): Record<string, string> {
|
|
596
|
+
const out: Record<string, string> = {};
|
|
597
|
+
for (const [r, v] of Object.entries(after)) if (before[r] !== v) out[r] = v;
|
|
598
|
+
for (const r of Object.keys(before)) if (after[r] === undefined) out[r] = "";
|
|
599
|
+
return out;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
/** Whether `argv` is a read-contract call: one a reader may make, which writes nothing. */
|
|
603
|
+
export function isReadCall(argv: readonly string[]): boolean {
|
|
604
|
+
if (argv[0] !== "workspace") return false;
|
|
605
|
+
const [, verb, sub] = argv;
|
|
606
|
+
switch (verb) {
|
|
607
|
+
case "ls":
|
|
608
|
+
case "graph":
|
|
609
|
+
case "check":
|
|
610
|
+
case "status":
|
|
611
|
+
return true;
|
|
612
|
+
case "records":
|
|
613
|
+
return !["new", "amend", "review", "close", "pin"].includes(sub ?? "");
|
|
614
|
+
case "runs":
|
|
615
|
+
return !["start", "end", "record"].includes(sub ?? "");
|
|
616
|
+
case "points":
|
|
617
|
+
return !["ask", "answer", "retract"].includes(sub ?? "");
|
|
618
|
+
case "work":
|
|
619
|
+
return sub === "history";
|
|
620
|
+
case "wip":
|
|
621
|
+
return !["save", "restore", "push", "fetch"].includes(sub ?? "");
|
|
622
|
+
default:
|
|
623
|
+
return false;
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
/**
|
|
628
|
+
* Whether an argument a writer passed is the step's: the same text, or for a
|
|
629
|
+
* step argument that is JSON (an ad-hoc ask's inputs, #3403) the same value,
|
|
630
|
+
* whatever its key order or spacing.
|
|
631
|
+
*/
|
|
632
|
+
function sameArg(given: string | undefined, want: string): boolean {
|
|
633
|
+
if (given === want) return true;
|
|
634
|
+
if (given === undefined || !/^[[{]/.test(want)) return false;
|
|
635
|
+
try {
|
|
636
|
+
return isDeepStrictEqual(JSON.parse(given), JSON.parse(want));
|
|
637
|
+
} catch {
|
|
638
|
+
return false;
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
/**
|
|
643
|
+
* What is wrong with the chant calls one step made: anything but exactly
|
|
644
|
+
* one call, of `workspace <action>` with the step's arguments in order and
|
|
645
|
+
* the action's JSON flag, given the step's fields on stdin. Empty when the
|
|
646
|
+
* calls conform.
|
|
647
|
+
*/
|
|
648
|
+
export function writerCallProblems(step: WriteStep, calls: readonly ChantRun[]): string[] {
|
|
649
|
+
const name = `${step.id} (${step.action})`;
|
|
650
|
+
if (calls.length !== 1) return [`${name}: made ${calls.length} chant calls (${calls.map((c) => c.argv.join(" ")).join("; ")}), expected exactly one`];
|
|
651
|
+
const { argv, input } = calls[0];
|
|
652
|
+
const prefix = ["workspace", ...step.action.split(" ")];
|
|
653
|
+
if (prefix.some((t, i) => argv[i] !== t)) return [`${name}: ran chant ${argv.join(" ")}, which is not workspace ${step.action}`];
|
|
654
|
+
const rest = argv.slice(prefix.length);
|
|
655
|
+
const at = rest.findIndex((_, i) => step.args.every((a, j) => sameArg(rest[i + j], a)));
|
|
656
|
+
if (step.args.length > 0 && at === -1) return [`${name}: ran chant ${argv.join(" ")}, which does not pass ${step.args.join(" ")} in order`];
|
|
657
|
+
const extra = step.args.length > 0 ? [...rest.slice(0, at), ...rest.slice(at + step.args.length)] : rest;
|
|
658
|
+
const allowed = WRITE_CONTRACT_JSON_FLAGS[step.action];
|
|
659
|
+
if (!allowed.some((flags) => flags.length === extra.length && flags.every((f, i) => extra[i] === f))) {
|
|
660
|
+
return [`${name}: ran chant ${argv.join(" ")}; beyond the command and its arguments it may add only ${allowed.map((f) => (f.length ? f.join(" ") : "nothing")).join(" or ")}, and it added ${extra.join(" ") || "nothing"}`];
|
|
661
|
+
}
|
|
662
|
+
if (step.input === undefined) return input === undefined || input === "" ? [] : [`${name}: gave chant ${argv.join(" ")} something on stdin, and the command reads nothing there`];
|
|
663
|
+
let given: unknown;
|
|
664
|
+
try {
|
|
665
|
+
given = input === undefined ? undefined : JSON.parse(input);
|
|
666
|
+
} catch {
|
|
667
|
+
given = undefined;
|
|
668
|
+
}
|
|
669
|
+
return isDeepStrictEqual(given, JSON.parse(step.input)) ? [] : [`${name}: gave chant ${argv.join(" ")} other fields on stdin than the step's`];
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
/** Everything wrong with the document one step returned (`doc`), given what chant printed (`printed`). Empty when it conforms. */
|
|
673
|
+
export function writerDocumentProblems(step: WriteStep, printed: ChantRun, doc: unknown): string[] {
|
|
674
|
+
const name = `${step.id} (${step.action})`;
|
|
675
|
+
if (printed.stdout.trim() === "") return [`${name}: chant ${printed.argv.join(" ")} printed nothing${printed.stderr ? `; stderr: ${printed.stderr.trim()}` : ""}`];
|
|
676
|
+
let parsed: unknown;
|
|
677
|
+
try {
|
|
678
|
+
parsed = JSON.parse(printed.stdout);
|
|
679
|
+
} catch (e) {
|
|
680
|
+
return [`${name}: chant ${printed.argv.join(" ")} printed something that is not JSON (${(e as Error).message}); stderr: ${printed.stderr.trim()}`];
|
|
681
|
+
}
|
|
682
|
+
const problems: string[] = [];
|
|
683
|
+
if (!isDeepStrictEqual(doc, parsed)) problems.push(`${name}: the writer must return the document chant printed, unchanged, and it returned something else`);
|
|
684
|
+
const { schema, validate } = writeContractSchema(step.action);
|
|
685
|
+
if (!validate(parsed)) problems.push(`${name}: the document does not validate against ${WRITE_CONTRACT_SCHEMAS[step.action]}: ${JSON.stringify(validate.errors)}`);
|
|
686
|
+
const head = (parsed ?? {}) as { $schema?: unknown; error?: { code?: string; message?: string }; refused?: { code?: string; message?: string } };
|
|
687
|
+
if (head.$schema !== schema.$id) problems.push(`${name}: $schema is ${JSON.stringify(head.$schema)}, expected ${schema.$id}`);
|
|
688
|
+
const no = head.error ?? head.refused;
|
|
689
|
+
if (no) problems.push(`${name}: chant did not write: ${no.code}: ${no.message}`);
|
|
690
|
+
return problems;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
/** The files and refs a step's document says chant wrote. */
|
|
694
|
+
export function reportedWrites(action: WriteContractAction, doc: unknown): { paths: string[]; refs: Record<string, string> } {
|
|
695
|
+
const d = (doc ?? {}) as Record<string, unknown>;
|
|
696
|
+
const str = (v: unknown) => (typeof v === "string" ? v : undefined);
|
|
697
|
+
if (action.startsWith("work ") && action !== "work evidence") {
|
|
698
|
+
// The lease ref holds a lease blob whose value the document does not
|
|
699
|
+
// print, so any value of the ref it names is chant's ("*").
|
|
700
|
+
const history = (d.history ?? {}) as Record<string, unknown>;
|
|
701
|
+
const refs: Record<string, string> = {};
|
|
702
|
+
if (str(history.commit)) refs["refs/heads/chant/lifecycle"] = String(history.commit);
|
|
703
|
+
if (str(d.ref)) refs[String(d.ref)] = "*";
|
|
704
|
+
return { paths: [], refs };
|
|
705
|
+
}
|
|
706
|
+
if (action.startsWith("runs ")) {
|
|
707
|
+
const ledger = (d.ledger ?? {}) as Record<string, unknown>;
|
|
708
|
+
return { paths: [], refs: str(ledger.commit) ? { "refs/heads/chant/lifecycle": String(ledger.commit) } : {} };
|
|
709
|
+
}
|
|
710
|
+
if (action === "box listing set") return { paths: Array.isArray(d.paths) ? d.paths.filter((p): p is string => typeof p === "string") : [], refs: {} };
|
|
711
|
+
if (action === "wip save" || action === "wip restore") {
|
|
712
|
+
// The one snapshot ref, at the snapshot taken (the restore's checkpoint), and any ref a push recorded under refs/chant/replica/.
|
|
713
|
+
const snapshot = (action === "wip save" ? d.snapshot : (d.checkpoint as Record<string, unknown> | undefined)?.snapshot) as { commit?: unknown } | undefined;
|
|
714
|
+
const refs: Record<string, string> = {};
|
|
715
|
+
if (str(d.ref) && str(snapshot?.commit)) refs[String(d.ref)] = String(snapshot!.commit);
|
|
716
|
+
const replication = (d.replication ?? null) as { remote?: string; pushed?: { ref: string; commit: string }[]; upToDate?: number } | null;
|
|
717
|
+
for (const p of replication?.pushed ?? []) refs[`refs/chant/replica/${replication!.remote}/${p.ref.slice("refs/".length)}`] = p.commit;
|
|
718
|
+
return { paths: Array.isArray(d.paths) ? d.paths.filter((p): p is string => typeof p === "string") : [], refs };
|
|
719
|
+
}
|
|
720
|
+
return { paths: str(d.path) ? [String(d.path)] : [], refs: {} };
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
/**
|
|
724
|
+
* What changed in the workspace during one step that the step's document
|
|
725
|
+
* does not report: a file other than the path chant wrote, or a ref other
|
|
726
|
+
* than the lease ref and the ledger commit chant wrote.
|
|
727
|
+
*/
|
|
728
|
+
export function unreportedChanges(
|
|
729
|
+
step: WriteStep,
|
|
730
|
+
doc: unknown,
|
|
731
|
+
files: { before: Record<string, string>; after: Record<string, string> },
|
|
732
|
+
refs: { before: Record<string, string>; after: Record<string, string> },
|
|
733
|
+
): string[] {
|
|
734
|
+
const name = `${step.id} (${step.action})`;
|
|
735
|
+
const reported = reportedWrites(step.action, doc);
|
|
736
|
+
const problems: string[] = [];
|
|
737
|
+
const changed = treeChanges(files.before, files.after).filter((c) => !reported.paths.includes(c.replace(/ \((added|changed|removed)\)$/, "")));
|
|
738
|
+
if (changed.length > 0) problems.push(`${name}: files changed that chant did not report writing: ${changed.join(", ")}`);
|
|
739
|
+
const refsChanged = Object.entries(refChanges(refs.before, refs.after)).filter(([r, v]) => !(r in reported.refs && (reported.refs[r] === "*" || reported.refs[r] === v)));
|
|
740
|
+
if (refsChanged.length > 0) problems.push(`${name}: git refs changed that chant did not report writing: ${refsChanged.map(([r]) => r).join(", ")}`);
|
|
741
|
+
return problems;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
/** Build the step a script step is at, from the documents of the steps before it and where the suite put its inputs. */
|
|
745
|
+
export function buildStep(s: WriterScriptStep, done: Record<string, Record<string, unknown>>, inputs?: WriterInputs): WriteStep {
|
|
746
|
+
const params = s.params(done, inputs);
|
|
747
|
+
return { id: s.id, action: s.action, params, ...writeArgv(s.action, params) } as WriteStep;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
/** The box listings `status --json` prints, by member: what a home site shows of each box (#3308). */
|
|
751
|
+
export async function readListing(run: (argv: string[]) => Promise<ChantRun>): Promise<Record<string, unknown>> {
|
|
752
|
+
const doc = JSON.parse((await run(["workspace", "status", "dev", "--json"])).stdout) as { members?: { name: string; box: { listing?: unknown } | null }[] };
|
|
753
|
+
return Object.fromEntries((doc.members ?? []).filter((m) => m.box?.listing).map((m) => [m.name, m.box!.listing]));
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
/** Each branch's work-in-progress snapshots `wip --json` lists (#3172), by branch: each snapshot's kind and label, newest first. */
|
|
757
|
+
export async function readWip(run: (argv: string[]) => Promise<ChantRun>): Promise<Record<string, [string, string | null][]>> {
|
|
758
|
+
const doc = JSON.parse((await run(["workspace", "wip", "--json"])).stdout) as { branches?: { branch: string; snapshots: { kind: string; label: string | null }[] }[] };
|
|
759
|
+
return Object.fromEntries((doc.branches ?? []).map((b) => [b.branch, b.snapshots.map((s) => [s.kind, s.label] as [string, string | null])]));
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
/** The smallest writer that conforms: each step is its one command, and its facts are read through the read contract. */
|
|
763
|
+
export const referenceWriter: WorkspaceWriterFactory = (chant) => ({
|
|
764
|
+
async write(step) {
|
|
765
|
+
const run = await chant.run(["workspace", ...step.action.split(" "), ...step.args, ...WRITE_CONTRACT_JSON_FLAGS[step.action][0]], step.input === undefined ? undefined : { input: step.input });
|
|
766
|
+
return JSON.parse(run.stdout);
|
|
767
|
+
},
|
|
768
|
+
async facts() {
|
|
769
|
+
const out: Record<string, unknown> = {};
|
|
770
|
+
for (const [name, kind] of Object.entries(WRITER_KINDS)) {
|
|
771
|
+
const run = await chant.run(["workspace", "records", "--kind", kind, "--json"]);
|
|
772
|
+
const doc = JSON.parse(run.stdout) as { records: { id: string; state: string | null }[] };
|
|
773
|
+
out[name] = doc.records.map((r) => [r.id, r.state]).sort();
|
|
774
|
+
}
|
|
775
|
+
const runs = JSON.parse((await chant.run(["workspace", "runs", "--json"])).stdout) as { runs: { id: string; state: string }[] };
|
|
776
|
+
out.runs = runs.runs.map((r) => [r.id, r.state]).sort();
|
|
777
|
+
out.listing = await readListing((argv) => chant.run(argv));
|
|
778
|
+
out.wip = await readWip((argv) => chant.run(argv));
|
|
779
|
+
return out;
|
|
780
|
+
},
|
|
781
|
+
});
|
|
782
|
+
|
|
783
|
+
/** A workspace generated for a writer run, and how to remove it. */
|
|
784
|
+
export interface WriterConformanceWorkspace {
|
|
785
|
+
dir: string;
|
|
786
|
+
dispose(): void;
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
/**
|
|
790
|
+
* Generate the writer conformance workspace: the reader suite's workspace
|
|
791
|
+
* ({@link createConformanceWorkspace}), with `__writer_fixture__/` copied over
|
|
792
|
+
* it, the four kinds declared in its `records`, a box block on
|
|
793
|
+
* {@link WRITER_BOX} for the listing step, and that committed.
|
|
794
|
+
*/
|
|
795
|
+
export function createWriterConformanceWorkspace(options: { chantCommand?: string[]; timeoutMs?: number } = {}): WriterConformanceWorkspace {
|
|
796
|
+
const ws = createConformanceWorkspace({ chantCommand: options.chantCommand, timeoutMs: options.timeoutMs });
|
|
797
|
+
try {
|
|
798
|
+
cpSync(WRITER_FIXTURE_DIR, ws.dir, { recursive: true });
|
|
799
|
+
const declFile = join(ws.dir, "chant.workspace.json");
|
|
800
|
+
const decl = JSON.parse(readFileSync(declFile, "utf-8")) as Record<string, unknown>;
|
|
801
|
+
decl.records = Object.values(WRITER_KINDS).map((kind) => ({ kind }));
|
|
802
|
+
const box = (decl.members as { name: string; box?: unknown }[] | undefined)?.find((m) => m.name === WRITER_BOX);
|
|
803
|
+
if (box) box.box = {};
|
|
804
|
+
writeFileSync(declFile, `${JSON.stringify(decl, null, 2)}\n`);
|
|
805
|
+
const git = (...args: string[]) =>
|
|
806
|
+
execFileSync("git", ["-c", "user.name=chant", "-c", "user.email=chant@localhost", "-c", "commit.gpgsign=false", ...args], {
|
|
807
|
+
cwd: ws.dir,
|
|
808
|
+
env: { ...process.env, GIT_CONFIG_GLOBAL: process.platform === "win32" ? "NUL" : "/dev/null", GIT_CONFIG_NOSYSTEM: "1" },
|
|
809
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
810
|
+
});
|
|
811
|
+
git("add", "-A");
|
|
812
|
+
git("commit", "--quiet", "-m", "the writer conformance workspace");
|
|
813
|
+
return ws;
|
|
814
|
+
} catch (e) {
|
|
815
|
+
ws.dispose();
|
|
816
|
+
const err = e as Error & { stderr?: Buffer | string };
|
|
817
|
+
throw new Error(`could not generate the writer conformance workspace: ${err.message}${err.stderr ? `\n${String(err.stderr)}` : ""}`);
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
/** The actions to exercise and the ones skipped, from `actions`. Throws on a name that is not a write-contract action. */
|
|
822
|
+
export function selectActions(actions?: readonly WriteContractAction[]): { checked: WriteContractAction[]; skipped: WriteContractAction[] } {
|
|
823
|
+
if (actions === undefined) return { checked: [...WRITE_CONTRACT_ACTIONS], skipped: [] };
|
|
824
|
+
const unknown = actions.filter((a) => !(WRITE_CONTRACT_ACTIONS as readonly string[]).includes(a));
|
|
825
|
+
if (unknown.length > 0) throw new Error(`not write-contract actions: ${unknown.join(", ")}; the actions are ${WRITE_CONTRACT_ACTIONS.join(", ")}`);
|
|
826
|
+
if (actions.length === 0) throw new Error("actions is empty; list at least one write-contract action");
|
|
827
|
+
return { checked: WRITE_CONTRACT_ACTIONS.filter((a) => actions.includes(a)), skipped: WRITE_CONTRACT_ACTIONS.filter((a) => !actions.includes(a)) };
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
/** Every file under `dir`, from it, with / separators. */
|
|
831
|
+
function filesUnder(dir: string): string[] {
|
|
832
|
+
const out: string[] = [];
|
|
833
|
+
const walk = (at: string, prefix: string) => {
|
|
834
|
+
for (const e of readdirSync(at, { withFileTypes: true })) {
|
|
835
|
+
const rel = prefix ? `${prefix}/${e.name}` : e.name;
|
|
836
|
+
if (e.isDirectory()) walk(join(at, e.name), rel);
|
|
837
|
+
else out.push(rel);
|
|
838
|
+
}
|
|
839
|
+
};
|
|
840
|
+
if (existsSync(dir)) walk(dir, "");
|
|
841
|
+
return out.sort();
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
/** The files in the state directory that `privateState` does not declare, and problems with the declaration itself. */
|
|
845
|
+
export function undeclaredState(files: readonly string[], privateState: readonly PrivateStatePath[]): string[] {
|
|
846
|
+
const problems: string[] = [];
|
|
847
|
+
for (const p of privateState) {
|
|
848
|
+
const is = typeof p.is === "string" ? [p.is] : [...p.is];
|
|
849
|
+
const wrong = is.filter((c) => !(PRIVATE_STATE_CATEGORIES as readonly string[]).includes(c));
|
|
850
|
+
if (is.length === 0 || wrong.length > 0) problems.push(`state: ${p.path} is declared as ${JSON.stringify(p.is)}; private state is ${PRIVATE_STATE_CATEGORIES.join(", ")} (ws-074)`);
|
|
851
|
+
}
|
|
852
|
+
const declared = (f: string) => privateState.some((p) => f === p.path || f.startsWith(`${p.path.replace(/\/+$/, "")}/`));
|
|
853
|
+
const loose = files.filter((f) => !declared(f));
|
|
854
|
+
if (loose.length > 0) problems.push(`state: the writer keeps ${loose.join(", ")} in its state directory, and privateState does not declare it`);
|
|
855
|
+
return problems;
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
/** A short JSON rendering of a value for a problem message. */
|
|
859
|
+
function brief(v: unknown): string {
|
|
860
|
+
const s = JSON.stringify(v) ?? String(v);
|
|
861
|
+
return s.length > 400 ? `${s.slice(0, 400)}...` : s;
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
/**
|
|
865
|
+
* Hold a writer to the write contract, with no test runner: drive
|
|
866
|
+
* {@link WRITER_SCRIPT} through it on a generated workspace, check each step,
|
|
867
|
+
* then the amnesia test and what the writer holds, and return what is wrong.
|
|
868
|
+
* The writer conforms when `problems` is empty.
|
|
869
|
+
*
|
|
870
|
+
* ```js
|
|
871
|
+
* import { test } from "node:test";
|
|
872
|
+
* import assert from "node:assert/strict";
|
|
873
|
+
* import { runWorkspaceWriterConformance } from "@intentius/chant/workspace/conformance";
|
|
874
|
+
*
|
|
875
|
+
* test("my writer writes only through chant", { timeout: 600_000 }, async () => {
|
|
876
|
+
* const report = await runWorkspaceWriterConformance(myWriter, {
|
|
877
|
+
* actions: ["records review", "points answer"],
|
|
878
|
+
* privateState: [{ path: "events.db", is: "cache" }],
|
|
879
|
+
* });
|
|
880
|
+
* assert.deepEqual(report.problems, []);
|
|
881
|
+
* });
|
|
882
|
+
* ```
|
|
883
|
+
*/
|
|
884
|
+
export async function runWorkspaceWriterConformance(writer: WorkspaceWriterFactory, options: WorkspaceWriterConformanceOptions = {}): Promise<WorkspaceWriterConformanceReport> {
|
|
885
|
+
const { checked, skipped } = selectActions(options.actions);
|
|
886
|
+
const chantCommand = options.chantCommand ?? defaultChantCommand();
|
|
887
|
+
const timeoutMs = options.timeoutMs ?? 120_000;
|
|
888
|
+
const privateState = options.privateState ?? [];
|
|
889
|
+
const ws = createWriterConformanceWorkspace({ chantCommand, timeoutMs });
|
|
890
|
+
const stateDir = realpathSync(mkdtempSync(join(tmpdir(), "chant-writer-state-")));
|
|
891
|
+
const inputsDir = realpathSync(mkdtempSync(join(tmpdir(), "chant-writer-inputs-")));
|
|
892
|
+
const inputs = writeWriterInputs(inputsDir);
|
|
893
|
+
const calls: ChantRun[] = [];
|
|
894
|
+
const transport: ChantTransport = {
|
|
895
|
+
async run(argv, opts) {
|
|
896
|
+
const run = await runChant(chantCommand, [...argv], ws.dir, timeoutMs, opts?.input);
|
|
897
|
+
calls.push(run);
|
|
898
|
+
return run;
|
|
899
|
+
},
|
|
900
|
+
};
|
|
901
|
+
const direct = async (argv: string[], input?: string): Promise<Record<string, unknown>> => {
|
|
902
|
+
const run = await runChant(chantCommand, argv, ws.dir, timeoutMs, input);
|
|
903
|
+
try {
|
|
904
|
+
return JSON.parse(run.stdout) as Record<string, unknown>;
|
|
905
|
+
} catch {
|
|
906
|
+
throw new Error(`chant ${argv.join(" ")} printed no JSON (exit ${run.status}): ${run.stderr.trim()}`);
|
|
907
|
+
}
|
|
908
|
+
};
|
|
909
|
+
const after = { facts: [] as string[], state: [] as string[], amnesia: [] as string[], holds: [] as string[], readBack: [] as string[], concurrent: [] as string[] };
|
|
910
|
+
const facts: { before: unknown; after: unknown } = { before: undefined, after: undefined };
|
|
911
|
+
const results: WriterStepResult[] = [];
|
|
912
|
+
try {
|
|
913
|
+
let built = writer(transport, { stateDir });
|
|
914
|
+
const done: Record<string, Record<string, unknown>> = {};
|
|
915
|
+
for (const s of WRITER_SCRIPT) {
|
|
916
|
+
const st = buildStep(s, done, inputs);
|
|
917
|
+
if (!checked.includes(st.action)) {
|
|
918
|
+
const doc = await direct(["workspace", ...st.action.split(" "), ...st.args, ...WRITE_CONTRACT_JSON_FLAGS[st.action][0]], st.input);
|
|
919
|
+
done[st.id] = doc;
|
|
920
|
+
const no = (doc.error ?? doc.refused) as { code?: string; message?: string } | undefined;
|
|
921
|
+
results.push({ id: st.id, action: st.action, by: "suite", args: st.args, problems: no ? [`${st.id} (${st.action}): the suite's own write failed: ${no.code}: ${no.message}`] : [] });
|
|
922
|
+
continue;
|
|
923
|
+
}
|
|
924
|
+
const files = { before: worktreeDigest(ws.dir), after: {} as Record<string, string> };
|
|
925
|
+
const refs = { before: gitRefs(ws.dir), after: {} as Record<string, string> };
|
|
926
|
+
calls.length = 0;
|
|
927
|
+
let doc: unknown;
|
|
928
|
+
let problems: string[];
|
|
929
|
+
try {
|
|
930
|
+
doc = await built.write(st);
|
|
931
|
+
problems = writerCallProblems(st, calls);
|
|
932
|
+
if (problems.length === 0) problems = writerDocumentProblems(st, calls[0], doc);
|
|
933
|
+
} catch (e) {
|
|
934
|
+
const stderr = calls[0]?.stderr.trim();
|
|
935
|
+
problems = [`${st.id} (${st.action}): the writer threw: ${(e as Error).message}${stderr ? `; chant's stderr: ${stderr}` : ""}`];
|
|
936
|
+
}
|
|
937
|
+
files.after = worktreeDigest(ws.dir);
|
|
938
|
+
refs.after = gitRefs(ws.dir);
|
|
939
|
+
// What chant printed is what the later steps build on, whatever the writer returned.
|
|
940
|
+
const printed = calls.length === 1 ? (() => { try { return JSON.parse(calls[0].stdout) as Record<string, unknown>; } catch { return undefined; } })() : undefined;
|
|
941
|
+
done[st.id] = printed ?? ((doc ?? {}) as Record<string, unknown>);
|
|
942
|
+
problems.push(...unreportedChanges(st, done[st.id], files, refs));
|
|
943
|
+
results.push({ id: st.id, action: st.action, by: "writer", args: st.args, problems });
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
// Concurrent writes (#3173): three amendments of one record at once, each from the digest read.
|
|
947
|
+
if (checked.includes("records amend")) {
|
|
948
|
+
after.concurrent.push(...(await concurrentAmends(built, calls, direct, String(done.decision?.id ?? ""), ws.dir)));
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
// What the writer shows, read only through the read contract.
|
|
952
|
+
const readFacts = async (w: WorkspaceWriter, when: string): Promise<unknown> => {
|
|
953
|
+
const files = worktreeDigest(ws.dir);
|
|
954
|
+
const refs = gitRefs(ws.dir);
|
|
955
|
+
calls.length = 0;
|
|
956
|
+
let value: unknown;
|
|
957
|
+
try {
|
|
958
|
+
value = await w.facts();
|
|
959
|
+
} catch (e) {
|
|
960
|
+
after.facts.push(`facts: ${when}, facts() threw: ${(e as Error).message}`);
|
|
961
|
+
}
|
|
962
|
+
const writes = calls.filter((c) => !isReadCall(c.argv));
|
|
963
|
+
if (writes.length > 0) after.facts.push(`facts: ${when}, facts() made calls outside the read contract: ${writes.map((c) => c.argv.join(" ")).join("; ")}`);
|
|
964
|
+
const changed = treeChanges(files, worktreeDigest(ws.dir));
|
|
965
|
+
if (changed.length > 0) after.facts.push(`facts: ${when}, facts() changed files: ${changed.join(", ")}`);
|
|
966
|
+
const refsChanged = Object.keys(refChanges(refs, gitRefs(ws.dir)));
|
|
967
|
+
if (refsChanged.length > 0) after.facts.push(`facts: ${when}, facts() changed git refs: ${refsChanged.join(", ")}`);
|
|
968
|
+
return value;
|
|
969
|
+
};
|
|
970
|
+
facts.before = await readFacts(built, "before amnesia");
|
|
971
|
+
if (facts.before === undefined || facts.before === null) after.facts.push("facts: facts() returned nothing; it returns the facts the tool shows, read the way the tool reads them");
|
|
972
|
+
|
|
973
|
+
// What the writer holds: each fact in the repo, or one of the four exceptions.
|
|
974
|
+
if (built.holds) {
|
|
975
|
+
let held: readonly HeldItem[] = [];
|
|
976
|
+
try {
|
|
977
|
+
held = await built.holds();
|
|
978
|
+
} catch (e) {
|
|
979
|
+
after.holds.push(`holds: holds() threw: ${(e as Error).message}`);
|
|
980
|
+
}
|
|
981
|
+
const records = new Map<string, Set<string>>();
|
|
982
|
+
const recordIds = async (kind: string) => {
|
|
983
|
+
if (!records.has(kind)) {
|
|
984
|
+
const doc = await direct(["workspace", "records", "--kind", kind, "--json"]);
|
|
985
|
+
records.set(kind, new Set(((doc.records ?? []) as { id: string }[]).map((r) => r.id)));
|
|
986
|
+
}
|
|
987
|
+
return records.get(kind)!;
|
|
988
|
+
};
|
|
989
|
+
let runIds: Set<string> | undefined;
|
|
990
|
+
for (const h of held) {
|
|
991
|
+
if ("record" in h) {
|
|
992
|
+
if (!(await recordIds(h.kind)).has(h.record)) after.holds.push(`holds: the writer holds record ${h.record} of ${h.kind}, which the repository does not have`);
|
|
993
|
+
} else if ("run" in h) {
|
|
994
|
+
runIds ??= new Set(((await direct(["workspace", "runs", "--json"])).runs as { id: string }[] | undefined ?? []).map((r) => r.id));
|
|
995
|
+
if (!runIds.has(h.run)) after.holds.push(`holds: the writer holds run ${h.run}, which the run ledger does not have`);
|
|
996
|
+
} else if ("lease" in h) {
|
|
997
|
+
const doc = await direct(["workspace", "work", "history", h.lease, "--kind", h.kind, "--json"]);
|
|
998
|
+
if (!Array.isArray(doc.claims) || doc.claims.length === 0) after.holds.push(`holds: the writer holds a lease on ${h.lease}, which has no lease history`);
|
|
999
|
+
} else if ("exempt" in h) {
|
|
1000
|
+
if (!(PRIVATE_STATE_CATEGORIES as readonly string[]).includes(h.exempt)) after.holds.push(`holds: ${h.what} is held as ${JSON.stringify(h.exempt)}; outside the repo a tool keeps only ${PRIVATE_STATE_CATEGORIES.join(", ")} (ws-074)`);
|
|
1001
|
+
} else {
|
|
1002
|
+
after.holds.push(`holds: ${brief(h)} is not a record, run, lease or exemption`);
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
// Amnesia: close the writer, delete its state, build it again, and ask again.
|
|
1008
|
+
try {
|
|
1009
|
+
await built.close?.();
|
|
1010
|
+
} catch (e) {
|
|
1011
|
+
after.amnesia.push(`amnesia: close() threw: ${(e as Error).message}`);
|
|
1012
|
+
}
|
|
1013
|
+
after.state.push(...undeclaredState(filesUnder(stateDir), privateState));
|
|
1014
|
+
rmSync(stateDir, { recursive: true, force: true });
|
|
1015
|
+
mkdirSync(stateDir, { recursive: true });
|
|
1016
|
+
built = writer(transport, { stateDir });
|
|
1017
|
+
facts.after = await readFacts(built, "after amnesia");
|
|
1018
|
+
if (!isDeepStrictEqual(facts.before, facts.after)) {
|
|
1019
|
+
after.amnesia.push(`amnesia: after its private state was deleted, the writer shows other facts; before: ${brief(facts.before)}; after: ${brief(facts.after)}`);
|
|
1020
|
+
}
|
|
1021
|
+
try {
|
|
1022
|
+
await built.close?.();
|
|
1023
|
+
} catch {
|
|
1024
|
+
// Closed again after the comparison; a failure here is not the writer's contract.
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
// Every fact the script produced, read back through the read contract.
|
|
1028
|
+
const uncommitted = new Map<string, Map<string, string>>();
|
|
1029
|
+
for (const kind of Object.values(WRITER_KINDS)) {
|
|
1030
|
+
const doc = await direct(["workspace", "records", "--uncommitted", "--kind", kind, "--json"]);
|
|
1031
|
+
uncommitted.set(kind, new Map(((doc.records ?? []) as { id: string; worktree: string }[]).map((r) => [r.id, r.worktree])));
|
|
1032
|
+
}
|
|
1033
|
+
const runs = new Map(((await direct(["workspace", "runs", "--json"])).runs as { id: string; state: string }[] | undefined ?? []).map((r) => [r.id, r.state]));
|
|
1034
|
+
const history = await direct(["workspace", "work", "history", "W-001", "--kind", WRITER_KINDS.work, "--json"]);
|
|
1035
|
+
const listings = await readListing((argv) => runChant(chantCommand, argv, ws.dir, timeoutMs));
|
|
1036
|
+
const wipDoc = await direct(["workspace", "wip", "--json"]);
|
|
1037
|
+
const snapshots = new Set(((wipDoc.branches ?? []) as { snapshots: { commit: string }[] }[]).flatMap((b) => b.snapshots.map((x) => x.commit)));
|
|
1038
|
+
const claims = (history.claims ?? []) as { token: string; ended: string | null; release?: { outcome?: string } | null }[];
|
|
1039
|
+
const questions = new Map(((await direct(["workspace", "points", "--kind", WRITER_KINDS.answer, "--json"])).questions as { id: string; retractions?: unknown[]; asked?: unknown }[] | undefined ?? []).map((q) => [q.id, q]));
|
|
1040
|
+
for (const s of WRITER_SCRIPT) {
|
|
1041
|
+
const doc = done[s.id] ?? {};
|
|
1042
|
+
if (s.action.startsWith("records ") || s.action.startsWith("points ")) {
|
|
1043
|
+
const kind = String((doc.kind as { file?: unknown } | undefined)?.file ?? "");
|
|
1044
|
+
const id = String(doc.id ?? "");
|
|
1045
|
+
if (!uncommitted.get(kind)?.has(id)) after.readBack.push(`read back: ${s.id} (${s.action}) wrote ${id}, and records --uncommitted --kind ${kind} does not list it`);
|
|
1046
|
+
else if (s.action === "points retract" && !(questions.get(id)?.retractions?.length ?? 0)) after.readBack.push(`read back: ${s.id} (${s.action}) retracted the answer to ${id}, and points --json lists no retraction for it`);
|
|
1047
|
+
else if (s.action === "points ask") {
|
|
1048
|
+
const asked = (doc.question as { asked?: unknown } | undefined)?.asked;
|
|
1049
|
+
if (asked && !isDeepStrictEqual(questions.get(id)?.asked, asked)) after.readBack.push(`read back: ${s.id} (${s.action}) asked ${id} with its own question and candidates, and points --json does not list them as asked`);
|
|
1050
|
+
}
|
|
1051
|
+
} else if (s.action === "work evidence") {
|
|
1052
|
+
if (!uncommitted.get(WRITER_KINDS.work)?.has(String(doc.item ?? ""))) after.readBack.push(`read back: ${s.id} (${s.action}) amended ${String(doc.item)}, and records --uncommitted does not list it`);
|
|
1053
|
+
} else if (s.action.startsWith("work ")) {
|
|
1054
|
+
const token = String((doc.lease as { token?: unknown } | undefined)?.token ?? "");
|
|
1055
|
+
const claim = claims.find((c) => c.token === token);
|
|
1056
|
+
if (!claim) after.readBack.push(`read back: ${s.id} (${s.action}) wrote a lease with token ${token}, and work history does not list it`);
|
|
1057
|
+
else if (s.action === "work release" && claim.release?.outcome !== "done") after.readBack.push(`read back: ${s.id} (${s.action}) released the lease as done, and work history says ${brief(claim.release)}`);
|
|
1058
|
+
} else if (s.action.startsWith("runs ")) {
|
|
1059
|
+
const id = String((doc.run as { id?: unknown } | undefined)?.id ?? "");
|
|
1060
|
+
const want = s.action === "runs start" ? undefined : "ended";
|
|
1061
|
+
if (!runs.has(id)) after.readBack.push(`read back: ${s.id} (${s.action}) wrote run ${id}, and runs does not list it`);
|
|
1062
|
+
else if (want && runs.get(id) !== want) after.readBack.push(`read back: ${s.id} (${s.action}) ended run ${id}, and runs says it is ${runs.get(id)}`);
|
|
1063
|
+
} else if (s.action === "box listing set") {
|
|
1064
|
+
const member = String(doc.member ?? "");
|
|
1065
|
+
if (!isDeepStrictEqual(listings[member], doc.listing)) after.readBack.push(`read back: ${s.id} (${s.action}) set ${member}'s listing to ${brief(doc.listing)}, and status --json says ${brief(listings[member] ?? null)}`);
|
|
1066
|
+
} else if (s.action === "wip save" || s.action === "wip restore") {
|
|
1067
|
+
const snap = (s.action === "wip save" ? doc.snapshot : (doc.checkpoint as Record<string, unknown> | undefined)?.snapshot) as { commit?: unknown } | undefined;
|
|
1068
|
+
const commit = String(snap?.commit ?? "");
|
|
1069
|
+
if (!snapshots.has(commit)) after.readBack.push(`read back: ${s.id} (${s.action}) wrote snapshot ${commit}, and wip --json does not list it`);
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
const problems = [...results.flatMap((r) => r.problems), ...after.concurrent, ...after.facts, ...after.state, ...after.amnesia, ...after.holds, ...after.readBack];
|
|
1074
|
+
return { problems, checked, skipped, results, after, facts, workspaceDir: ws.dir };
|
|
1075
|
+
} finally {
|
|
1076
|
+
rmSync(stateDir, { recursive: true, force: true });
|
|
1077
|
+
rmSync(inputsDir, { recursive: true, force: true });
|
|
1078
|
+
ws.dispose();
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
/**
|
|
1083
|
+
* The concurrent case (#3173, ws-089): the writer makes the three
|
|
1084
|
+
* {@link CONCURRENT_AMENDS} of record `id` at once, each from the digest the
|
|
1085
|
+
* suite read. Exactly one is written and the others are refused with
|
|
1086
|
+
* record-conflict naming its digest; each is then retried from the digest
|
|
1087
|
+
* its refusal named, and written. Returns the problems, each starting
|
|
1088
|
+
* `concurrent:`.
|
|
1089
|
+
*/
|
|
1090
|
+
export async function concurrentAmends(
|
|
1091
|
+
built: WorkspaceWriter,
|
|
1092
|
+
calls: ChantRun[],
|
|
1093
|
+
direct: (argv: string[], input?: string) => Promise<Record<string, unknown>>,
|
|
1094
|
+
id: string,
|
|
1095
|
+
dir: string,
|
|
1096
|
+
): Promise<string[]> {
|
|
1097
|
+
const problems: string[] = [];
|
|
1098
|
+
const digestOf = async (): Promise<{ digest: string; path: string } | undefined> => {
|
|
1099
|
+
const doc = await direct(["workspace", "records", "--kind", WRITER_KINDS.decision, "--json"]);
|
|
1100
|
+
const r = ((doc.records ?? []) as { id: string; digest: string; path: string }[]).find((x) => x.id === id);
|
|
1101
|
+
return r ? { digest: r.digest, path: r.path } : undefined;
|
|
1102
|
+
};
|
|
1103
|
+
const start = await digestOf();
|
|
1104
|
+
if (!start) return [`concurrent: the script's decision ${id || "(none)"} can't be read back to amend`];
|
|
1105
|
+
const files = { before: worktreeDigest(dir), after: {} as Record<string, string> };
|
|
1106
|
+
const refs = { before: gitRefs(dir), after: {} as Record<string, string> };
|
|
1107
|
+
const steps = CONCURRENT_AMENDS.map((c) => concurrentAmendStep(c, id, start.digest));
|
|
1108
|
+
calls.length = 0;
|
|
1109
|
+
const docs = await Promise.all(
|
|
1110
|
+
steps.map(async (st) => {
|
|
1111
|
+
try {
|
|
1112
|
+
return { st, doc: await built.write(st) };
|
|
1113
|
+
} catch (e) {
|
|
1114
|
+
problems.push(`concurrent: ${st.id} (${st.action}): the writer threw: ${(e as Error).message}`);
|
|
1115
|
+
return { st, doc: undefined };
|
|
1116
|
+
}
|
|
1117
|
+
}),
|
|
1118
|
+
);
|
|
1119
|
+
// Each step's call: the one that gave chant its fields.
|
|
1120
|
+
const printed = new Map<string, Record<string, unknown> | undefined>();
|
|
1121
|
+
for (const { st, doc } of docs) {
|
|
1122
|
+
const mine = calls.filter((c) => c.input !== undefined && st.input !== undefined && isDeepStrictEqual(safeJson(c.input), JSON.parse(st.input)));
|
|
1123
|
+
const callProblems = writerCallProblems(st, mine);
|
|
1124
|
+
if (callProblems.length > 0) {
|
|
1125
|
+
problems.push(...callProblems.map((p) => `concurrent: ${p}`));
|
|
1126
|
+
continue;
|
|
1127
|
+
}
|
|
1128
|
+
const out = safeJson(mine[0].stdout) as Record<string, unknown> | undefined;
|
|
1129
|
+
printed.set(st.id, out);
|
|
1130
|
+
if (!isDeepStrictEqual(doc, out)) problems.push(`concurrent: ${st.id} (${st.action}): the writer must return the document chant printed, unchanged, refusal included, and it returned something else`);
|
|
1131
|
+
const { validate } = writeContractSchema(st.action);
|
|
1132
|
+
if (!validate(out)) problems.push(`concurrent: ${st.id} (${st.action}): the document does not validate against ${WRITE_CONTRACT_SCHEMAS[st.action]}: ${JSON.stringify(validate.errors)}`);
|
|
1133
|
+
}
|
|
1134
|
+
if (problems.length > 0) return problems;
|
|
1135
|
+
const won = steps.filter((st) => printed.get(st.id)?.error === undefined);
|
|
1136
|
+
const lost = steps.filter((st) => printed.get(st.id)?.error !== undefined);
|
|
1137
|
+
if (won.length !== 1) {
|
|
1138
|
+
problems.push(`concurrent: ${won.length} of the three amendments from digest ${start.digest} were written (${won.map((s) => s.id).join(", ") || "none"}); exactly one may be`);
|
|
1139
|
+
return problems;
|
|
1140
|
+
}
|
|
1141
|
+
const d1 = String(printed.get(won[0].id)?.digest ?? "");
|
|
1142
|
+
for (const st of lost) {
|
|
1143
|
+
const doc = printed.get(st.id)!;
|
|
1144
|
+
const code = (doc.error as { code?: string }).code;
|
|
1145
|
+
const conflict = doc.conflict as { digest?: string; expected?: string } | undefined;
|
|
1146
|
+
if (code !== "record-conflict") problems.push(`concurrent: ${st.id} was refused with ${code}, not record-conflict`);
|
|
1147
|
+
else if (conflict?.digest !== d1 || conflict?.expected !== start.digest) problems.push(`concurrent: ${st.id}'s conflict names ${brief(conflict)}, not expected ${start.digest} and digest ${d1}`);
|
|
1148
|
+
}
|
|
1149
|
+
files.after = worktreeDigest(dir);
|
|
1150
|
+
refs.after = gitRefs(dir);
|
|
1151
|
+
const changed = treeChanges(files.before, files.after).filter((c) => c.replace(/ \((added|changed|removed)\)$/, "") !== start.path);
|
|
1152
|
+
if (changed.length > 0) problems.push(`concurrent: files changed that chant did not report writing: ${changed.join(", ")}`);
|
|
1153
|
+
if (Object.keys(refChanges(refs.before, refs.after)).length > 0) problems.push(`concurrent: git refs changed: ${Object.keys(refChanges(refs.before, refs.after)).join(", ")}`);
|
|
1154
|
+
if (problems.length > 0) return problems;
|
|
1155
|
+
// Each loser retries from the digest it was told, one after the other.
|
|
1156
|
+
let digest = d1;
|
|
1157
|
+
for (const st of lost) {
|
|
1158
|
+
const retry = concurrentAmendStep(CONCURRENT_AMENDS.find((c) => c.id === st.id)!, id, digest);
|
|
1159
|
+
calls.length = 0;
|
|
1160
|
+
let doc: unknown;
|
|
1161
|
+
try {
|
|
1162
|
+
doc = await built.write(retry);
|
|
1163
|
+
} catch (e) {
|
|
1164
|
+
problems.push(`concurrent: ${st.id}'s retry: the writer threw: ${(e as Error).message}`);
|
|
1165
|
+
continue;
|
|
1166
|
+
}
|
|
1167
|
+
const p = writerCallProblems({ ...retry, id: `${st.id} retry` } as WriteStep, calls);
|
|
1168
|
+
if (p.length > 0) {
|
|
1169
|
+
problems.push(...p.map((x) => `concurrent: ${x}`));
|
|
1170
|
+
continue;
|
|
1171
|
+
}
|
|
1172
|
+
const out = safeJson(calls[0].stdout) as Record<string, unknown> | undefined;
|
|
1173
|
+
if (out?.error !== undefined) {
|
|
1174
|
+
problems.push(`concurrent: ${st.id}'s retry from ${digest} was refused: ${brief(out.error)}`);
|
|
1175
|
+
continue;
|
|
1176
|
+
}
|
|
1177
|
+
if (!isDeepStrictEqual(doc, out)) problems.push(`concurrent: ${st.id}'s retry: the writer must return the document chant printed, unchanged`);
|
|
1178
|
+
digest = String(out?.digest ?? "");
|
|
1179
|
+
}
|
|
1180
|
+
const end = await digestOf();
|
|
1181
|
+
if (problems.length === 0 && end?.digest !== digest) problems.push(`concurrent: after the retries records --json gives ${id} digest ${end?.digest}, and the last retry printed ${digest}`);
|
|
1182
|
+
return problems;
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
function safeJson(text: string): unknown {
|
|
1186
|
+
try {
|
|
1187
|
+
return JSON.parse(text);
|
|
1188
|
+
} catch {
|
|
1189
|
+
return undefined;
|
|
1190
|
+
}
|
|
1191
|
+
}
|