@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,474 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The working tree's write lock and write journal (#3173, ws-089).
|
|
3
|
+
*
|
|
4
|
+
* The repo is the database (ws-074), and one working tree is written by
|
|
5
|
+
* several principals at once: people through hud, the coding agent, the
|
|
6
|
+
* steward and builders. chant's working-tree writes (records new, amend,
|
|
7
|
+
* review and close, points ask and answer, box listing set) each read the
|
|
8
|
+
* records, build the file they write, validate it and write it. Two of them
|
|
9
|
+
* running at once could each read the same record and the second write would
|
|
10
|
+
* drop the first's change, or two `records new` could hand out the same id.
|
|
11
|
+
* So every such write holds this lock from its first read to its last write:
|
|
12
|
+
* writes to one working tree are serialised, and each one reads what the one
|
|
13
|
+
* before it wrote.
|
|
14
|
+
*
|
|
15
|
+
* The lock is a directory, `chant-write.lock`, in the working tree's git
|
|
16
|
+
* directory (`.git`, or `.git/worktrees/<name>` for a linked worktree, so two
|
|
17
|
+
* worktrees of one repository never wait on each other), made with `mkdir`,
|
|
18
|
+
* which either creates it or fails. `owner.json` inside it says who holds it.
|
|
19
|
+
* Outside git it lives in the system's temporary directory, keyed by the
|
|
20
|
+
* working directory's real path, so nothing is added to the files a person
|
|
21
|
+
* reviews. A holder that died leaves its lock behind: one whose process is
|
|
22
|
+
* gone on this host, or whose `expiresAt` has passed, is broken by the next
|
|
23
|
+
* writer. A writer waits for the lock up to `CHANT_WRITE_LOCK_WAIT_MS`
|
|
24
|
+
* (default {@link DEFAULT_WAIT_MS}) and is then refused with
|
|
25
|
+
* `write-lock-timeout`, naming the holder.
|
|
26
|
+
*
|
|
27
|
+
* A caller can hold the lock across several chant calls, for a batch of
|
|
28
|
+
* writes nobody else interleaves with (`chant workspace lock acquire`, hud#819):
|
|
29
|
+
* each call of the batch runs with `CHANT_WRITE_LOCK=<token>` in its
|
|
30
|
+
* environment, and goes ahead without waiting while the lock is held under
|
|
31
|
+
* that token. A token that no longer holds the lock (released, or broken
|
|
32
|
+
* after it expired) is refused with `write-lock-not-held`, so a batch never
|
|
33
|
+
* runs on unlocked.
|
|
34
|
+
*
|
|
35
|
+
* The journal, `chant-writes.json` beside the lock, notes the last chant
|
|
36
|
+
* write of each record file: the verb, who it named and when, keyed by the
|
|
37
|
+
* file's SHA-256 after the write. The read contract reports it as a record's
|
|
38
|
+
* `lastWrite` while the file still has those bytes, and a conflict refusal
|
|
39
|
+
* names it. It is a cache in the git directory (ws-074's exceptions), never
|
|
40
|
+
* a fact: lost, every record's `lastWrite` is null, and nothing else changes.
|
|
41
|
+
*/
|
|
42
|
+
|
|
43
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
44
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
45
|
+
import { existsSync, mkdirSync, readFileSync, realpathSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
46
|
+
import { hostname, tmpdir } from "node:os";
|
|
47
|
+
import { dirname, isAbsolute, join, resolve } from "node:path";
|
|
48
|
+
|
|
49
|
+
/** The environment variable that names a lock token a batch holds across chant calls. */
|
|
50
|
+
export const WRITE_LOCK_ENV = "CHANT_WRITE_LOCK";
|
|
51
|
+
/** The environment variable that sets how long a write waits for the lock, in milliseconds. */
|
|
52
|
+
export const WRITE_LOCK_WAIT_ENV = "CHANT_WRITE_LOCK_WAIT_MS";
|
|
53
|
+
/** How long a write waits for the lock by default. */
|
|
54
|
+
export const DEFAULT_WAIT_MS = 15_000;
|
|
55
|
+
/** How long one command's lock is good for, should its process be unable to say it died. */
|
|
56
|
+
export const COMMAND_TTL_MS = 2 * 60_000;
|
|
57
|
+
/** The longest a batch may hold the lock with `lock acquire --ttl`. */
|
|
58
|
+
export const MAX_BATCH_TTL_MS = 10 * 60_000;
|
|
59
|
+
/** The default `--ttl` of `lock acquire`. */
|
|
60
|
+
export const DEFAULT_BATCH_TTL_MS = 60_000;
|
|
61
|
+
|
|
62
|
+
/** The lock directory's name in the git directory. */
|
|
63
|
+
export const WRITE_LOCK_NAME = "chant-write.lock";
|
|
64
|
+
/** The journal's name in the git directory. */
|
|
65
|
+
export const WRITE_JOURNAL_NAME = "chant-writes.json";
|
|
66
|
+
|
|
67
|
+
export const WRITE_LOCK_CODES = ["write-lock-timeout", "write-lock-not-held"] as const;
|
|
68
|
+
export type WriteLockCode = (typeof WRITE_LOCK_CODES)[number];
|
|
69
|
+
|
|
70
|
+
/** Who holds the lock, as `owner.json` holds it. */
|
|
71
|
+
export interface WriteLockHolder {
|
|
72
|
+
token: string;
|
|
73
|
+
/** The process holding it, or null for a batch held across calls by `lock acquire`. */
|
|
74
|
+
pid: number | null;
|
|
75
|
+
host: string;
|
|
76
|
+
/** What holds it: a write verb, such as `records amend`, or `batch`. */
|
|
77
|
+
verb: string;
|
|
78
|
+
by: string | null;
|
|
79
|
+
agent: string | null;
|
|
80
|
+
acquiredAt: string;
|
|
81
|
+
expiresAt: string;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Who is writing: the verb, and the principal and agent session the write names. */
|
|
85
|
+
export interface WriteLockWho {
|
|
86
|
+
verb: string;
|
|
87
|
+
by?: string | null;
|
|
88
|
+
agent?: string | null;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export class WriteLockError extends Error {
|
|
92
|
+
constructor(
|
|
93
|
+
readonly code: WriteLockCode,
|
|
94
|
+
message: string,
|
|
95
|
+
readonly holder: WriteLockHolder | null,
|
|
96
|
+
) {
|
|
97
|
+
super(message);
|
|
98
|
+
this.name = "WriteLockError";
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ── Where ────────────────────────────────────────────────────────────────────
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* The git directory of the working tree `start` is in: `.git`, or the
|
|
106
|
+
* directory a linked worktree's `.git` file names. Null outside git.
|
|
107
|
+
*/
|
|
108
|
+
export function gitDirOf(start: string): string | null {
|
|
109
|
+
let dir = resolve(start);
|
|
110
|
+
for (;;) {
|
|
111
|
+
const dotGit = join(dir, ".git");
|
|
112
|
+
try {
|
|
113
|
+
const st = statSync(dotGit);
|
|
114
|
+
if (st.isDirectory()) return dotGit;
|
|
115
|
+
if (st.isFile()) {
|
|
116
|
+
const m = readFileSync(dotGit, "utf-8").match(/^gitdir:\s*(.+?)\s*$/m);
|
|
117
|
+
if (m) return isAbsolute(m[1]) ? m[1] : resolve(dir, m[1]);
|
|
118
|
+
}
|
|
119
|
+
} catch {
|
|
120
|
+
// No .git here: go up.
|
|
121
|
+
}
|
|
122
|
+
const up = dirname(dir);
|
|
123
|
+
if (up === dir) return null;
|
|
124
|
+
dir = up;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function real(p: string): string {
|
|
129
|
+
try {
|
|
130
|
+
return realpathSync(p);
|
|
131
|
+
} catch {
|
|
132
|
+
return resolve(p);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** The lock directory for the working tree `start` is in. */
|
|
137
|
+
export function writeLockPath(start: string): string {
|
|
138
|
+
const git = gitDirOf(real(start));
|
|
139
|
+
if (git) return join(git, WRITE_LOCK_NAME);
|
|
140
|
+
return join(tmpdir(), `chant-write-${createHash("sha256").update(real(start)).digest("hex").slice(0, 16)}.lock`);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** The journal for the working tree `start` is in, or null outside git. */
|
|
144
|
+
export function writeJournalPath(start: string): string | null {
|
|
145
|
+
const git = gitDirOf(real(start));
|
|
146
|
+
return git ? join(git, WRITE_JOURNAL_NAME) : null;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ── The lock ─────────────────────────────────────────────────────────────────
|
|
150
|
+
|
|
151
|
+
const HOST = hostname();
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* The locks the running write holds, through every await inside it, so a
|
|
155
|
+
* write inside a write (work evidence's amend) goes ahead and two writes this
|
|
156
|
+
* process runs at once (two requests to chant serve mcp) still wait on each
|
|
157
|
+
* other.
|
|
158
|
+
*/
|
|
159
|
+
const context = new AsyncLocalStorage<ReadonlySet<string>>();
|
|
160
|
+
/** Every lock some write in this process holds now. */
|
|
161
|
+
const heldHere = new Set<string>();
|
|
162
|
+
|
|
163
|
+
function readHolder(lock: string): WriteLockHolder | null {
|
|
164
|
+
try {
|
|
165
|
+
return JSON.parse(readFileSync(join(lock, "owner.json"), "utf-8")) as WriteLockHolder;
|
|
166
|
+
} catch {
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function alive(pid: number): boolean {
|
|
172
|
+
try {
|
|
173
|
+
process.kill(pid, 0);
|
|
174
|
+
return true;
|
|
175
|
+
} catch (err) {
|
|
176
|
+
return (err as NodeJS.ErrnoException).code === "EPERM";
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** Why a lock someone else holds can be broken, or null while it holds. */
|
|
181
|
+
function staleness(lock: string, holder: WriteLockHolder | null, now: number): string | null {
|
|
182
|
+
if (holder === null) {
|
|
183
|
+
// Made and not yet given its owner file, or left that way by a crash: give the maker a moment.
|
|
184
|
+
try {
|
|
185
|
+
return now - statSync(lock).mtimeMs > 5_000 ? "it names no holder" : null;
|
|
186
|
+
} catch {
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
if (Date.parse(holder.expiresAt) < now) return `it expired at ${holder.expiresAt}`;
|
|
191
|
+
if (holder.pid !== null && holder.host === HOST && !alive(holder.pid)) return `its process ${holder.pid} has exited`;
|
|
192
|
+
return null;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function describe(h: WriteLockHolder | null): string {
|
|
196
|
+
if (!h) return "a writer that has not said who it is";
|
|
197
|
+
const who = [h.by, h.agent ? `agent ${h.agent}` : null].filter(Boolean).join(", ");
|
|
198
|
+
return `${h.verb}${who ? ` (${who})` : ""}, since ${h.acquiredAt}, until ${h.expiresAt}`;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** Move a stale lock aside and remove it. Puts it back when another writer took the lock in between. */
|
|
202
|
+
function breakLock(lock: string, stale: WriteLockHolder | null): void {
|
|
203
|
+
const aside = `${lock}.broken-${randomBytes(6).toString("hex")}`;
|
|
204
|
+
try {
|
|
205
|
+
renameSync(lock, aside);
|
|
206
|
+
} catch {
|
|
207
|
+
return; // Gone already: someone else broke or released it.
|
|
208
|
+
}
|
|
209
|
+
const moved = readHolder(aside);
|
|
210
|
+
if (moved !== null && stale !== null && moved.token !== stale.token) {
|
|
211
|
+
// A fresh lock was moved aside: give it back if nobody has taken the path since.
|
|
212
|
+
try {
|
|
213
|
+
renameSync(aside, lock);
|
|
214
|
+
return;
|
|
215
|
+
} catch {
|
|
216
|
+
// The path was taken again; the fresh holder's release finds its lock gone, which it tolerates.
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
rmSync(aside, { recursive: true, force: true });
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** Try once to take the lock. Returns the holder written, or the holder in the way. */
|
|
223
|
+
function tryTake(lock: string, holder: WriteLockHolder, mayBreak = true): { ok: true } | { ok: false; holder: WriteLockHolder | null } {
|
|
224
|
+
try {
|
|
225
|
+
mkdirSync(lock);
|
|
226
|
+
} catch (err) {
|
|
227
|
+
if ((err as NodeJS.ErrnoException).code !== "EEXIST") throw err;
|
|
228
|
+
const current = readHolder(lock);
|
|
229
|
+
if (mayBreak && staleness(lock, current, Date.now()) !== null) {
|
|
230
|
+
breakLock(lock, current);
|
|
231
|
+
return tryTake(lock, holder, false);
|
|
232
|
+
}
|
|
233
|
+
return { ok: false, holder: current };
|
|
234
|
+
}
|
|
235
|
+
const tmp = join(lock, `owner.${process.pid}.tmp`);
|
|
236
|
+
writeFileSync(tmp, `${JSON.stringify(holder, null, 2)}\n`);
|
|
237
|
+
renameSync(tmp, join(lock, "owner.json"));
|
|
238
|
+
return { ok: true };
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function waitMs(): number {
|
|
242
|
+
const raw = process.env[WRITE_LOCK_WAIT_ENV];
|
|
243
|
+
const n = raw === undefined || raw === "" ? NaN : Number(raw);
|
|
244
|
+
return Number.isFinite(n) && n >= 0 ? n : DEFAULT_WAIT_MS;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function newHolder(who: WriteLockWho, ttlMs: number, pid: number | null): WriteLockHolder {
|
|
248
|
+
const now = Date.now();
|
|
249
|
+
return {
|
|
250
|
+
token: randomBytes(16).toString("hex"),
|
|
251
|
+
pid,
|
|
252
|
+
host: HOST,
|
|
253
|
+
verb: who.verb,
|
|
254
|
+
by: who.by ?? null,
|
|
255
|
+
agent: who.agent ?? null,
|
|
256
|
+
acquiredAt: new Date(now).toISOString(),
|
|
257
|
+
expiresAt: new Date(now + ttlMs).toISOString(),
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Whether this call needs no lock of its own: the write it runs inside holds
|
|
263
|
+
* it, or the environment names the token of a batch holding it. Throws
|
|
264
|
+
* write-lock-not-held when the token named no longer holds it.
|
|
265
|
+
*/
|
|
266
|
+
function alreadyHeld(lock: string): boolean {
|
|
267
|
+
if (context.getStore()?.has(lock)) return true;
|
|
268
|
+
const token = process.env[WRITE_LOCK_ENV];
|
|
269
|
+
if (token === undefined || token === "") return false;
|
|
270
|
+
const current = readHolder(lock);
|
|
271
|
+
if (current?.token !== token || staleness(lock, current, Date.now()) !== null) {
|
|
272
|
+
throw new WriteLockError(
|
|
273
|
+
"write-lock-not-held",
|
|
274
|
+
`${WRITE_LOCK_ENV} names a lock token that no longer holds this working tree's write lock (${current ? `it is held by ${describe(current)}` : "nobody holds it"}): the batch's lock was released or expired, so take it again with chant workspace lock acquire and re-read what the batch writes`,
|
|
275
|
+
current,
|
|
276
|
+
);
|
|
277
|
+
}
|
|
278
|
+
return true;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/** Remove the lock at `lock` when `token` holds it. Returns whether it did. */
|
|
282
|
+
export function releaseLock(lock: string, token: string): boolean {
|
|
283
|
+
const current = readHolder(lock);
|
|
284
|
+
if (current?.token !== token) return false;
|
|
285
|
+
try {
|
|
286
|
+
unlinkSync(join(lock, "owner.json"));
|
|
287
|
+
} catch {
|
|
288
|
+
// Already gone.
|
|
289
|
+
}
|
|
290
|
+
rmSync(lock, { recursive: true, force: true });
|
|
291
|
+
return true;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function timeout(lock: string, current: WriteLockHolder | null, ms: number): WriteLockError {
|
|
295
|
+
return new WriteLockError(
|
|
296
|
+
"write-lock-timeout",
|
|
297
|
+
`another write holds this working tree's write lock (${lock}): ${describe(current)}. Waited ${ms}ms; run the write again once it is done, or raise ${WRITE_LOCK_WAIT_ENV}`,
|
|
298
|
+
current,
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const pause = (): number => 20 + Math.floor(Math.random() * 40);
|
|
303
|
+
|
|
304
|
+
/** Take the lock at `lock` as `holder`, waiting up to the wait limit. */
|
|
305
|
+
async function takeWaiting(lock: string, holder: WriteLockHolder): Promise<void> {
|
|
306
|
+
const limit = waitMs();
|
|
307
|
+
const begun = Date.now();
|
|
308
|
+
for (;;) {
|
|
309
|
+
const got = tryTake(lock, holder);
|
|
310
|
+
if (got.ok) return;
|
|
311
|
+
if (Date.now() - begun >= limit) throw timeout(lock, got.holder, limit);
|
|
312
|
+
await new Promise((r) => setTimeout(r, pause()));
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* Run `fn` holding the working tree's write lock, taken for this one write
|
|
318
|
+
* and released when `fn` settles. With `dryRun`, without it, since a dry run
|
|
319
|
+
* writes nothing. Throws a {@link WriteLockError} when the lock can't be had.
|
|
320
|
+
*/
|
|
321
|
+
export async function withWriteLock<T>(start: string, who: WriteLockWho, dryRun: boolean | undefined, fn: () => Promise<T>): Promise<T> {
|
|
322
|
+
if (dryRun) return fn();
|
|
323
|
+
const lock = writeLockPath(start);
|
|
324
|
+
if (alreadyHeld(lock)) return fn();
|
|
325
|
+
const holder = newHolder(who, COMMAND_TTL_MS, process.pid);
|
|
326
|
+
await takeWaiting(lock, holder);
|
|
327
|
+
heldHere.add(lock);
|
|
328
|
+
try {
|
|
329
|
+
return await context.run(new Set([...(context.getStore() ?? []), lock]), fn);
|
|
330
|
+
} finally {
|
|
331
|
+
heldHere.delete(lock);
|
|
332
|
+
releaseLock(lock, holder.token);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* {@link withWriteLock} for a write that is synchronous throughout: it
|
|
338
|
+
* blocks while it waits. When another write in this same process holds the
|
|
339
|
+
* lock, blocking would keep that write from ever finishing, so it is refused
|
|
340
|
+
* with write-lock-timeout at once.
|
|
341
|
+
*/
|
|
342
|
+
export function withWriteLockSync<T>(start: string, who: WriteLockWho, dryRun: boolean | undefined, fn: () => T): T {
|
|
343
|
+
if (dryRun) return fn();
|
|
344
|
+
const lock = writeLockPath(start);
|
|
345
|
+
if (alreadyHeld(lock)) return fn();
|
|
346
|
+
if (heldHere.has(lock)) throw timeout(lock, readHolder(lock), 0);
|
|
347
|
+
const holder = newHolder(who, COMMAND_TTL_MS, process.pid);
|
|
348
|
+
const limit = waitMs();
|
|
349
|
+
const begun = Date.now();
|
|
350
|
+
const cell = new Int32Array(new SharedArrayBuffer(4));
|
|
351
|
+
for (;;) {
|
|
352
|
+
const got = tryTake(lock, holder);
|
|
353
|
+
if (got.ok) break;
|
|
354
|
+
if (Date.now() - begun >= limit) throw timeout(lock, got.holder, limit);
|
|
355
|
+
Atomics.wait(cell, 0, 0, pause());
|
|
356
|
+
}
|
|
357
|
+
heldHere.add(lock);
|
|
358
|
+
try {
|
|
359
|
+
return context.run(new Set([...(context.getStore() ?? []), lock]), fn);
|
|
360
|
+
} finally {
|
|
361
|
+
heldHere.delete(lock);
|
|
362
|
+
releaseLock(lock, holder.token);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* Take the lock for a batch held across chant calls (`lock acquire`): no
|
|
368
|
+
* process holds it, so it lasts until it is released or `ttlMs` passes.
|
|
369
|
+
* Waits like a write does.
|
|
370
|
+
*/
|
|
371
|
+
export async function acquireBatchLock(start: string, who: WriteLockWho, ttlMs: number): Promise<{ path: string; holder: WriteLockHolder }> {
|
|
372
|
+
const lock = writeLockPath(start);
|
|
373
|
+
const holder = newHolder({ ...who, verb: "batch" }, ttlMs, null);
|
|
374
|
+
await takeWaiting(lock, holder);
|
|
375
|
+
return { path: lock, holder };
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
/** Who holds the working tree's write lock now, or null when nobody does (a stale lock counts as nobody). */
|
|
379
|
+
export function writeLockHolder(start: string): { path: string; holder: WriteLockHolder | null } {
|
|
380
|
+
const lock = writeLockPath(start);
|
|
381
|
+
if (!existsSync(lock)) return { path: lock, holder: null };
|
|
382
|
+
const current = readHolder(lock);
|
|
383
|
+
return { path: lock, holder: staleness(lock, current, Date.now()) === null ? current : null };
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// ── Writing a file ───────────────────────────────────────────────────────────
|
|
387
|
+
|
|
388
|
+
/** Write `text` to `file` through a sibling temporary file, so a reader never sees half of it. */
|
|
389
|
+
export function writeFileAtomic(file: string, text: string): void {
|
|
390
|
+
const tmp = `${file}.chant-${process.pid}-${randomBytes(4).toString("hex")}.tmp`;
|
|
391
|
+
writeFileSync(tmp, text);
|
|
392
|
+
try {
|
|
393
|
+
renameSync(tmp, file);
|
|
394
|
+
} catch (err) {
|
|
395
|
+
rmSync(tmp, { force: true });
|
|
396
|
+
throw err;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
// ── The journal ──────────────────────────────────────────────────────────────
|
|
401
|
+
|
|
402
|
+
/** The last chant write of a record file, as the read contract reports it. */
|
|
403
|
+
export interface LastWrite {
|
|
404
|
+
/** The write command, such as `records amend` or `points answer`. */
|
|
405
|
+
verb: string;
|
|
406
|
+
/** The principal the write named (--by, or the channel's author), or null when it named none. */
|
|
407
|
+
by: string | null;
|
|
408
|
+
/** The agent session it was made in (CHANT_AGENT), or null. */
|
|
409
|
+
agent: string | null;
|
|
410
|
+
/** When, as an ISO 8601 time. */
|
|
411
|
+
at: string;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
interface Journal {
|
|
415
|
+
version: 1;
|
|
416
|
+
/** By record path from the repository root: the file's SHA-256 after the write, and the write. */
|
|
417
|
+
files: Record<string, LastWrite & { sha256: string }>;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/** How many files the journal remembers; the oldest are forgotten first. */
|
|
421
|
+
const JOURNAL_LIMIT = 2_000;
|
|
422
|
+
|
|
423
|
+
export function sha256Text(text: string): string {
|
|
424
|
+
return createHash("sha256").update(text, "utf-8").digest("hex");
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function readJournal(file: string): Journal {
|
|
428
|
+
try {
|
|
429
|
+
const j = JSON.parse(readFileSync(file, "utf-8")) as Journal;
|
|
430
|
+
if (j && j.version === 1 && j.files && typeof j.files === "object") return j;
|
|
431
|
+
} catch {
|
|
432
|
+
// Missing or unreadable: start again. It is a cache.
|
|
433
|
+
}
|
|
434
|
+
return { version: 1, files: {} };
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/**
|
|
438
|
+
* Note in the journal that `who` wrote each file, with the text it now
|
|
439
|
+
* holds. Called holding the lock, after the write. Best effort: a journal
|
|
440
|
+
* that can't be written never fails the write it notes.
|
|
441
|
+
*/
|
|
442
|
+
export function noteWrites(start: string, writes: readonly { path: string; text: string }[], who: WriteLockWho): void {
|
|
443
|
+
const file = writeJournalPath(start);
|
|
444
|
+
if (file === null || writes.length === 0) return;
|
|
445
|
+
try {
|
|
446
|
+
const journal = readJournal(file);
|
|
447
|
+
const at = new Date().toISOString();
|
|
448
|
+
for (const w of writes) {
|
|
449
|
+
delete journal.files[w.path];
|
|
450
|
+
journal.files[w.path] = { sha256: sha256Text(w.text), verb: who.verb, by: who.by ?? null, agent: who.agent ?? null, at };
|
|
451
|
+
}
|
|
452
|
+
const keys = Object.keys(journal.files);
|
|
453
|
+
for (const k of keys.slice(0, Math.max(0, keys.length - JOURNAL_LIMIT))) delete journal.files[k];
|
|
454
|
+
writeFileAtomic(file, `${JSON.stringify(journal)}\n`);
|
|
455
|
+
} catch {
|
|
456
|
+
// A cache: never fail the write over it.
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* A reader of the journal for the working tree `start` is in: the last chant
|
|
462
|
+
* write of a record file, while the file still holds the text that write
|
|
463
|
+
* left, and null otherwise (written by something else since, or never by
|
|
464
|
+
* chant here).
|
|
465
|
+
*/
|
|
466
|
+
export function lastWriteReader(start: string): (path: string, text: string | null) => LastWrite | null {
|
|
467
|
+
const file = writeJournalPath(start);
|
|
468
|
+
const journal = file !== null && existsSync(file) ? readJournal(file) : null;
|
|
469
|
+
return (path, text) => {
|
|
470
|
+
const e = journal?.files[path];
|
|
471
|
+
if (!e || text === null || e.sha256 !== sha256Text(text)) return null;
|
|
472
|
+
return { verb: e.verb, by: e.by, agent: e.agent, at: e.at };
|
|
473
|
+
};
|
|
474
|
+
}
|
|
@@ -21,13 +21,14 @@ import { cleanScratch, contract, git, REPO, repo, writeFiles } from "./__fixture
|
|
|
21
21
|
import { agentSession } from "./agent-cli";
|
|
22
22
|
import agentSchema from "./agent.schema.json";
|
|
23
23
|
import { checkChanges, type ChangesDocument } from "./changes";
|
|
24
|
+
import { runDeclarationChecks } from "./checks";
|
|
24
25
|
import changesSchema from "./changes.schema.json";
|
|
25
26
|
import { formatChanges } from "./changes-cli";
|
|
26
27
|
import { parseDeclaration, WorkspaceReadError } from "./declaration";
|
|
27
28
|
import { parseFrontMatter } from "./records";
|
|
28
29
|
import { amendRecord, newRecord, renderRecord, reviewRecord } from "./records-write";
|
|
29
30
|
import { emptyPolicy } from "./trust/policy";
|
|
30
|
-
import { judgePath, judgeRecord, principalClass, resolveWriter, WriteScopeError } from "./write-scope";
|
|
31
|
+
import { judgePath, judgeRecord, onlyKeysChanged, principalClass, resolveWriter, WriteScopeError } from "./write-scope";
|
|
31
32
|
import { createWorkspaceTools } from "../cli/mcp/workspace-tools";
|
|
32
33
|
|
|
33
34
|
const DECISIONS = join(REPO, "docs", "design", "decisions");
|
|
@@ -109,8 +110,8 @@ describe("the declaration's writeScope and agents (#2548)", () => {
|
|
|
109
110
|
test("are read with their defaults", () => {
|
|
110
111
|
const d = parseDeclaration(declaration(), "chant.workspace.json");
|
|
111
112
|
expect(d.writeScope).toEqual({
|
|
112
|
-
agent: { members: null, records: { decision: ["new", "review"] }, pointer: "/writeScope/agent" },
|
|
113
|
-
runner: { members: ["app"], records: { decision: ["review"] }, pointer: "/writeScope/runner" },
|
|
113
|
+
agent: { members: null, records: { decision: ["new", "review"] }, protected: [], pointer: "/writeScope/agent" },
|
|
114
|
+
runner: { members: ["app"], records: { decision: ["review"] }, protected: [], pointer: "/writeScope/runner" },
|
|
114
115
|
});
|
|
115
116
|
expect(d.agents.map((a) => [a.name, a.member, a.principals])).toEqual([
|
|
116
117
|
["app-agent", "app", ["bot@example.com"]],
|
|
@@ -140,6 +141,21 @@ describe("the declaration's writeScope and agents (#2548)", () => {
|
|
|
140
141
|
});
|
|
141
142
|
});
|
|
142
143
|
|
|
144
|
+
describe("a protected JSON file's except by JSON Pointer (#3308)", () => {
|
|
145
|
+
const decl = (listing: unknown, intent = "ws-001") => JSON.stringify({ name: "w", members: [{ name: "a", box: { intent, listing } }, { name: "b" }] });
|
|
146
|
+
test("a * token matches every key or index at its level, and only what the pointer names may change", () => {
|
|
147
|
+
expect(onlyKeysChanged(decl({ title: "A" }), decl({ title: "B" }), ["/members/*/box/listing"])).toBe(true);
|
|
148
|
+
expect(onlyKeysChanged(decl({ title: "A" }), decl(undefined), ["/members/*/box/listing"])).toBe(true);
|
|
149
|
+
expect(onlyKeysChanged(decl({ title: "A" }), decl({ title: "B" }, "ws-002"), ["/members/*/box/listing"])).toBe(false);
|
|
150
|
+
expect(onlyKeysChanged(decl({ title: "A" }), decl({ title: "B" }), ["/members/1/box/listing"])).toBe(false);
|
|
151
|
+
expect(onlyKeysChanged(decl({ title: "A" }), decl({ title: "B" }), ["/members/0/box/listing"])).toBe(true);
|
|
152
|
+
expect(onlyKeysChanged(decl({ title: "A" }), decl({ title: "B" }), ["name"])).toBe(false);
|
|
153
|
+
});
|
|
154
|
+
test("reads a .jsonc file's comments and trailing commas", () => {
|
|
155
|
+
expect(onlyKeysChanged('{ "a": 1, // x\n "b": 2, }', '{ "a": 1, "b": 3 }', ["b"])).toBe(true);
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
143
159
|
describe("judging a write", () => {
|
|
144
160
|
const decl = parseDeclaration(declaration(), "chant.workspace.json");
|
|
145
161
|
const policy = { ...emptyPolicy("x"), roles: { runner: ["CI@example.com"], agent: ["model@example.com"] } };
|
|
@@ -326,6 +342,7 @@ describe("chant workspace agent: what a session reloads from (#2548)", () => {
|
|
|
326
342
|
{ path: KIND, kind: "decision", name: null, member: null, verbs: ["new", "review"] },
|
|
327
343
|
{ path: DESIGN_KIND, kind: "decision", name: "screen", member: "design", verbs: ["new", "review"] },
|
|
328
344
|
],
|
|
345
|
+
protected: [],
|
|
329
346
|
});
|
|
330
347
|
expect(doc.spec.kinds.map((k) => k.path)).toEqual([KIND, DESIGN_KIND]);
|
|
331
348
|
expect(doc.spec.records.map((r) => `${r.kind}/${r.id}`)).toContain("decision/ws-001");
|
|
@@ -338,3 +355,127 @@ describe("chant workspace agent: what a session reloads from (#2548)", () => {
|
|
|
338
355
|
expect(code(doc)).toBe("agent-unknown");
|
|
339
356
|
});
|
|
340
357
|
});
|
|
358
|
+
|
|
359
|
+
describe("principal classes a plugin defines (#3080)", () => {
|
|
360
|
+
const PLUGIN = "plugins/review";
|
|
361
|
+
const PRINCIPALS = JSON.stringify({ schema: 1, classes: [{ name: "reviewer", description: "people who review decisions", role: "reviewer" }] });
|
|
362
|
+
const REVIEWER_SCOPE = { reviewer: { members: ["design"], records: { decision: ["review"] } } };
|
|
363
|
+
let dom: string;
|
|
364
|
+
const at: Record<string, string> = {};
|
|
365
|
+
|
|
366
|
+
function domainDeclaration(scope: unknown): string {
|
|
367
|
+
return JSON.stringify({ ...JSON.parse(declaration(scope, [])), pins: [{ path: PLUGIN }] }, null, 2);
|
|
368
|
+
}
|
|
369
|
+
function commitIn(message: string, email?: string): string {
|
|
370
|
+
git(dom, "add", "-A");
|
|
371
|
+
git(dom, ...(email ? ["-c", `user.email=${email}`] : []), "commit", "-q", "-m", message);
|
|
372
|
+
return git(dom, "rev-parse", "HEAD");
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
beforeAll(() => {
|
|
376
|
+
const kind = readFileSync(join(DECISIONS, "decision.kind.mjs"), "utf-8");
|
|
377
|
+
const schema = readFileSync(join(DECISIONS, "decision.schema.json"), "utf-8");
|
|
378
|
+
dom = repo({
|
|
379
|
+
"chant.workspace.json": domainDeclaration(REVIEWER_SCOPE),
|
|
380
|
+
".chant/trust.json": JSON.stringify({ schema: 1, roles: { reviewer: ["rev@example.com"] } }),
|
|
381
|
+
[`${PLUGIN}/package.json`]: JSON.stringify({ name: "review-classes", version: "1.0.0", exports: { "./workspace-principals": "./workspace-principals.json" } }),
|
|
382
|
+
[`${PLUGIN}/workspace-principals.json`]: PRINCIPALS,
|
|
383
|
+
[KIND]: kind,
|
|
384
|
+
"decisions/decision.schema.json": schema,
|
|
385
|
+
"decisions/ws-001-one.md": record(decided({ id: "ws-001", title: "One" })),
|
|
386
|
+
[DESIGN_KIND]: kind,
|
|
387
|
+
"design/decisions/decision.schema.json": schema,
|
|
388
|
+
"design/decisions/ws-001-screen.md": record(decided({ id: "ws-001", title: "Screen" })),
|
|
389
|
+
"app/server.mjs": "export const port = 8080;\n",
|
|
390
|
+
"design/spec.md": "# Spec\n",
|
|
391
|
+
});
|
|
392
|
+
at.base = commitIn("the workspace");
|
|
393
|
+
git(dom, "branch", "-M", "main");
|
|
394
|
+
});
|
|
395
|
+
|
|
396
|
+
test("writeScope takes a domain class's name as a key, in file order", () => {
|
|
397
|
+
const d = parseDeclaration(domainDeclaration({ reviewer: { records: { decision: ["review"] } }, human: {} }), "chant.workspace.json");
|
|
398
|
+
expect(Object.keys(d.writeScope!)).toEqual(["reviewer", "human"]);
|
|
399
|
+
expect(() => parseDeclaration(domainDeclaration({ "x-reviewer": {} }), "chant.workspace.json")).toThrow(WorkspaceReadError);
|
|
400
|
+
expect(() => parseDeclaration(domainDeclaration({ Reviewer: {} }), "chant.workspace.json")).toThrow(WorkspaceReadError);
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
test("a principal holding the class's role at base is held to its entry; anyone else is a human", async () => {
|
|
404
|
+
expect(code(await reviewRecord({ kind: KIND, id: "ws-001", verdict: "agree", by: "rev@example.com", cwd: dom, dryRun: true }))).toBeNull();
|
|
405
|
+
const proposed = await newRecord({ kind: KIND, fields: newDecision("Logs"), by: "rev@example.com", cwd: dom, dryRun: true });
|
|
406
|
+
expect(proposed).toMatchObject({ error: { code: "write-scope-kind", message: expect.stringContaining("rev@example.com (reviewer) may review decision records, and not new") } });
|
|
407
|
+
expect(code(await newRecord({ kind: KIND, fields: newDecision("Logs"), by: "lex00", cwd: dom, dryRun: true }))).toBeNull();
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
test("a path-pinned plugin's classes are read at base: remapping the class in the working tree changes nothing", async () => {
|
|
411
|
+
writeFiles(dom, { [`${PLUGIN}/workspace-principals.json`]: JSON.stringify({ schema: 1, classes: [{ name: "reviewer", description: "d", role: "nobody" }] }) });
|
|
412
|
+
try {
|
|
413
|
+
expect(code(await newRecord({ kind: KIND, fields: newDecision("Logs"), by: "rev@example.com", cwd: dom, dryRun: true }))).toBe("write-scope-kind");
|
|
414
|
+
} finally {
|
|
415
|
+
writeFiles(dom, { [`${PLUGIN}/workspace-principals.json`]: PRINCIPALS });
|
|
416
|
+
}
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
test("check --changes judges a commit by its author's domain class", async () => {
|
|
420
|
+
git(dom, "checkout", "-q", "-b", "work");
|
|
421
|
+
writeFiles(dom, { "app/server.mjs": "export const port = 9090;\n" });
|
|
422
|
+
at.outside = commitIn("app: a port", "rev@example.com");
|
|
423
|
+
writeFiles(dom, { "design/spec.md": "# Spec, reviewed\n" });
|
|
424
|
+
at.inside = commitIn("design: a note", "rev@example.com");
|
|
425
|
+
const { doc } = (await checkChanges({ cwd: dom, range: "main..work" })) as { doc: Exclude<ChangesDocument, { error: unknown }> };
|
|
426
|
+
contract(changesSchema).expectValid(doc);
|
|
427
|
+
expect(doc.scope).toMatchObject({ restricted: ["reviewer"] });
|
|
428
|
+
expect(doc.scope!.commits.map((c) => [c.commit, c.class])).toEqual([
|
|
429
|
+
[at.outside, "reviewer"],
|
|
430
|
+
[at.inside, "reviewer"],
|
|
431
|
+
]);
|
|
432
|
+
expect(doc.scope!.findings.map((f) => [f.commit, f.code, f.path, f.class])).toEqual([[at.outside, "write-scope-member", "app/server.mjs", "reviewer"]]);
|
|
433
|
+
git(dom, "checkout", "-q", "main");
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
describe("a class no pinned package supplies fails closed", () => {
|
|
437
|
+
beforeAll(() => {
|
|
438
|
+
writeFiles(dom, { "chant.workspace.json": domainDeclaration({ ...REVIEWER_SCOPE, auditor: { members: ["design"] } }) });
|
|
439
|
+
at.unknown = commitIn("an auditor class no plugin supplies");
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
test("a writer judged human is refused; one in a known class is judged by it", async () => {
|
|
443
|
+
const refused = await newRecord({ kind: KIND, fields: newDecision("Logs"), by: "lex00", cwd: dom, dryRun: true });
|
|
444
|
+
expect(refused).toMatchObject({ error: { code: "write-scope-class-unknown", message: expect.stringContaining("writeScope.auditor") } });
|
|
445
|
+
expect(code(await newRecord({ kind: KIND, fields: newDecision("Logs"), cwd: dom, dryRun: true }))).toBe("write-scope-class-unknown");
|
|
446
|
+
expect(code(await reviewRecord({ kind: KIND, id: "ws-001", verdict: "agree", by: "rev@example.com", cwd: dom, dryRun: true }))).toBeNull();
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
test("check --changes reports the human's commit as a whole", async () => {
|
|
450
|
+
git(dom, "checkout", "-q", "-b", "audit");
|
|
451
|
+
writeFiles(dom, { "design/spec.md": "# Spec, by a person\n" });
|
|
452
|
+
const human = commitIn("a person's edit");
|
|
453
|
+
const { doc } = (await checkChanges({ cwd: dom, range: "main..audit" })) as { doc: Exclude<ChangesDocument, { error: unknown }> };
|
|
454
|
+
contract(changesSchema).expectValid(doc);
|
|
455
|
+
expect(doc.scope).toMatchObject({ restricted: ["reviewer", "auditor"] });
|
|
456
|
+
expect(doc.scope!.findings.map((f) => [f.id, f.path, f.class])).toEqual([[`finding:write-scope-class-unknown:${human}`, null, "human"]]);
|
|
457
|
+
git(dom, "checkout", "-q", "main");
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
test("workspace check reports the unknown class with WSP003", async () => {
|
|
461
|
+
const report = await runDeclarationChecks(dom, undefined, { gather: false });
|
|
462
|
+
const found = report.diagnostics.filter((d) => d.ruleId === "WSP003");
|
|
463
|
+
expect(found).toHaveLength(1);
|
|
464
|
+
expect(found[0].message).toMatch(/writeScope names the principal class auditor, which no core class or pinned package supplies/);
|
|
465
|
+
expect(found[0].message).toMatch(/known classes: human, agent, runner, service, reviewer/);
|
|
466
|
+
});
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
test("workspace check reports an unreadable principals file with WSP002", async () => {
|
|
470
|
+
const root = repo({
|
|
471
|
+
"chant.workspace.json": domainDeclaration(REVIEWER_SCOPE),
|
|
472
|
+
[`${PLUGIN}/package.json`]: JSON.stringify({ exports: { "./workspace-principals": "./workspace-principals.json" } }),
|
|
473
|
+
[`${PLUGIN}/workspace-principals.json`]: JSON.stringify({ schema: 1, classes: [{ name: "human", description: "d", role: "people" }] }),
|
|
474
|
+
"app/server.mjs": "",
|
|
475
|
+
"design/spec.md": "",
|
|
476
|
+
});
|
|
477
|
+
const report = await runDeclarationChecks(root, undefined, { gather: false });
|
|
478
|
+
expect(report.diagnostics.filter((d) => d.ruleId === "WSP002").map((d) => d.message)).toEqual([`${PLUGIN}: class human is a core class and can't be supplied by a package`]);
|
|
479
|
+
expect(report.diagnostics.filter((d) => d.ruleId === "WSP003").map((d) => d.message)).toEqual([expect.stringContaining("principal class reviewer")]);
|
|
480
|
+
});
|
|
481
|
+
});
|