@intentius/chant 0.102.0 → 0.104.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/audit/core.d.ts +2 -0
- package/dist/audit/core.d.ts.map +1 -1
- package/dist/audit/discover.d.ts.map +1 -1
- package/dist/change-set.d.ts +232 -0
- package/dist/change-set.d.ts.map +1 -0
- package/dist/cli/commands/audit.d.ts +7 -0
- package/dist/cli/commands/audit.d.ts.map +1 -1
- package/dist/cli/commands/build.d.ts.map +1 -1
- package/dist/cli/handlers/build.d.ts.map +1 -1
- package/dist/cli/handlers/change-set.d.ts +11 -0
- package/dist/cli/handlers/change-set.d.ts.map +1 -0
- package/dist/cli/handlers/fan-out.d.ts +33 -0
- package/dist/cli/handlers/fan-out.d.ts.map +1 -1
- package/dist/cli/handlers/operator.d.ts +7 -0
- package/dist/cli/handlers/operator.d.ts.map +1 -1
- package/dist/cli/handlers/pr.d.ts +31 -0
- package/dist/cli/handlers/pr.d.ts.map +1 -0
- package/dist/cli/handlers/run.d.ts.map +1 -1
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/mcp/op-tools.d.ts.map +1 -1
- package/dist/cli/mcp/workspace-tools.d.ts +3 -2
- package/dist/cli/mcp/workspace-tools.d.ts.map +1 -1
- package/dist/cli/registry.d.ts +63 -1
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/components/auto-release.d.ts +16 -0
- package/dist/components/auto-release.d.ts.map +1 -1
- package/dist/components/capability.d.ts +53 -0
- package/dist/components/capability.d.ts.map +1 -1
- package/dist/components/changed-files.d.ts +31 -0
- package/dist/components/changed-files.d.ts.map +1 -0
- package/dist/components/cli-support.d.ts +9 -1
- package/dist/components/cli-support.d.ts.map +1 -1
- package/dist/components/component.d.ts +24 -0
- package/dist/components/component.d.ts.map +1 -1
- package/dist/components/deploy-units.d.ts.map +1 -1
- package/dist/components/driver.d.ts +13 -0
- package/dist/components/driver.d.ts.map +1 -1
- package/dist/components/fan-out-output.d.ts +6 -0
- package/dist/components/fan-out-output.d.ts.map +1 -1
- package/dist/components/fan-out-record.d.ts +41 -0
- package/dist/components/fan-out-record.d.ts.map +1 -0
- package/dist/components/fan-out-run.d.ts +65 -0
- package/dist/components/fan-out-run.d.ts.map +1 -1
- package/dist/components/fan-out-support.d.ts +2 -0
- package/dist/components/fan-out-support.d.ts.map +1 -1
- package/dist/components/fan-out.d.ts +6 -0
- package/dist/components/fan-out.d.ts.map +1 -1
- package/dist/components/gated-wave-pipeline.d.ts +50 -0
- package/dist/components/gated-wave-pipeline.d.ts.map +1 -0
- package/dist/components/index.d.ts +1 -1
- package/dist/components/index.d.ts.map +1 -1
- package/dist/components/layers.d.ts +25 -0
- package/dist/components/layers.d.ts.map +1 -0
- package/dist/components/pr-pipeline.d.ts +61 -0
- package/dist/components/pr-pipeline.d.ts.map +1 -0
- package/dist/components/pr-run.d.ts +119 -0
- package/dist/components/pr-run.d.ts.map +1 -0
- package/dist/components/promote.d.ts.map +1 -1
- package/dist/components/verbs/shell.d.ts.map +1 -1
- package/dist/components/wave-plan.d.ts +49 -0
- package/dist/components/wave-plan.d.ts.map +1 -0
- package/dist/gated-waves.d.ts +147 -0
- package/dist/gated-waves.d.ts.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/lexicon.d.ts +48 -0
- package/dist/lexicon.d.ts.map +1 -1
- package/dist/lifecycle/gate-ledger.d.ts +7 -0
- package/dist/lifecycle/gate-ledger.d.ts.map +1 -1
- package/dist/lifecycle/git.d.ts +46 -1
- package/dist/lifecycle/git.d.ts.map +1 -1
- package/dist/lint/post-synth.d.ts +18 -0
- package/dist/lint/post-synth.d.ts.map +1 -1
- package/dist/op/activities/activity-contracts.d.ts +126 -0
- package/dist/op/activities/activity-contracts.d.ts.map +1 -1
- package/dist/op/activities/change-set.d.ts +62 -0
- package/dist/op/activities/change-set.d.ts.map +1 -0
- package/dist/op/activities/decide.d.ts +2 -0
- package/dist/op/activities/decide.d.ts.map +1 -1
- package/dist/op/activities/factory.d.ts +137 -0
- package/dist/op/activities/factory.d.ts.map +1 -0
- package/dist/op/activities/index.d.ts +4 -0
- package/dist/op/activities/index.d.ts.map +1 -1
- package/dist/op/activity-contract.d.ts.map +1 -1
- package/dist/op/builders.d.ts +11 -0
- package/dist/op/builders.d.ts.map +1 -1
- package/dist/op/decide-backend.d.ts +5 -1
- package/dist/op/decide-backend.d.ts.map +1 -1
- package/dist/op/decide-read-inputs.d.ts +4 -2
- package/dist/op/decide-read-inputs.d.ts.map +1 -1
- package/dist/op/factory.d.ts +83 -0
- package/dist/op/factory.d.ts.map +1 -0
- package/dist/op/index.d.ts +4 -1
- package/dist/op/index.d.ts.map +1 -1
- package/dist/op/op-ir.d.ts +2 -0
- package/dist/op/op-ir.d.ts.map +1 -1
- package/dist/op/op-verb-class.d.ts.map +1 -1
- package/dist/op/types.d.ts +8 -0
- package/dist/op/types.d.ts.map +1 -1
- package/dist/op/work-lease-run.d.ts.map +1 -1
- package/dist/plan-summary.d.ts +207 -0
- package/dist/plan-summary.d.ts.map +1 -0
- package/dist/pr-forge.d.ts +105 -0
- package/dist/pr-forge.d.ts.map +1 -0
- package/dist/pr-loop.d.ts +178 -0
- package/dist/pr-loop.d.ts.map +1 -0
- package/dist/telemetry-attribution.d.ts +31 -0
- package/dist/telemetry-attribution.d.ts.map +1 -1
- package/dist/terraform/hcl2json.d.ts +45 -0
- package/dist/terraform/hcl2json.d.ts.map +1 -0
- package/dist/terraform/parse.d.ts +4 -40
- package/dist/terraform/parse.d.ts.map +1 -1
- package/dist/workspace/box-cli.d.ts +12 -0
- package/dist/workspace/box-cli.d.ts.map +1 -0
- package/dist/workspace/box-factory.d.ts +27 -3
- package/dist/workspace/box-factory.d.ts.map +1 -1
- package/dist/workspace/box-listing.d.ts +85 -0
- package/dist/workspace/box-listing.d.ts.map +1 -0
- package/dist/workspace/box-publish.d.ts +147 -0
- package/dist/workspace/box-publish.d.ts.map +1 -0
- package/dist/workspace/broker-protocol.d.ts +190 -0
- package/dist/workspace/broker-protocol.d.ts.map +1 -0
- package/dist/workspace/checks.d.ts.map +1 -1
- package/dist/workspace/composites.d.ts +2 -2
- package/dist/workspace/composites.d.ts.map +1 -1
- package/dist/workspace/conformance/__fixtures__/reference-broker.d.ts +17 -0
- package/dist/workspace/conformance/__fixtures__/reference-broker.d.ts.map +1 -0
- package/dist/workspace/conformance/broker.d.ts +202 -0
- package/dist/workspace/conformance/broker.d.ts.map +1 -0
- package/dist/workspace/conformance/index.d.ts +2 -1
- package/dist/workspace/conformance/index.d.ts.map +1 -1
- package/dist/workspace/conformance/vitest.d.ts +9 -2
- package/dist/workspace/conformance/vitest.d.ts.map +1 -1
- package/dist/workspace/conformance/writer.d.ts +108 -14
- package/dist/workspace/conformance/writer.d.ts.map +1 -1
- package/dist/workspace/decide.d.ts +126 -12
- package/dist/workspace/decide.d.ts.map +1 -1
- package/dist/workspace/decision-points.schema.json +11 -4
- package/dist/workspace/declaration.d.ts +65 -0
- package/dist/workspace/declaration.d.ts.map +1 -1
- package/dist/workspace/declaration.schema.json +153 -4
- package/dist/workspace/environments.d.ts +36 -10
- package/dist/workspace/environments.d.ts.map +1 -1
- package/dist/workspace/factory-rules.d.ts +180 -0
- package/dist/workspace/factory-rules.d.ts.map +1 -0
- package/dist/workspace/init.d.ts +21 -0
- package/dist/workspace/init.d.ts.map +1 -1
- package/dist/workspace/intent-cli.d.ts +1 -1
- package/dist/workspace/intent-cli.d.ts.map +1 -1
- package/dist/workspace/intent-record.d.ts +28 -1
- package/dist/workspace/intent-record.d.ts.map +1 -1
- package/dist/workspace/intent.d.ts +102 -5
- package/dist/workspace/intent.d.ts.map +1 -1
- package/dist/workspace/json-edit.d.ts +24 -0
- package/dist/workspace/json-edit.d.ts.map +1 -0
- package/dist/workspace/jsonc.d.ts +11 -0
- package/dist/workspace/jsonc.d.ts.map +1 -1
- package/dist/workspace/kinds.d.ts +48 -0
- package/dist/workspace/kinds.d.ts.map +1 -1
- package/dist/workspace/lexicon-symbols.d.ts +29 -0
- package/dist/workspace/lexicon-symbols.d.ts.map +1 -0
- package/dist/workspace/lineage-upgrade.d.ts +31 -2
- package/dist/workspace/lineage-upgrade.d.ts.map +1 -1
- package/dist/workspace/point-answer.schema.json +42 -2
- package/dist/workspace/points-cli.d.ts +10 -3
- package/dist/workspace/points-cli.d.ts.map +1 -1
- package/dist/workspace/points.d.ts +66 -5
- package/dist/workspace/points.d.ts.map +1 -1
- package/dist/workspace/reason-codes.d.ts +33 -4
- package/dist/workspace/reason-codes.d.ts.map +1 -1
- package/dist/workspace/record-sessions.d.ts +5 -7
- package/dist/workspace/record-sessions.d.ts.map +1 -1
- package/dist/workspace/records-cli.d.ts +7 -0
- package/dist/workspace/records-cli.d.ts.map +1 -1
- package/dist/workspace/records-close.d.ts +7 -3
- package/dist/workspace/records-close.d.ts.map +1 -1
- package/dist/workspace/records-write.d.ts +66 -6
- package/dist/workspace/records-write.d.ts.map +1 -1
- package/dist/workspace/records.d.ts +28 -9
- package/dist/workspace/records.d.ts.map +1 -1
- package/dist/workspace/runs-cli.d.ts +138 -6
- package/dist/workspace/runs-cli.d.ts.map +1 -1
- package/dist/workspace/runs.d.ts +132 -11
- package/dist/workspace/runs.d.ts.map +1 -1
- package/dist/workspace/session-kinds.d.ts +1 -1
- package/dist/workspace/session-kinds.d.ts.map +1 -1
- package/dist/workspace/squash.d.ts +80 -0
- package/dist/workspace/squash.d.ts.map +1 -0
- package/dist/workspace/status-gates.d.ts +2 -0
- package/dist/workspace/status-gates.d.ts.map +1 -1
- package/dist/workspace/status.d.ts +54 -0
- package/dist/workspace/status.d.ts.map +1 -1
- package/dist/workspace/symbols.d.ts +16 -5
- package/dist/workspace/symbols.d.ts.map +1 -1
- package/dist/workspace/trust/run-commits.d.ts +46 -0
- package/dist/workspace/trust/run-commits.d.ts.map +1 -0
- package/dist/workspace/trust/run-statement.d.ts +128 -0
- package/dist/workspace/trust/run-statement.d.ts.map +1 -0
- package/dist/workspace/trust/seal.d.ts +6 -1
- package/dist/workspace/trust/seal.d.ts.map +1 -1
- package/dist/workspace/trust/verify-cli.d.ts +1 -1
- package/dist/workspace/trust/verify.d.ts +16 -2
- package/dist/workspace/trust/verify.d.ts.map +1 -1
- package/dist/workspace/wip-cli.d.ts +21 -0
- package/dist/workspace/wip-cli.d.ts.map +1 -0
- package/dist/workspace/wip.d.ts +268 -0
- package/dist/workspace/wip.d.ts.map +1 -0
- package/dist/workspace/work-cli.d.ts.map +1 -1
- package/dist/workspace/work-evidence.d.ts +1 -1
- package/dist/workspace/work-evidence.d.ts.map +1 -1
- package/dist/workspace/work-size.d.ts +55 -0
- package/dist/workspace/work-size.d.ts.map +1 -0
- package/dist/workspace/workspace-kinds.schema.json +122 -0
- package/dist/workspace/write-lock-cli.d.ts +60 -0
- package/dist/workspace/write-lock-cli.d.ts.map +1 -0
- package/dist/workspace/write-lock.d.ts +152 -0
- package/dist/workspace/write-lock.d.ts.map +1 -0
- package/dist/workspace/write-scope.d.ts +6 -2
- package/dist/workspace/write-scope.d.ts.map +1 -1
- package/package.json +6 -1
- package/src/__fixtures__/change-set/lifecycle-plan.floci.json +59 -0
- package/src/__fixtures__/change-set/lifecycle-v1.ts.txt +4 -0
- package/src/__fixtures__/change-set/lifecycle-v2.ts.txt +4 -0
- package/src/__fixtures__/change-set/lifecycle.change-set.golden.json +95 -0
- package/src/__fixtures__/change-set/warden.change-set.golden.json +106 -0
- package/src/__fixtures__/change-set/warden.plan.json +44 -0
- package/src/__fixtures__/plan-summary/normalization-vectors.json +1593 -0
- package/src/__fixtures__/pr-loop/pr-apply-applied.note.golden.md +31 -0
- package/src/__fixtures__/pr-loop/pr-apply-refused.note.golden.md +31 -0
- package/src/__fixtures__/pr-loop/pr-plan.golden.json +127 -0
- package/src/__fixtures__/pr-loop/pr-plan.note.golden.md +37 -0
- package/src/audit/core.test.ts +15 -0
- package/src/audit/core.ts +8 -2
- package/src/audit/discover.test.ts +11 -0
- package/src/audit/discover.ts +7 -2
- package/src/change-set-bundle.test.ts +57 -0
- package/src/change-set.test.ts +215 -0
- package/src/change-set.ts +445 -0
- package/src/cli/commands/audit-opt-in.test.ts +54 -0
- package/src/cli/commands/audit.ts +29 -2
- package/src/cli/commands/build.test.ts +19 -0
- package/src/cli/commands/build.ts +3 -2
- package/src/cli/handlers/build.ts +13 -1
- package/src/cli/handlers/change-set.test.ts +56 -0
- package/src/cli/handlers/change-set.ts +44 -0
- package/src/cli/handlers/fan-out.test.ts +221 -2
- package/src/cli/handlers/fan-out.ts +165 -75
- package/src/cli/handlers/graph.ts +3 -0
- package/src/cli/handlers/operator.test.ts +21 -0
- package/src/cli/handlers/operator.ts +34 -3
- package/src/cli/handlers/pr.ts +373 -0
- package/src/cli/handlers/run.ts +1 -0
- package/src/cli/main.test.ts +20 -0
- package/src/cli/main.ts +185 -23
- package/src/cli/mcp/never-over-mcp.test.ts +101 -0
- package/src/cli/mcp/op-tools.ts +24 -2
- package/src/cli/mcp/workspace-tools.test.ts +1 -1
- package/src/cli/mcp/workspace-tools.ts +34 -6
- package/src/cli/registry.ts +63 -1
- package/src/components/auto-release.ts +1 -1
- package/src/components/capability.ts +55 -0
- package/src/components/changed-files.ts +66 -0
- package/src/components/cli-support.ts +28 -5
- package/src/components/component.schema.json +17 -0
- package/src/components/component.ts +25 -0
- package/src/components/deploy-units.ts +3 -0
- package/src/components/driver.test.ts +52 -0
- package/src/components/driver.ts +31 -13
- package/src/components/fan-out-output.ts +42 -1
- package/src/components/fan-out-record.ts +69 -0
- package/src/components/fan-out-run.test.ts +17 -0
- package/src/components/fan-out-run.ts +304 -1
- package/src/components/fan-out-support.ts +3 -0
- package/src/components/fan-out-waves.test.ts +375 -0
- package/src/components/fan-out.ts +18 -17
- package/src/components/gated-wave-pipeline.ts +82 -0
- package/src/components/index.ts +1 -0
- package/src/components/layers.ts +30 -0
- package/src/components/pr-pipeline.ts +91 -0
- package/src/components/pr-run.test.ts +205 -0
- package/src/components/pr-run.ts +280 -0
- package/src/components/promote.ts +5 -0
- package/src/components/verbs/shell.test.ts +11 -0
- package/src/components/verbs/shell.ts +7 -3
- package/src/components/wave-plan.ts +124 -0
- package/src/discovery/sandbox/bundle.ts +1 -1
- package/src/gated-waves-bundle.test.ts +62 -0
- package/src/gated-waves.test.ts +118 -0
- package/src/gated-waves.ts +240 -0
- package/src/index.ts +1 -0
- package/src/lexicon.ts +51 -0
- package/src/lifecycle/gate-ledger.ts +7 -0
- package/src/lifecycle/git.ts +102 -16
- package/src/lint/post-synth.test.ts +19 -0
- package/src/lint/post-synth.ts +19 -1
- package/src/meta/declared-imports.test.ts +1 -1
- package/src/op/activities/activity-contracts.ts +84 -0
- package/src/op/activities/change-set.ts +114 -0
- package/src/op/activities/decide.ts +3 -0
- package/src/op/activities/factory.ts +675 -0
- package/src/op/activities/index.ts +6 -0
- package/src/op/activity-contract.test.ts +21 -1
- package/src/op/activity-contract.ts +19 -9
- package/src/op/builders.ts +23 -0
- package/src/op/decide-backend.ts +5 -1
- package/src/op/decide-read-inputs.ts +33 -5
- package/src/op/factory.e2e.test.ts +232 -0
- package/src/op/factory.ts +118 -0
- package/src/op/index.ts +9 -1
- package/src/op/op-ir.test.ts +9 -0
- package/src/op/op-ir.ts +4 -0
- package/src/op/op-verb-class.ts +3 -0
- package/src/op/types.ts +8 -0
- package/src/op/work-lease-run.ts +3 -0
- package/src/plan-summary.test.ts +263 -0
- package/src/plan-summary.ts +845 -0
- package/src/pr-forge.test.ts +237 -0
- package/src/pr-forge.ts +332 -0
- package/src/pr-loop-bundle.test.ts +46 -0
- package/src/pr-loop.test.ts +171 -0
- package/src/pr-loop.ts +324 -0
- package/src/telemetry-attribution.test.ts +23 -1
- package/src/telemetry-attribution.ts +51 -0
- package/src/terraform/hcl2json.test.ts +21 -0
- package/src/terraform/hcl2json.ts +81 -0
- package/src/terraform/parse.ts +11 -74
- package/src/workspace/__fixtures__/studio-template-84c8b21.json +1 -1
- package/src/workspace/app-kind.test.ts +196 -0
- package/src/workspace/box-cli.ts +113 -0
- package/src/workspace/box-factory.test.ts +4 -1
- package/src/workspace/box-factory.ts +37 -4
- package/src/workspace/box-listing-write.schema.json +204 -0
- package/src/workspace/box-listing.test.ts +234 -0
- package/src/workspace/box-listing.ts +324 -0
- package/src/workspace/box-publish.schema.json +498 -0
- package/src/workspace/box-publish.test.ts +204 -0
- package/src/workspace/box-publish.ts +420 -0
- package/src/workspace/box-ship.test.ts +96 -0
- package/src/workspace/broker-protocol.schema.json +186 -0
- package/src/workspace/broker-protocol.test.ts +122 -0
- package/src/workspace/broker-protocol.ts +317 -0
- package/src/workspace/builder-tiers.test.ts +146 -0
- package/src/workspace/change-set.schema.json +460 -0
- package/src/workspace/chant-migrations.e2e.test.ts +17 -5
- package/src/workspace/checks/boxes.test.ts +3 -0
- package/src/workspace/checks.ts +10 -3
- package/src/workspace/component-environments.test.ts +105 -0
- package/src/workspace/composites.schema.json +23 -4
- package/src/workspace/composites.test.ts +8 -8
- package/src/workspace/composites.ts +13 -2
- package/src/workspace/conformance/__fixture__/decisions/decision.schema.json +2 -2
- package/src/workspace/conformance/__fixtures__/reference-broker.ts +207 -0
- package/src/workspace/conformance/__writer_fixture__/answers/answer.kind.mjs +7 -2
- package/src/workspace/conformance/__writer_fixture__/answers/answer.schema.json +42 -2
- package/src/workspace/conformance/__writer_fixture__/contracts/contract.kind.mjs +37 -0
- package/src/workspace/conformance/__writer_fixture__/contracts/contract.schema.json +213 -0
- package/src/workspace/conformance/__writer_fixture__/decisions/points.json +96 -0
- package/src/workspace/conformance/__writer_fixture__/drivers/driver.kind.mjs +15 -0
- package/src/workspace/conformance/__writer_fixture__/drivers/driver.schema.json +38 -0
- package/src/workspace/conformance/__writer_fixture__/sessions/session.kind.mjs +14 -8
- package/src/workspace/conformance/__writer_fixture__/sessions/session.schema.json +187 -6
- package/src/workspace/conformance/__writer_fixture__/work/work.schema.json +25 -0
- package/src/workspace/conformance/broker.test.ts +122 -0
- package/src/workspace/conformance/broker.ts +792 -0
- package/src/workspace/conformance/conformance.e2e.test.ts +2 -1
- package/src/workspace/conformance/index.mjs +22 -1
- package/src/workspace/conformance/index.ts +5 -1
- package/src/workspace/conformance/vitest.ts +33 -1
- package/src/workspace/conformance/writer-conformance.e2e.test.ts +49 -3
- package/src/workspace/conformance/writer.test.ts +32 -2
- package/src/workspace/conformance/writer.ts +339 -28
- package/src/workspace/decide.test.ts +208 -4
- package/src/workspace/decide.ts +427 -73
- package/src/workspace/decision-points.schema.json +11 -4
- package/src/workspace/declaration.schema.json +153 -4
- package/src/workspace/declaration.ts +141 -0
- package/src/workspace/environments.ts +66 -14
- package/src/workspace/factory-rules.test.ts +62 -0
- package/src/workspace/factory-rules.ts +218 -0
- package/src/workspace/graph-cli.ts +2 -2
- package/src/workspace/graph.schema.json +67 -0
- package/src/workspace/identity.test.ts +23 -0
- package/src/workspace/init.ts +82 -2
- package/src/workspace/intent-cli.ts +26 -9
- package/src/workspace/intent-patch-id.test.ts +162 -0
- package/src/workspace/intent-record.schema.json +130 -3
- package/src/workspace/intent-record.ts +74 -28
- package/src/workspace/intent-reviews.test.ts +76 -0
- package/src/workspace/intent-squash.test.ts +213 -0
- package/src/workspace/intent-why.test.ts +5 -5
- package/src/workspace/intent.schema.json +234 -4
- package/src/workspace/intent.ts +260 -66
- package/src/workspace/json-edit.test.ts +121 -0
- package/src/workspace/json-edit.ts +142 -0
- package/src/workspace/jsonc.ts +20 -0
- package/src/workspace/kinds.ts +157 -2
- package/src/workspace/lexicon-symbols.test.ts +148 -0
- package/src/workspace/lexicon-symbols.ts +125 -0
- package/src/workspace/lineage-upgrade.test.ts +63 -0
- package/src/workspace/lineage-upgrade.ts +146 -5
- package/src/workspace/plan-summary.schema.json +155 -0
- package/src/workspace/point-answer.schema.json +42 -2
- package/src/workspace/points-cli.ts +42 -16
- package/src/workspace/points-write.schema.json +162 -4
- package/src/workspace/points.schema.json +157 -2
- package/src/workspace/points.test.ts +13 -1
- package/src/workspace/points.ts +118 -12
- package/src/workspace/pr-report.schema.json +95 -0
- package/src/workspace/profiles.test.ts +159 -0
- package/src/workspace/read-contract.test.ts +2 -2
- package/src/workspace/reason-codes.test.ts +13 -1
- package/src/workspace/reason-codes.ts +38 -4
- package/src/workspace/record-sessions.ts +9 -8
- package/src/workspace/records-amend.schema.json +78 -0
- package/src/workspace/records-cli.ts +20 -6
- package/src/workspace/records-close.schema.json +78 -0
- package/src/workspace/records-close.ts +31 -7
- package/src/workspace/records-concurrent.e2e.test.ts +135 -0
- package/src/workspace/records-concurrent.test.ts +145 -0
- package/src/workspace/records-formats.test.ts +46 -10
- package/src/workspace/records-new.schema.json +7 -0
- package/src/workspace/records-review.schema.json +78 -0
- package/src/workspace/records-sessions-write.test.ts +69 -0
- package/src/workspace/records-sessions.test.ts +22 -0
- package/src/workspace/records-write.ts +192 -25
- package/src/workspace/records.schema.json +16 -0
- package/src/workspace/records.ts +50 -9
- package/src/workspace/reference-kinds/app/package.json +10 -0
- package/src/workspace/reference-kinds/app/workspace-kinds.json +45 -0
- package/src/workspace/run-statement.schema.json +622 -0
- package/src/workspace/runs-cli.ts +343 -14
- package/src/workspace/runs-statement.test.ts +239 -0
- package/src/workspace/runs-write.schema.json +416 -5
- package/src/workspace/runs.schema.json +451 -4
- package/src/workspace/runs.test.ts +39 -0
- package/src/workspace/runs.ts +296 -35
- package/src/workspace/session-kinds.ts +3 -3
- package/src/workspace/squash.ts +198 -0
- package/src/workspace/status-contract.test.ts +5 -2
- package/src/workspace/status-gates.ts +3 -1
- package/src/workspace/status.schema.json +126 -3
- package/src/workspace/status.ts +90 -1
- package/src/workspace/symbols.ts +20 -9
- package/src/workspace/trust/run-commits.ts +115 -0
- package/src/workspace/trust/run-statement.ts +226 -0
- package/src/workspace/trust/seal.ts +8 -2
- package/src/workspace/trust/verify-cli.ts +6 -5
- package/src/workspace/trust/verify.ts +39 -6
- package/src/workspace/wip-cli.ts +97 -0
- package/src/workspace/wip-write.schema.json +504 -0
- package/src/workspace/wip.schema.json +334 -0
- package/src/workspace/wip.test.ts +294 -0
- package/src/workspace/wip.ts +753 -0
- package/src/workspace/work-cli.ts +3 -0
- package/src/workspace/work-evidence.schema.json +3 -0
- package/src/workspace/work-size.test.ts +77 -0
- package/src/workspace/work-size.ts +93 -0
- package/src/workspace/workspace-kinds.schema.json +122 -0
- package/src/workspace/write-lock-cli.ts +131 -0
- package/src/workspace/write-lock.schema.json +172 -0
- package/src/workspace/write-lock.test.ts +189 -0
- package/src/workspace/write-lock.ts +474 -0
- package/src/workspace/write-scope.test.ts +16 -1
- package/src/workspace/write-scope.ts +33 -11
package/src/workspace/decide.ts
CHANGED
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
* Answers to decision points as records (ws-058, #2739).
|
|
3
3
|
*
|
|
4
4
|
* {@link askPoint} asks a point's deciders for one set of inputs and records
|
|
5
|
-
* the answer in the answer kind's records directory,
|
|
6
|
-
* records people's answer to an open question
|
|
7
|
-
*
|
|
5
|
+
* the answer in the answer kind's records directory, {@link answerPoint}
|
|
6
|
+
* records people's answer to an open question, with an optional note, and
|
|
7
|
+
* {@link retractAnswer} takes an answer back (#3351). Each writes one Markdown
|
|
8
|
+
* record or none, never commits, and goes through the checks every records write goes
|
|
8
9
|
* through (`records-write.ts`): the record is read back with the kind's schema
|
|
9
10
|
* before it is written, and no other record may become invalid.
|
|
10
11
|
*
|
|
@@ -21,12 +22,28 @@
|
|
|
21
22
|
* escalated one is asked again, since a backend may answer now, and rewritten
|
|
22
23
|
* only when the chain no longer escalates.
|
|
23
24
|
*
|
|
25
|
+
* An ad-hoc point (#3403) has no candidates of its own: each ask gives the
|
|
26
|
+
* question's text and candidates, the record keeps them as `asked`, and they
|
|
27
|
+
* are part of the inputs hash, so a question asked with other options is
|
|
28
|
+
* another question. People's answer is checked against them.
|
|
29
|
+
*
|
|
30
|
+
* An answer never changes in place. People may retract it (ws-084): the
|
|
31
|
+
* question is escalated to them again, and the answer, who gave it, when, and
|
|
32
|
+
* its note move into the record's `retractions` with who retracted it, when
|
|
33
|
+
* and why. Asking a retracted question again returns it as it stands, since
|
|
34
|
+
* people took it back from the deciders; people answer it again.
|
|
35
|
+
*
|
|
36
|
+
* Each write holds the working tree's write lock (#3173, ws-089) from the first read
|
|
37
|
+
* to the write, so two answers to one question are never both taken from the
|
|
38
|
+
* same reading of it.
|
|
39
|
+
*
|
|
24
40
|
* chant never calls a model (ws-052): the model call is the caller's
|
|
25
41
|
* {@link ModelAsk}, such as the decide Op activity (#2740), or a backend's
|
|
26
42
|
* response the caller already has (`points ask --response`).
|
|
27
43
|
*/
|
|
28
44
|
|
|
29
45
|
import { writeFileSync } from "node:fs";
|
|
46
|
+
import { noteWrites, withWriteLock, WriteLockError, WRITE_LOCK_CODES, type WriteLockWho } from "./write-lock";
|
|
30
47
|
import type { ReasonCode } from "./reason-codes";
|
|
31
48
|
import { gitRoot } from "./record-source";
|
|
32
49
|
import { loadRecordKind, normalisePrincipal, RECORD_REASON_CODES, RecordReadError, type RecordEntry, type RecordWarning } from "./records";
|
|
@@ -47,15 +64,19 @@ import {
|
|
|
47
64
|
} from "./records-write";
|
|
48
65
|
import {
|
|
49
66
|
answerId,
|
|
67
|
+
askedOf,
|
|
68
|
+
askedQuestion,
|
|
50
69
|
candidates,
|
|
51
70
|
DeciderFailed,
|
|
52
71
|
inputsHash,
|
|
72
|
+
parseAsked,
|
|
53
73
|
pointOf,
|
|
54
74
|
pointsFileOf,
|
|
55
75
|
pointVersion,
|
|
56
76
|
quorumOf,
|
|
57
77
|
readPointsThrough,
|
|
58
78
|
runChain,
|
|
79
|
+
type Asked,
|
|
59
80
|
type ChainResult,
|
|
60
81
|
type Escalation,
|
|
61
82
|
type ModelAsk,
|
|
@@ -73,11 +94,11 @@ import { scopeSource } from "./write-scope";
|
|
|
73
94
|
import { RefCASConflictError } from "../lifecycle/git";
|
|
74
95
|
import { readLedgerAnswers, refreshLedger, withLedgerAnswers, writeLedgerAnswer, type LedgerAnswers } from "./answers-ledger";
|
|
75
96
|
|
|
76
|
-
/** The version of the write documents `points ask` and `points
|
|
97
|
+
/** The version of the write documents `points ask`, `points answer` and `points retract` print. */
|
|
77
98
|
export const POINTS_WRITE_CONTRACT_VERSION = 1;
|
|
78
99
|
export const POINTS_WRITE_SCHEMA_ID = "https://intentius.io/chant/schemas/workspace/points-write/v1/points-write.schema.json";
|
|
79
100
|
|
|
80
|
-
/** Why `points ask` or `points
|
|
101
|
+
/** Why `points ask`, `points answer` or `points retract` wrote nothing. Closed: a reader may switch on it. */
|
|
81
102
|
export const POINTS_WRITE_ERROR_CODES = [
|
|
82
103
|
...LOAD_ERROR_CODES,
|
|
83
104
|
"write-usage-invalid",
|
|
@@ -103,6 +124,13 @@ export const POINTS_WRITE_ERROR_CODES = [
|
|
|
103
124
|
"answer-in-steward-turn",
|
|
104
125
|
/** An answerer is named by a bare name, and identity.attribution at base is identified (#3163). */
|
|
105
126
|
"principal-unidentified",
|
|
127
|
+
/** `points retract` names a question that has no answer: escalated or proposed (#3351). */
|
|
128
|
+
"answer-not-answered",
|
|
129
|
+
/** The answer kind's schema copy has no `note`, `retractions`, `relayed_by` or `asked` field for what the write was given (#3351, #3402, #3403). */
|
|
130
|
+
"answer-field-unsupported",
|
|
131
|
+
/** An ad-hoc point was asked without its question and candidates, a declared point with them, or they don't fit the point's question type (#3403). */
|
|
132
|
+
"point-candidates-invalid",
|
|
133
|
+
...WRITE_LOCK_CODES,
|
|
106
134
|
...RECORD_REASON_CODES,
|
|
107
135
|
] as const satisfies readonly ReasonCode[];
|
|
108
136
|
export type PointsWriteErrorCode = (typeof POINTS_WRITE_ERROR_CODES)[number];
|
|
@@ -141,16 +169,34 @@ export interface QuestionView {
|
|
|
141
169
|
probabilities: Record<string, number> | null;
|
|
142
170
|
confidence: number | null;
|
|
143
171
|
threshold: number | null;
|
|
172
|
+
/**
|
|
173
|
+
* The model's explanation of the recorded answer (#3345): a proposal's, or a
|
|
174
|
+
* confirmed proposal's. null when a model gave none, or did not decide.
|
|
175
|
+
*/
|
|
176
|
+
reason: string | null;
|
|
144
177
|
/**
|
|
145
178
|
* Any model answer: the proposal for a proposed question, or for an
|
|
146
179
|
* escalated one the last model answer below its threshold. null when no
|
|
147
|
-
* model answered.
|
|
180
|
+
* model answered. `reason` is the model's explanation of that answer, or
|
|
181
|
+
* null when it gave none (#3345).
|
|
148
182
|
*/
|
|
149
|
-
model: { answer: string | boolean | null; confidence: number | null; threshold: number | null; model: string | null; backend: string | null; observed: boolean } | null;
|
|
183
|
+
model: { answer: string | boolean | null; confidence: number | null; threshold: number | null; model: string | null; backend: string | null; observed: boolean; reason: string | null } | null;
|
|
150
184
|
escalations: Escalation[];
|
|
151
185
|
answeredBy: string[];
|
|
152
186
|
askedOn: string | null;
|
|
153
187
|
answeredOn: string | null;
|
|
188
|
+
/** What the people who answered wrote with their answer (`points answer --note`, #3351), or null. */
|
|
189
|
+
note: string | null;
|
|
190
|
+
/** Who relayed the answer to chant for the people who gave it (`points answer --relayed-by`, #3402), or null. */
|
|
191
|
+
relayedBy: string | null;
|
|
192
|
+
/** Answers people took back, oldest first (`points retract`, #3351). Empty when none was. */
|
|
193
|
+
retractions: RetractionView[];
|
|
194
|
+
/**
|
|
195
|
+
* An ad-hoc point's question as its ask gave it (#3403): the question's text
|
|
196
|
+
* and its criteria, what each candidate means. null for a declared point's
|
|
197
|
+
* question, whose text and criteria are the point's.
|
|
198
|
+
*/
|
|
199
|
+
asked: Asked | null;
|
|
154
200
|
/**
|
|
155
201
|
* The steward whose turn asked the question (#2749), and the run it was in,
|
|
156
202
|
* or null when no steward asked it. The steward waits on the question and
|
|
@@ -169,6 +215,43 @@ export interface QuestionView {
|
|
|
169
215
|
warnings: RecordWarning[];
|
|
170
216
|
}
|
|
171
217
|
|
|
218
|
+
/** One retracted answer, as a {@link QuestionView} lists it: the answer as it was, and who took it back, when and why. */
|
|
219
|
+
export interface RetractionView {
|
|
220
|
+
answer: string | boolean | null;
|
|
221
|
+
decider: Record<string, unknown>;
|
|
222
|
+
answeredBy: string[];
|
|
223
|
+
answeredOn: string | null;
|
|
224
|
+
/** The note the answer carried, or null. */
|
|
225
|
+
answerNote: string | null;
|
|
226
|
+
/** Who relayed the answer (#3402), or null. */
|
|
227
|
+
answerRelayedBy: string | null;
|
|
228
|
+
/** Who retracted it. */
|
|
229
|
+
by: string[];
|
|
230
|
+
on: string | null;
|
|
231
|
+
/** Why it was retracted (`points retract --note`), or null. */
|
|
232
|
+
note: string | null;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** The record's `retractions`, as views. */
|
|
236
|
+
function retractionsOf(d: Record<string, unknown>): RetractionView[] {
|
|
237
|
+
if (!Array.isArray(d.retractions)) return [];
|
|
238
|
+
const str = (v: unknown): string | null => (typeof v === "string" ? v : null);
|
|
239
|
+
const strs = (v: unknown): string[] => (Array.isArray(v) ? v.filter((x): x is string => typeof x === "string") : []);
|
|
240
|
+
return d.retractions
|
|
241
|
+
.filter((r): r is Record<string, unknown> => r !== null && typeof r === "object" && !Array.isArray(r))
|
|
242
|
+
.map((r) => ({
|
|
243
|
+
answer: typeof r.answer === "string" || typeof r.answer === "boolean" ? r.answer : null,
|
|
244
|
+
decider: r.decider !== null && typeof r.decider === "object" && !Array.isArray(r.decider) ? (r.decider as Record<string, unknown>) : {},
|
|
245
|
+
answeredBy: strs(r.answered_by),
|
|
246
|
+
answeredOn: str(r.answered_on),
|
|
247
|
+
answerNote: str(r.answer_note),
|
|
248
|
+
answerRelayedBy: str(r.answer_relayed_by),
|
|
249
|
+
by: strs(r.by),
|
|
250
|
+
on: str(r.on),
|
|
251
|
+
note: str(r.note),
|
|
252
|
+
}));
|
|
253
|
+
}
|
|
254
|
+
|
|
172
255
|
/** The harness a steward's question names in its source block (#2749). */
|
|
173
256
|
export const STEWARD_HARNESS = "chant-steward";
|
|
174
257
|
|
|
@@ -202,10 +285,20 @@ export function questionView(entry: RecordEntry, point: Point | undefined, ledge
|
|
|
202
285
|
const answer = typeof d.answer === "string" || typeof d.answer === "boolean" ? d.answer : null;
|
|
203
286
|
let model: QuestionView["model"] = null;
|
|
204
287
|
if (decider.kind === "model") {
|
|
205
|
-
model = { answer, confidence: num(d.confidence), threshold: num(d.threshold), model: str(decider.model), backend: str(decider.backend), observed: true };
|
|
288
|
+
model = { answer, confidence: num(d.confidence), threshold: num(d.threshold), model: str(decider.model), backend: str(decider.backend), observed: true, reason: str(d.reason) };
|
|
206
289
|
} else {
|
|
207
290
|
const lean = [...escalations].reverse().find((e) => e.kind === "model" && e.answer !== undefined && e.answer !== null);
|
|
208
|
-
if (lean)
|
|
291
|
+
if (lean) {
|
|
292
|
+
model = {
|
|
293
|
+
answer: lean.answer ?? null,
|
|
294
|
+
confidence: lean.confidence ?? null,
|
|
295
|
+
threshold: lean.threshold ?? null,
|
|
296
|
+
model: lean.model ?? null,
|
|
297
|
+
backend: lean.backend ?? null,
|
|
298
|
+
observed: false,
|
|
299
|
+
reason: typeof lean.model_reason === "string" ? lean.model_reason : null,
|
|
300
|
+
};
|
|
301
|
+
}
|
|
209
302
|
}
|
|
210
303
|
const constrains = Array.isArray(d.constrains) ? d.constrains.filter((c): c is string => typeof c === "string") : [];
|
|
211
304
|
return {
|
|
@@ -227,11 +320,16 @@ export function questionView(entry: RecordEntry, point: Point | undefined, ledge
|
|
|
227
320
|
probabilities: d.probabilities !== null && typeof d.probabilities === "object" ? (d.probabilities as Record<string, number>) : null,
|
|
228
321
|
confidence: num(d.confidence),
|
|
229
322
|
threshold: num(d.threshold),
|
|
323
|
+
reason: decider.kind === "model" ? str(d.reason) : null,
|
|
230
324
|
model,
|
|
231
325
|
escalations,
|
|
232
326
|
answeredBy: Array.isArray(d.answered_by) ? d.answered_by.filter((b): b is string => typeof b === "string") : [],
|
|
233
327
|
askedOn: str(d.asked_on),
|
|
234
328
|
answeredOn: str(d.answered_on),
|
|
329
|
+
note: str(d.note),
|
|
330
|
+
relayedBy: str(d.relayed_by),
|
|
331
|
+
retractions: retractionsOf(d),
|
|
332
|
+
asked: askedOf(d) ?? null,
|
|
235
333
|
askedBy: askedByOf(d),
|
|
236
334
|
ledger,
|
|
237
335
|
valid: entry.valid,
|
|
@@ -239,12 +337,15 @@ export function questionView(entry: RecordEntry, point: Point | undefined, ledge
|
|
|
239
337
|
};
|
|
240
338
|
}
|
|
241
339
|
|
|
242
|
-
/**
|
|
340
|
+
/** The write verbs of `points`. */
|
|
341
|
+
export type PointsWriteVerb = "ask" | "answer" | "retract";
|
|
342
|
+
|
|
343
|
+
/** What `points ask`, `points answer` and `points retract` print. */
|
|
243
344
|
export type PointsWriteDocument =
|
|
244
345
|
| {
|
|
245
346
|
$schema: string;
|
|
246
347
|
contract: number;
|
|
247
|
-
verb:
|
|
348
|
+
verb: PointsWriteVerb;
|
|
248
349
|
kind: KindView;
|
|
249
350
|
id: string;
|
|
250
351
|
path: string;
|
|
@@ -257,10 +358,10 @@ export type PointsWriteDocument =
|
|
|
257
358
|
/** With --dry-run, the text the command would write. */
|
|
258
359
|
text?: string;
|
|
259
360
|
}
|
|
260
|
-
| { $schema: string; contract: number; verb:
|
|
361
|
+
| { $schema: string; contract: number; verb: PointsWriteVerb; error: { code: PointsWriteErrorCode; message: string } };
|
|
261
362
|
|
|
262
|
-
function failure(verb:
|
|
263
|
-
if (err instanceof PointsWriteError || err instanceof RecordWriteError || err instanceof RecordReadError || err instanceof IdentityError) {
|
|
363
|
+
function failure(verb: PointsWriteVerb, err: unknown): PointsWriteDocument {
|
|
364
|
+
if (err instanceof PointsWriteError || err instanceof RecordWriteError || err instanceof RecordReadError || err instanceof IdentityError || err instanceof WriteLockError) {
|
|
264
365
|
return { $schema: POINTS_WRITE_SCHEMA_ID, contract: POINTS_WRITE_CONTRACT_VERSION, verb, error: { code: err.code as PointsWriteErrorCode, message: err.message } };
|
|
265
366
|
}
|
|
266
367
|
if (err instanceof WorkspaceReadError) {
|
|
@@ -313,6 +414,12 @@ export interface AskPointOptions {
|
|
|
313
414
|
point: string;
|
|
314
415
|
/** The inputs, keyed by the point's declared input names. */
|
|
315
416
|
inputs: unknown;
|
|
417
|
+
/**
|
|
418
|
+
* An ad-hoc point's question and candidates (#3403), as `{ question,
|
|
419
|
+
* criteria }` in the point's question type's shape. Required for an ad-hoc
|
|
420
|
+
* point and refused for a declared one.
|
|
421
|
+
*/
|
|
422
|
+
candidates?: unknown;
|
|
316
423
|
/** What the question is about, such as a work item's id: written to `constrains`. */
|
|
317
424
|
subject?: string;
|
|
318
425
|
/** The answer kind file, or a declared kind's name. Without it, the declared answer kind whose points file declares the point. */
|
|
@@ -346,14 +453,15 @@ export interface AskPointOptions {
|
|
|
346
453
|
const today = (): string => new Date().toISOString().slice(0, 10);
|
|
347
454
|
const show = (answer: string | boolean, type: string): string => (type === "noul" ? (answer ? "yes" : "no") : String(answer));
|
|
348
455
|
|
|
349
|
-
|
|
456
|
+
/** A record's title: the point's title, or an ad-hoc question's own text (#3403), what it is about, and where it stands. */
|
|
457
|
+
function titleFor(point: Point, subject: string | undefined, state: string, answer: string | boolean | undefined, asked?: Asked): string {
|
|
350
458
|
const outcome = state === "escalated" ? "open for people" : state === "proposed" ? `${show(answer!, point.question.type)}, proposed` : show(answer!, point.question.type);
|
|
351
|
-
return `${point.title}${subject ? ` (${subject})` : ""}: ${outcome}`;
|
|
459
|
+
return `${asked ? asked.question : point.title}${subject ? ` (${subject})` : ""}: ${outcome}`;
|
|
352
460
|
}
|
|
353
461
|
|
|
354
|
-
function body(point: Point, title: string): string {
|
|
355
|
-
const q = point
|
|
356
|
-
const describe = (c: string | boolean): string => (q.type === "score" ? "" : ((q.criteria as Record<string, string>)[String(c)] ?? ""));
|
|
462
|
+
function body(point: Point, title: string, asked?: Asked): string {
|
|
463
|
+
const q = askedQuestion(point, asked);
|
|
464
|
+
const describe = (c: string | boolean): string => (q.type === "score" ? "" : ((q.criteria as Record<string, string> | undefined)?.[String(c)] ?? ""));
|
|
357
465
|
const options = candidates(q).map((c) => `- ${show(c, q.type)}${describe(c) ? `: ${describe(c)}` : ""}`);
|
|
358
466
|
return ["", `# ${title}`, "", q.instructions.trim(), "", ...options, ""].join("\n");
|
|
359
467
|
}
|
|
@@ -371,6 +479,26 @@ async function findPoint(kinds: string[], name: string, cwd: string): Promise<Op
|
|
|
371
479
|
throw new PointsWriteError("point-unknown", `no points file declares ${JSON.stringify(name)}${declared.length ? ` (the points are ${declared.join(", ")})` : ""}`);
|
|
372
480
|
}
|
|
373
481
|
|
|
482
|
+
/**
|
|
483
|
+
* An ad-hoc point's question and candidates from the ask (#3403), or
|
|
484
|
+
* undefined for a declared point. Refused when an ad-hoc point has none, a
|
|
485
|
+
* declared point is given them, they don't fit the question type, or the
|
|
486
|
+
* answer kind's schema copy has no `asked` field to keep them in.
|
|
487
|
+
*/
|
|
488
|
+
function checkAsked(name: string, point: Point, given: unknown, loaded: { kind: { name: string }; schema: Record<string, unknown> }): Asked | undefined {
|
|
489
|
+
if (!point.adhoc) {
|
|
490
|
+
if (given !== undefined) throw new PointsWriteError("point-candidates-invalid", `${name} declares its candidates, so the ask gives none: --candidates is for an ad-hoc point`);
|
|
491
|
+
return undefined;
|
|
492
|
+
}
|
|
493
|
+
if (given === undefined) throw new PointsWriteError("point-candidates-invalid", `${name} is an ad-hoc point, so each ask gives the question and its candidates: --candidates <file|-|json>`);
|
|
494
|
+
const parsed = parseAsked(point, given);
|
|
495
|
+
if ("problems" in parsed) throw new PointsWriteError("point-candidates-invalid", `the candidates for ${name}, a ${point.question.type} question: ${parsed.problems.join("; ")}`);
|
|
496
|
+
if (!answerFields(loaded.schema).asked) {
|
|
497
|
+
throw new PointsWriteError("answer-field-unsupported", `the ${loaded.kind.name} kind's schema has no asked field, so an ad-hoc question's text and candidates can't be kept: copy point-answer.schema.json from @intentius/chant anew`);
|
|
498
|
+
}
|
|
499
|
+
return parsed.asked;
|
|
500
|
+
}
|
|
501
|
+
|
|
374
502
|
function checkInputs(name: string, point: Point, inputs: unknown): Record<string, unknown> {
|
|
375
503
|
if (inputs === null || typeof inputs !== "object" || Array.isArray(inputs)) throw new PointsWriteError("point-inputs-invalid", `the inputs to ${name} must be a JSON object`);
|
|
376
504
|
const declared = Object.keys(point.inputs);
|
|
@@ -389,6 +517,8 @@ interface WriteTarget {
|
|
|
389
517
|
/** Write to the ledger rather than the tree. */
|
|
390
518
|
onLedger: boolean;
|
|
391
519
|
message: string;
|
|
520
|
+
/** Who writes, for the write journal (#3173). */
|
|
521
|
+
who: WriteLockWho;
|
|
392
522
|
}
|
|
393
523
|
|
|
394
524
|
/**
|
|
@@ -412,6 +542,7 @@ async function write(o: Opened, before: RecordEntry[], path: string, text: strin
|
|
|
412
542
|
if (!dryRun) {
|
|
413
543
|
try {
|
|
414
544
|
writeFileSync(abs(o, path), text, create ? { flag: "wx" } : undefined);
|
|
545
|
+
noteWrites(o.root, [{ path, text }], target.who);
|
|
415
546
|
} catch (err) {
|
|
416
547
|
if ((err as NodeJS.ErrnoException).code === "EEXIST") throw new PointsWriteError("record-id-taken", `${path} was written by another ask at the same time: ask again to read it`);
|
|
417
548
|
throw err;
|
|
@@ -433,10 +564,28 @@ function recordData(o: Opened, fields: Record<string, unknown>): Record<string,
|
|
|
433
564
|
return pick(clean, schemaOrder(clean, { properties: o.loaded.schema.properties }));
|
|
434
565
|
}
|
|
435
566
|
|
|
436
|
-
|
|
437
|
-
|
|
567
|
+
/**
|
|
568
|
+
* Whether an answer kind's schema copy takes the model's reason (#3345): at
|
|
569
|
+
* the top level, and in an escalation entry as `model_reason`. A workspace
|
|
570
|
+
* whose copy of point-answer.schema.json predates them keeps no reason rather
|
|
571
|
+
* than failing the write; copying the schema anew turns them on.
|
|
572
|
+
*/
|
|
573
|
+
export function reasonFields(schema: Record<string, unknown>): { top: boolean; escalation: boolean } {
|
|
574
|
+
const has = (o: unknown, key: string): boolean => o !== null && typeof o === "object" && Object.prototype.hasOwnProperty.call(o, key);
|
|
575
|
+
const defs = (schema.definitions ?? schema.$defs) as Record<string, { properties?: unknown }> | undefined;
|
|
576
|
+
return { top: has(schema.properties, "reason"), escalation: has(defs?.escalation?.properties, "model_reason") };
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
/** Escalations without `model_reason` when the kind's schema does not take it. */
|
|
580
|
+
function keepReasons(escalations: Escalation[], takes: { escalation: boolean }): Escalation[] {
|
|
581
|
+
return takes.escalation ? escalations : escalations.map(({ model_reason: _, ...e }) => e);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
function resultFields(result: ChainResult, takes: { top: boolean; escalation: boolean }): Record<string, unknown> {
|
|
585
|
+
const escalations = result.escalations.length > 0 ? keepReasons(result.escalations, takes) : undefined;
|
|
438
586
|
if (result.status === "proposed") {
|
|
439
|
-
|
|
587
|
+
const reason = takes.top ? result.reason : undefined;
|
|
588
|
+
return { state: "proposed", answer: result.answer, decider: result.decider, probabilities: result.probabilities, confidence: result.confidence, threshold: result.threshold, reason, escalations };
|
|
440
589
|
}
|
|
441
590
|
if (result.status === "answered") return { state: "answered", answer: result.answer, decider: result.decider, escalations };
|
|
442
591
|
return { state: "escalated", decider: result.decider, escalations };
|
|
@@ -448,6 +597,14 @@ function resultFields(result: ChainResult): Record<string, unknown> {
|
|
|
448
597
|
* answered or proposed before.
|
|
449
598
|
*/
|
|
450
599
|
export async function askPoint(opts: AskPointOptions): Promise<PointsWriteDocument> {
|
|
600
|
+
try {
|
|
601
|
+
return await withWriteLock(opts.cwd, { verb: "points ask", by: opts.steward?.name ?? null }, opts.dryRun, () => askPointLocked(opts));
|
|
602
|
+
} catch (err) {
|
|
603
|
+
return failure("ask", err);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
async function askPointLocked(opts: AskPointOptions): Promise<PointsWriteDocument> {
|
|
451
608
|
try {
|
|
452
609
|
// A steward's turn (#2749): named by the caller, or this process's, as when an Op shells out to `points ask`.
|
|
453
610
|
const turn = opts.steward ? undefined : currentStewardTurn();
|
|
@@ -456,8 +613,9 @@ export async function askPoint(opts: AskPointOptions): Promise<PointsWriteDocume
|
|
|
456
613
|
const kinds = await answerKindFiles(opts.cwd, opts.kind);
|
|
457
614
|
const { o, point } = await findPoint(kinds, opts.point, opts.cwd);
|
|
458
615
|
const inputs = checkInputs(opts.point, point, opts.inputs);
|
|
616
|
+
const asked = checkAsked(opts.point, point, opts.candidates, o.loaded);
|
|
459
617
|
const version = pointVersion(point);
|
|
460
|
-
const hash = inputsHash(opts.point, version, inputs);
|
|
618
|
+
const hash = inputsHash(opts.point, version, inputs, asked);
|
|
461
619
|
const id = answerId(opts.point, hash);
|
|
462
620
|
if (toLedger && !opts.dryRun) await refreshLedger(o.loaded.file);
|
|
463
621
|
const { ledger, source: read, before } = await readWithLedger(o);
|
|
@@ -477,10 +635,12 @@ export async function askPoint(opts: AskPointOptions): Promise<PointsWriteDocume
|
|
|
477
635
|
...(extra.text !== undefined ? { text: extra.text } : {}),
|
|
478
636
|
});
|
|
479
637
|
if (existing && existing.data !== null && (existing.state === "answered" || existing.state === "proposed")) return done(existing, { reused: true, written: false });
|
|
638
|
+
// People took a retracted question back from the deciders (ws-084): they answer it again, and asking returns it as it stands.
|
|
639
|
+
if (existing && existing.data !== null && Array.isArray(existing.data.retractions) && existing.data.retractions.length > 0) return done(existing, { reused: true, written: false });
|
|
480
640
|
|
|
481
641
|
let result: ChainResult;
|
|
482
642
|
try {
|
|
483
|
-
result = await runChain(opts.point, point, inputs, opts.ask);
|
|
643
|
+
result = await runChain(opts.point, { ...point, question: askedQuestion(point, asked) }, inputs, opts.ask);
|
|
484
644
|
} catch (err) {
|
|
485
645
|
if (err instanceof DeciderFailed) throw new PointsWriteError("point-decider-failed", err.message);
|
|
486
646
|
throw err;
|
|
@@ -491,7 +651,7 @@ export async function askPoint(opts: AskPointOptions): Promise<PointsWriteDocume
|
|
|
491
651
|
const on = opts.on ?? today();
|
|
492
652
|
const state = result.status;
|
|
493
653
|
const answer = result.status === "escalated" ? undefined : result.answer;
|
|
494
|
-
const title = titleFor(point, opts.subject, state, answer);
|
|
654
|
+
const title = titleFor(point, opts.subject, state, answer, asked);
|
|
495
655
|
const modelId = result.status === "proposed" ? result.decider.model : undefined;
|
|
496
656
|
const client = steward ? { name: steward.name } : opts.client;
|
|
497
657
|
const source = {
|
|
@@ -507,21 +667,22 @@ export async function askPoint(opts: AskPointOptions): Promise<PointsWriteDocume
|
|
|
507
667
|
point: opts.point,
|
|
508
668
|
point_version: version,
|
|
509
669
|
question_type: point.question.type,
|
|
510
|
-
candidates: candidates(point
|
|
670
|
+
candidates: candidates(askedQuestion(point, asked)),
|
|
671
|
+
asked,
|
|
511
672
|
inputs,
|
|
512
673
|
inputs_hash: hash,
|
|
513
674
|
constrains: opts.subject !== undefined ? [opts.subject] : existing?.data?.constrains,
|
|
514
|
-
...resultFields(result),
|
|
675
|
+
...resultFields(result, reasonFields(o.loaded.schema)),
|
|
515
676
|
asked_on: on,
|
|
516
677
|
answered_on: state === "answered" ? on : undefined,
|
|
517
678
|
source,
|
|
518
679
|
});
|
|
519
680
|
const path = existing ? existing.path : o.dirRel === "." ? `${id}.md` : `${o.dirRel}/${id}.md`;
|
|
520
|
-
const text = renderRecord(data, body(point, title));
|
|
681
|
+
const text = renderRecord(data, body(point, title, asked));
|
|
521
682
|
// A steward's question goes on the ledger, and one already held there stays there (#2786).
|
|
522
683
|
const onLedger = toLedger || ledger.byPath.has(path);
|
|
523
684
|
const message = `Decision point ${opts.point}: ${state}${opts.subject ? ` (${opts.subject})` : ""}${steward ? `, asked by ${steward.name}` : ""}`;
|
|
524
|
-
const wrote = await write(o, before, path, text, !existing, !!opts.dryRun, { source: read, ledger, onLedger, message });
|
|
685
|
+
const wrote = await write(o, before, path, text, !existing, !!opts.dryRun, { source: read, ledger, onLedger, message, who: { verb: "points ask", by: steward?.name ?? null } });
|
|
525
686
|
const entry: RecordEntry = { ...(existing ?? emptyEntry(path)), id, path, state, data, valid: true, reasons: [], warnings: wrote.warnings };
|
|
526
687
|
return done(entry, { reused: false, written: !opts.dryRun, ledger: wrote.ledger, ...(opts.dryRun ? { text } : {}) });
|
|
527
688
|
} catch (err) {
|
|
@@ -543,11 +704,38 @@ export interface AnswerPointOptions {
|
|
|
543
704
|
answer: string | boolean;
|
|
544
705
|
/** Who answered, as the caller names them. chant does not check who they are; the trust policy says who counts. */
|
|
545
706
|
by: string[];
|
|
707
|
+
/** What the people who answered say with it, recorded as the record's `note` (#3351). */
|
|
708
|
+
note?: string;
|
|
709
|
+
/**
|
|
710
|
+
* Who carried the answer to chant for the people in `by` (#3402), such as a
|
|
711
|
+
* follower relaying a person's answer through hud, recorded as the record's
|
|
712
|
+
* `relayed_by`. The relay does not count toward the quorum.
|
|
713
|
+
*/
|
|
714
|
+
relayedBy?: string;
|
|
546
715
|
kind?: string;
|
|
547
716
|
on?: string;
|
|
548
717
|
dryRun?: boolean;
|
|
549
718
|
}
|
|
550
719
|
|
|
720
|
+
/**
|
|
721
|
+
* Whether an answer kind's schema copy takes the fields of #3351 (`note` on an
|
|
722
|
+
* answer, and `retractions`), of #3402 (`relayed_by`) and of #3403 (`asked`).
|
|
723
|
+
* A copy of point-answer.schema.json from before them refuses a note, a
|
|
724
|
+
* retraction, a relay or an ad-hoc ask with `answer-field-unsupported`, rather
|
|
725
|
+
* than dropping what a person wrote or the question an agent asked.
|
|
726
|
+
*/
|
|
727
|
+
export function answerFields(schema: Record<string, unknown>): { note: boolean; retractions: boolean; relayedBy: boolean; asked: boolean } {
|
|
728
|
+
const props = schema.properties as Record<string, unknown> | undefined;
|
|
729
|
+
const has = (key: string): boolean => props !== undefined && props !== null && typeof props === "object" && Object.prototype.hasOwnProperty.call(props, key);
|
|
730
|
+
return { note: has("note"), retractions: has("retractions"), relayedBy: has("relayed_by"), asked: has("asked") };
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
/** A note as given, trimmed, or undefined when it is empty. */
|
|
734
|
+
const noteOf = (note: string | undefined): string | undefined => {
|
|
735
|
+
const t = note?.trim();
|
|
736
|
+
return t ? t : undefined;
|
|
737
|
+
};
|
|
738
|
+
|
|
551
739
|
/** The trust policy at base, for the quorum: who holds the agent role and each role a quorum names. */
|
|
552
740
|
function policyFor(root: string): TrustPolicy {
|
|
553
741
|
if (!gitRoot(root)) return emptyPolicy(null);
|
|
@@ -597,73 +785,114 @@ export function tallyQuorum(
|
|
|
597
785
|
return { counted, left, met: counted.length >= quorum.count };
|
|
598
786
|
}
|
|
599
787
|
|
|
788
|
+
/**
|
|
789
|
+
* A steward waits on a question and never answers or retracts one (#2749), as
|
|
790
|
+
* it never clears a gate: the write has to come from a person, through hud or
|
|
791
|
+
* a shell, not from the steward's own turn or a process it started.
|
|
792
|
+
*/
|
|
793
|
+
function refuseStewardTurn(id: string, does: "answers" | "retracts"): void {
|
|
794
|
+
const turn = currentStewardTurn();
|
|
795
|
+
if (turn) {
|
|
796
|
+
throw new PointsWriteError(
|
|
797
|
+
"answer-in-steward-turn",
|
|
798
|
+
`this is the steward ${turn.steward}'s turn, and a steward never ${does} a decision point: a person ${does === "answers" ? "answers" : "retracts the answer to"} ${id} through hud or \`chant workspace points ${does === "answers" ? "answer" : "retract"}\` at a shell`,
|
|
799
|
+
);
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
interface FoundQuestion {
|
|
804
|
+
opened: OpenedPoints;
|
|
805
|
+
before: RecordEntry[];
|
|
806
|
+
read: Awaited<ReturnType<typeof readWithLedger>>;
|
|
807
|
+
target: RecordEntry & { data: Record<string, unknown> };
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
/** The answer record with this id, in the answer kind `kind` names or the first declared one that has it, read with the ledger's answers laid over the tree. */
|
|
811
|
+
async function findQuestion(cwd: string, kind: string | undefined, id: string): Promise<FoundQuestion> {
|
|
812
|
+
const kinds = await answerKindFiles(cwd, kind);
|
|
813
|
+
if (kinds.length === 0) throw new PointsWriteError("points-undeclared", "no record kind with an answers block is declared: name one with --kind, or declare one in chant.workspace.json");
|
|
814
|
+
for (const k of kinds) {
|
|
815
|
+
const opened = await openAnswers(k, cwd);
|
|
816
|
+
// A question a steward asked is on the ledger (#2786), and people's answer goes back there.
|
|
817
|
+
const read = await readWithLedger(opened.o);
|
|
818
|
+
const target = read.before.find((e) => e.id === id);
|
|
819
|
+
if (target && target.data !== null) return { opened, before: read.before, read, target: target as FoundQuestion["target"] };
|
|
820
|
+
}
|
|
821
|
+
throw new PointsWriteError("record-not-found", `no answer record has id ${id}`);
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
/** Count `by` toward the point's quorum, or refuse with `quorum-not-met`. */
|
|
825
|
+
function meetQuorum(name: string, by: string[], quorum: { count: number; roles?: string[] }, root: string, steward: string | undefined, verb: "answer" | "retract"): { counted: string[] } {
|
|
826
|
+
const tally = tallyQuorum(by, quorum, policyFor(root), steward);
|
|
827
|
+
if (!tally.met) {
|
|
828
|
+
const got = tally.counted.length;
|
|
829
|
+
const left = tally.left.length ? `; not counted: ${tally.left.map((l) => `${l.name}, who ${l.why}`).join("; ")}` : "";
|
|
830
|
+
throw new PointsWriteError(
|
|
831
|
+
"quorum-not-met",
|
|
832
|
+
`${name} needs ${quorum.count} ${quorum.count === 1 ? "person" : "people"}${quorum.roles ? ` holding ${quorum.roles.join(" or ")}` : ""} to ${verb === "answer" ? "answer" : "retract an answer"}, and ${got} ${got === 1 ? "counts" : "count"}${got ? ` (${tally.counted.join(", ")})` : ""}${left}`,
|
|
833
|
+
);
|
|
834
|
+
}
|
|
835
|
+
return tally;
|
|
836
|
+
}
|
|
837
|
+
|
|
600
838
|
/**
|
|
601
839
|
* People answer an open question: `points answer`. A proposed question the
|
|
602
840
|
* people answer as the model did is confirmed, and keeps the model as its
|
|
603
841
|
* decider; any other answer is the quorum's, and a model's proposal moves into
|
|
604
|
-
* the escalations. The question becomes `answered
|
|
842
|
+
* the escalations. The question becomes `answered`, with the people's note
|
|
843
|
+
* when they give one (#3351).
|
|
605
844
|
*/
|
|
606
845
|
export async function answerPoint(opts: AnswerPointOptions): Promise<PointsWriteDocument> {
|
|
607
846
|
try {
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
}
|
|
847
|
+
return await withWriteLock(opts.cwd, { verb: "points answer", by: (opts.by ?? []).join(", ") || null }, opts.dryRun, () => answerPointLocked(opts));
|
|
848
|
+
} catch (err) {
|
|
849
|
+
return failure("answer", err);
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
async function answerPointLocked(opts: AnswerPointOptions): Promise<PointsWriteDocument> {
|
|
854
|
+
try {
|
|
855
|
+
refuseStewardTurn(opts.id, "answers");
|
|
618
856
|
// #3163: under identity.attribution "identified" at base, each answerer is a forge identity or a signer.
|
|
619
857
|
refuseUnidentified(scopeSource(opts.cwd), opts.by, "--by");
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
| undefined;
|
|
625
|
-
for (const k of kinds) {
|
|
626
|
-
const opened = await openAnswers(k, opts.cwd);
|
|
627
|
-
// A question a steward asked is on the ledger (#2786), and people's answer goes back there.
|
|
628
|
-
const read = await readWithLedger(opened.o);
|
|
629
|
-
const target = read.before.find((e) => e.id === opts.id);
|
|
630
|
-
if (target && target.data !== null) {
|
|
631
|
-
found = { opened, before: read.before, read, target: target as RecordEntry & { data: Record<string, unknown> } };
|
|
632
|
-
break;
|
|
633
|
-
}
|
|
634
|
-
}
|
|
635
|
-
if (!found) throw new PointsWriteError("record-not-found", `no answer record has id ${opts.id}`);
|
|
858
|
+
// #3402: and so is whoever relayed the answer.
|
|
859
|
+
const relayedBy = noteOf(opts.relayedBy);
|
|
860
|
+
if (relayedBy !== undefined) refuseUnidentified(scopeSource(opts.cwd), [relayedBy], "--relayed-by");
|
|
861
|
+
const found = await findQuestion(opts.cwd, opts.kind, opts.id);
|
|
636
862
|
const { opened, before, target } = found;
|
|
637
863
|
const ledger = found.read.ledger;
|
|
638
864
|
const { o } = opened;
|
|
639
865
|
const d = target.data;
|
|
640
|
-
if (target.state === "answered")
|
|
866
|
+
if (target.state === "answered") {
|
|
867
|
+
throw new PointsWriteError("record-closed", `${opts.id} is answered, and an answer never changes in place: retract it with \`points retract ${opts.id}\` and answer again, ask again with other inputs, or change the point, which asks the question anew`);
|
|
868
|
+
}
|
|
869
|
+
const note = noteOf(opts.note);
|
|
870
|
+
if (note !== undefined && !answerFields(o.loaded.schema).note) {
|
|
871
|
+
throw new PointsWriteError("answer-field-unsupported", `the ${o.loaded.kind.name} kind's schema has no note field, so ${opts.id} can't keep the note: copy point-answer.schema.json from @intentius/chant anew`);
|
|
872
|
+
}
|
|
873
|
+
if (relayedBy !== undefined && !answerFields(o.loaded.schema).relayedBy) {
|
|
874
|
+
throw new PointsWriteError("answer-field-unsupported", `the ${o.loaded.kind.name} kind's schema has no relayed_by field, so ${opts.id} can't record who relayed the answer: copy point-answer.schema.json from @intentius/chant anew`);
|
|
875
|
+
}
|
|
641
876
|
const name = String(d.point);
|
|
642
877
|
const point = pointOf(opened.points, name);
|
|
643
878
|
if (!point) throw new PointsWriteError("point-unknown", `${opts.id} answers ${name}, which ${opened.pointsFile} no longer declares`);
|
|
644
|
-
|
|
879
|
+
// An ad-hoc question is answered from the candidates it was asked with (#3403).
|
|
880
|
+
const asked = askedOf(d);
|
|
881
|
+
const allowed = asked ? candidates(askedQuestion(point, asked)) : Array.isArray(d.candidates) ? (d.candidates as (string | boolean)[]) : candidates(point.question);
|
|
645
882
|
const type = String(d.question_type);
|
|
646
883
|
const value = type === "noul" && typeof opts.answer === "string" ? ({ true: true, yes: true, false: false, no: false } as Record<string, boolean>)[opts.answer.toLowerCase()] : opts.answer;
|
|
647
884
|
if (value === undefined || !allowed.includes(value)) {
|
|
648
885
|
throw new PointsWriteError("answer-not-candidate", `${opts.id} takes one of ${allowed.map((c) => JSON.stringify(c)).join(", ")}, not ${JSON.stringify(opts.answer)}`);
|
|
649
886
|
}
|
|
650
887
|
const quorum = quorumOf(point);
|
|
651
|
-
const tally =
|
|
652
|
-
if (!tally.met) {
|
|
653
|
-
const got = tally.counted.length;
|
|
654
|
-
const left = tally.left.length ? `; not counted: ${tally.left.map((l) => `${l.name}, who ${l.why}`).join("; ")}` : "";
|
|
655
|
-
throw new PointsWriteError(
|
|
656
|
-
"quorum-not-met",
|
|
657
|
-
`${name} needs ${quorum.count} ${quorum.count === 1 ? "person" : "people"}${quorum.roles ? ` holding ${quorum.roles.join(" or ")}` : ""} to answer, and ${got} ${got === 1 ? "counts" : "count"}${got ? ` (${tally.counted.join(", ")})` : ""}${left}`,
|
|
658
|
-
);
|
|
659
|
-
}
|
|
888
|
+
const tally = meetQuorum(name, opts.by, quorum, o.root, askedByOf(d)?.steward, "answer");
|
|
660
889
|
const on = opts.on ?? today();
|
|
661
890
|
const decider = d.decider as Record<string, unknown>;
|
|
662
891
|
const confirmed = target.state === "proposed" && decider.kind === "model" && d.answer === value;
|
|
663
892
|
const escalations = Array.isArray(d.escalations) ? [...(d.escalations as Escalation[])] : [];
|
|
664
893
|
let fields: Record<string, unknown>;
|
|
665
894
|
if (confirmed) {
|
|
666
|
-
fields = { ...d, state: "answered", answered_by: tally.counted, answered_on: on };
|
|
895
|
+
fields = { ...d, state: "answered", answered_by: tally.counted, answered_on: on, note, relayed_by: relayedBy };
|
|
667
896
|
} else {
|
|
668
897
|
if (target.state === "proposed" && decider.kind === "model") {
|
|
669
898
|
escalations.push({
|
|
@@ -675,9 +904,11 @@ export async function answerPoint(opts: AnswerPointOptions): Promise<PointsWrite
|
|
|
675
904
|
...(typeof d.confidence === "number" ? { confidence: d.confidence } : {}),
|
|
676
905
|
...(typeof d.threshold === "number" ? { threshold: d.threshold } : {}),
|
|
677
906
|
reason: `proposed ${show(d.answer as string | boolean, type)}, and people answered ${show(value, type)}`,
|
|
907
|
+
// The model's own reason moves with its proposal (#3345).
|
|
908
|
+
...(typeof d.reason === "string" && reasonFields(o.loaded.schema).escalation ? { model_reason: d.reason } : {}),
|
|
678
909
|
});
|
|
679
910
|
}
|
|
680
|
-
const rest = Object.fromEntries(Object.entries(d).filter(([k]) => k !== "probabilities" && k !== "confidence" && k !== "threshold"));
|
|
911
|
+
const rest = Object.fromEntries(Object.entries(d).filter(([k]) => k !== "probabilities" && k !== "confidence" && k !== "threshold" && k !== "reason"));
|
|
681
912
|
fields = {
|
|
682
913
|
...rest,
|
|
683
914
|
state: "answered",
|
|
@@ -686,15 +917,17 @@ export async function answerPoint(opts: AnswerPointOptions): Promise<PointsWrite
|
|
|
686
917
|
escalations: escalations.length > 0 ? escalations : undefined,
|
|
687
918
|
answered_by: tally.counted,
|
|
688
919
|
answered_on: on,
|
|
920
|
+
note,
|
|
921
|
+
relayed_by: relayedBy,
|
|
689
922
|
};
|
|
690
923
|
}
|
|
691
924
|
const subject = Array.isArray(d.constrains) && typeof d.constrains[0] === "string" ? (d.constrains[0] as string) : undefined;
|
|
692
|
-
fields.title = titleFor(point, subject, "answered", value);
|
|
925
|
+
fields.title = titleFor(point, subject, "answered", value, asked);
|
|
693
926
|
const data = recordData(o, fields);
|
|
694
|
-
const text = renderRecord(data, body(point, String(fields.title)));
|
|
927
|
+
const text = renderRecord(data, body(point, String(fields.title), asked));
|
|
695
928
|
const onLedger = ledger.byPath.has(target.path);
|
|
696
|
-
const message = `Decision point ${name}: answered ${show(value, type)} by ${tally.counted.join(", ")}`;
|
|
697
|
-
const wrote = await write(o, before, target.path, text, false, !!opts.dryRun, { source: found.read.source, ledger, onLedger, message });
|
|
929
|
+
const message = `Decision point ${name}: answered ${show(value, type)} by ${tally.counted.join(", ")}${relayedBy ? `, relayed by ${relayedBy}` : ""}`;
|
|
930
|
+
const wrote = await write(o, before, target.path, text, false, !!opts.dryRun, { source: found.read.source, ledger, onLedger, message, who: { verb: "points answer", by: tally.counted.join(", ") } });
|
|
698
931
|
const entry: RecordEntry = { ...target, state: "answered", data, valid: true, reasons: [], warnings: wrote.warnings };
|
|
699
932
|
return {
|
|
700
933
|
$schema: POINTS_WRITE_SCHEMA_ID,
|
|
@@ -713,3 +946,124 @@ export async function answerPoint(opts: AnswerPointOptions): Promise<PointsWrite
|
|
|
713
946
|
return failure("answer", err);
|
|
714
947
|
}
|
|
715
948
|
}
|
|
949
|
+
|
|
950
|
+
// ── points retract ───────────────────────────────────────────────────────────
|
|
951
|
+
|
|
952
|
+
export interface RetractAnswerOptions {
|
|
953
|
+
cwd: string;
|
|
954
|
+
/** The answer record's id. */
|
|
955
|
+
id: string;
|
|
956
|
+
/** Who takes the answer back. They count toward the point's quorum as answerers do. */
|
|
957
|
+
by: string[];
|
|
958
|
+
/** Why, recorded on the retraction. */
|
|
959
|
+
note?: string;
|
|
960
|
+
kind?: string;
|
|
961
|
+
on?: string;
|
|
962
|
+
dryRun?: boolean;
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
/**
|
|
966
|
+
* People take an answer back: `points retract` (#3351, ws-084). The answer
|
|
967
|
+
* never changes in place. The question is escalated to the point's quorum
|
|
968
|
+
* again, open for people, and the answer as it was (its value, decider, who
|
|
969
|
+
* gave it, when, and its note) moves into the record's `retractions`, with who
|
|
970
|
+
* retracted it, when and why. A model's confirmed proposal also moves into the
|
|
971
|
+
* escalations, as when people answer otherwise. Retracting needs the point's
|
|
972
|
+
* quorum, counted as an answer is, and is refused in a steward's turn. People
|
|
973
|
+
* answer the question again with `points answer`.
|
|
974
|
+
*/
|
|
975
|
+
export async function retractAnswer(opts: RetractAnswerOptions): Promise<PointsWriteDocument> {
|
|
976
|
+
try {
|
|
977
|
+
return await withWriteLock(opts.cwd, { verb: "points retract", by: (opts.by ?? []).join(", ") || null }, opts.dryRun, () => retractAnswerLocked(opts));
|
|
978
|
+
} catch (err) {
|
|
979
|
+
return failure("retract", err);
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
async function retractAnswerLocked(opts: RetractAnswerOptions): Promise<PointsWriteDocument> {
|
|
984
|
+
try {
|
|
985
|
+
refuseStewardTurn(opts.id, "retracts");
|
|
986
|
+
refuseUnidentified(scopeSource(opts.cwd), opts.by, "--by");
|
|
987
|
+
const found = await findQuestion(opts.cwd, opts.kind, opts.id);
|
|
988
|
+
const { opened, before, target } = found;
|
|
989
|
+
const ledger = found.read.ledger;
|
|
990
|
+
const { o } = opened;
|
|
991
|
+
const d = target.data;
|
|
992
|
+
if (target.state !== "answered") {
|
|
993
|
+
throw new PointsWriteError("answer-not-answered", `${opts.id} is ${target.state ?? "not answered"}, so there is no answer to retract: answer it with \`points answer ${opts.id}\``);
|
|
994
|
+
}
|
|
995
|
+
if (!answerFields(o.loaded.schema).retractions) {
|
|
996
|
+
throw new PointsWriteError("answer-field-unsupported", `the ${o.loaded.kind.name} kind's schema has no retractions field, so ${opts.id} can't keep the answer it would retract: copy point-answer.schema.json from @intentius/chant anew`);
|
|
997
|
+
}
|
|
998
|
+
const name = String(d.point);
|
|
999
|
+
const point = pointOf(opened.points, name);
|
|
1000
|
+
if (!point) throw new PointsWriteError("point-unknown", `${opts.id} answers ${name}, which ${opened.pointsFile} no longer declares, so no quorum can take the answer back: ask the point as it is declared now`);
|
|
1001
|
+
const quorum = quorumOf(point);
|
|
1002
|
+
const tally = meetQuorum(name, opts.by, quorum, o.root, askedByOf(d)?.steward, "retract");
|
|
1003
|
+
const on = opts.on ?? today();
|
|
1004
|
+
const type = String(d.question_type);
|
|
1005
|
+
const decider = d.decider !== null && typeof d.decider === "object" && !Array.isArray(d.decider) ? (d.decider as Record<string, unknown>) : { kind: "quorum" };
|
|
1006
|
+
const answer = d.answer as string | boolean;
|
|
1007
|
+
const note = noteOf(opts.note);
|
|
1008
|
+
const retraction = {
|
|
1009
|
+
answer,
|
|
1010
|
+
decider,
|
|
1011
|
+
answered_by: Array.isArray(d.answered_by) && d.answered_by.length > 0 ? d.answered_by : undefined,
|
|
1012
|
+
answered_on: typeof d.answered_on === "string" ? d.answered_on : undefined,
|
|
1013
|
+
answer_note: typeof d.note === "string" ? d.note : undefined,
|
|
1014
|
+
answer_relayed_by: typeof d.relayed_by === "string" ? d.relayed_by : undefined,
|
|
1015
|
+
by: tally.counted,
|
|
1016
|
+
on,
|
|
1017
|
+
note,
|
|
1018
|
+
};
|
|
1019
|
+
const escalations = Array.isArray(d.escalations) ? [...(d.escalations as Escalation[])] : [];
|
|
1020
|
+
if (decider.kind === "model") {
|
|
1021
|
+
// The confirmed proposal goes back among the deciders that did not settle the question.
|
|
1022
|
+
escalations.push({
|
|
1023
|
+
kind: "model",
|
|
1024
|
+
backend: String(decider.backend),
|
|
1025
|
+
model: String(decider.model),
|
|
1026
|
+
answer,
|
|
1027
|
+
...(d.probabilities ? { probabilities: d.probabilities as Record<string, number> } : {}),
|
|
1028
|
+
...(typeof d.confidence === "number" ? { confidence: d.confidence } : {}),
|
|
1029
|
+
...(typeof d.threshold === "number" ? { threshold: d.threshold } : {}),
|
|
1030
|
+
reason: `proposed ${show(answer, type)}, which people confirmed and then retracted`,
|
|
1031
|
+
...(typeof d.reason === "string" && reasonFields(o.loaded.schema).escalation ? { model_reason: d.reason } : {}),
|
|
1032
|
+
});
|
|
1033
|
+
}
|
|
1034
|
+
const gone = new Set(["answer", "answered_by", "answered_on", "note", "relayed_by", "probabilities", "confidence", "threshold", "reason"]);
|
|
1035
|
+
const rest = Object.fromEntries(Object.entries(d).filter(([k]) => !gone.has(k)));
|
|
1036
|
+
const prior = Array.isArray(d.retractions) ? (d.retractions as unknown[]) : [];
|
|
1037
|
+
const subject = Array.isArray(d.constrains) && typeof d.constrains[0] === "string" ? (d.constrains[0] as string) : undefined;
|
|
1038
|
+
const asked = askedOf(d);
|
|
1039
|
+
const title = titleFor(point, subject, "escalated", undefined, asked);
|
|
1040
|
+
const data = recordData(o, {
|
|
1041
|
+
...rest,
|
|
1042
|
+
title,
|
|
1043
|
+
state: "escalated",
|
|
1044
|
+
decider: { kind: "quorum", count: quorum.count, ...(quorum.roles ? { roles: quorum.roles } : {}) },
|
|
1045
|
+
escalations: escalations.length > 0 ? escalations : undefined,
|
|
1046
|
+
retractions: [...prior, Object.fromEntries(Object.entries(retraction).filter(([, v]) => v !== undefined))],
|
|
1047
|
+
});
|
|
1048
|
+
const text = renderRecord(data, body(point, title, asked));
|
|
1049
|
+
const onLedger = ledger.byPath.has(target.path);
|
|
1050
|
+
const message = `Decision point ${name}: ${show(answer, type)} retracted by ${tally.counted.join(", ")}`;
|
|
1051
|
+
const wrote = await write(o, before, target.path, text, false, !!opts.dryRun, { source: found.read.source, ledger, onLedger, message, who: { verb: "points retract", by: tally.counted.join(", ") } });
|
|
1052
|
+
const entry: RecordEntry = { ...target, state: "escalated", data, valid: true, reasons: [], warnings: wrote.warnings };
|
|
1053
|
+
return {
|
|
1054
|
+
$schema: POINTS_WRITE_SCHEMA_ID,
|
|
1055
|
+
contract: POINTS_WRITE_CONTRACT_VERSION,
|
|
1056
|
+
verb: "retract",
|
|
1057
|
+
kind: o.view,
|
|
1058
|
+
id: opts.id,
|
|
1059
|
+
path: target.path,
|
|
1060
|
+
reused: false,
|
|
1061
|
+
written: !opts.dryRun,
|
|
1062
|
+
dryRun: !!opts.dryRun,
|
|
1063
|
+
question: questionView(entry, point, onLedger ? (wrote.ledger ?? ledger.byPath.get(target.path)?.ledger ?? null) : null)!,
|
|
1064
|
+
...(opts.dryRun ? { text } : {}),
|
|
1065
|
+
};
|
|
1066
|
+
} catch (err) {
|
|
1067
|
+
return failure("retract", err);
|
|
1068
|
+
}
|
|
1069
|
+
}
|