@intentius/chant 0.90.0 → 0.93.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/cli/commands/build.d.ts.map +1 -1
- package/dist/cli/commands/lint.d.ts.map +1 -1
- package/dist/cli/handlers/components.d.ts +8 -0
- package/dist/cli/handlers/components.d.ts.map +1 -1
- package/dist/cli/handlers/graph.d.ts.map +1 -1
- package/dist/cli/handlers/operator.d.ts +20 -0
- package/dist/cli/handlers/operator.d.ts.map +1 -1
- package/dist/cli/handlers/run.d.ts.map +1 -1
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/mcp/workspace-tools.d.ts +7 -4
- package/dist/cli/mcp/workspace-tools.d.ts.map +1 -1
- package/dist/cli/plugins.d.ts.map +1 -1
- package/dist/cli/registry.d.ts +42 -1
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/codegen/pinned-upgrade.d.ts.map +1 -1
- package/dist/components/verbs/vuln-scan.d.ts +72 -0
- package/dist/components/verbs/vuln-scan.d.ts.map +1 -1
- package/dist/config.d.ts +22 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/lexicon-module.d.ts +22 -0
- package/dist/lexicon-module.d.ts.map +1 -1
- package/dist/lifecycle/converge-ledger.d.ts +19 -0
- package/dist/lifecycle/converge-ledger.d.ts.map +1 -1
- package/dist/lifecycle/git.d.ts +40 -5
- package/dist/lifecycle/git.d.ts.map +1 -1
- package/dist/lifecycle/lease.d.ts +72 -16
- package/dist/lifecycle/lease.d.ts.map +1 -1
- package/dist/lifecycle/member-ledger.d.ts +3 -2
- package/dist/lifecycle/member-ledger.d.ts.map +1 -1
- package/dist/lifecycle/plan-ledger.d.ts +114 -0
- package/dist/lifecycle/plan-ledger.d.ts.map +1 -0
- package/dist/lifecycle/run-ledger.d.ts +2 -0
- package/dist/lifecycle/run-ledger.d.ts.map +1 -1
- package/dist/lifecycle/symptoms.d.ts +51 -0
- package/dist/lifecycle/symptoms.d.ts.map +1 -1
- package/dist/lifecycle/work-lease.d.ts +140 -0
- package/dist/lifecycle/work-lease.d.ts.map +1 -0
- package/dist/lint/rules/index.d.ts +4 -2
- package/dist/lint/rules/index.d.ts.map +1 -1
- package/dist/lint/rules/op/index.d.ts +14 -3
- package/dist/lint/rules/op/index.d.ts.map +1 -1
- package/dist/lint/rules/op/ops014-converge-rule-refusals.d.ts.map +1 -1
- package/dist/lint/rules/op/sys010-key-over-plain-http.d.ts +29 -0
- package/dist/lint/rules/op/sys010-key-over-plain-http.d.ts.map +1 -0
- package/dist/lint/rules/sys001-no-literal-key.d.ts +3 -0
- package/dist/lint/rules/sys001-no-literal-key.d.ts.map +1 -0
- package/dist/op/__fixtures__/decide-stub-backend.d.ts +53 -0
- package/dist/op/__fixtures__/decide-stub-backend.d.ts.map +1 -0
- package/dist/op/activities/activity-contracts.d.ts +203 -2
- package/dist/op/activities/activity-contracts.d.ts.map +1 -1
- package/dist/op/activities/apply.d.ts.map +1 -1
- package/dist/op/activities/change-coverage.d.ts +44 -0
- package/dist/op/activities/change-coverage.d.ts.map +1 -0
- package/dist/op/activities/converge.d.ts +24 -1
- package/dist/op/activities/converge.d.ts.map +1 -1
- package/dist/op/activities/decide.d.ts +107 -0
- package/dist/op/activities/decide.d.ts.map +1 -0
- package/dist/op/activities/index.d.ts +10 -0
- package/dist/op/activities/index.d.ts.map +1 -1
- package/dist/op/activities/shell.d.ts +32 -0
- package/dist/op/activities/shell.d.ts.map +1 -1
- package/dist/op/activities/source-release.d.ts +86 -0
- package/dist/op/activities/source-release.d.ts.map +1 -0
- package/dist/op/activities/source-rollback.d.ts +100 -0
- package/dist/op/activities/source-rollback.d.ts.map +1 -0
- package/dist/op/activities/work-evidence.d.ts +42 -0
- package/dist/op/activities/work-evidence.d.ts.map +1 -0
- package/dist/op/activity-contract.d.ts +1 -0
- package/dist/op/activity-contract.d.ts.map +1 -1
- package/dist/op/activity-profiles.d.ts +16 -0
- package/dist/op/activity-profiles.d.ts.map +1 -1
- package/dist/op/builders.d.ts +84 -2
- package/dist/op/builders.d.ts.map +1 -1
- package/dist/op/composites/converge-op.d.ts +31 -4
- package/dist/op/composites/converge-op.d.ts.map +1 -1
- package/dist/op/decide-backend.d.ts +116 -0
- package/dist/op/decide-backend.d.ts.map +1 -0
- package/dist/op/decide-config.d.ts +120 -0
- package/dist/op/decide-config.d.ts.map +1 -0
- package/dist/op/decide-read-inputs.d.ts +40 -0
- package/dist/op/decide-read-inputs.d.ts.map +1 -0
- package/dist/op/discover.d.ts +25 -0
- package/dist/op/discover.d.ts.map +1 -1
- package/dist/op/gate-wait.d.ts +43 -0
- package/dist/op/gate-wait.d.ts.map +1 -0
- package/dist/op/gate.d.ts +5 -2
- package/dist/op/gate.d.ts.map +1 -1
- package/dist/op/index.d.ts +18 -5
- package/dist/op/index.d.ts.map +1 -1
- package/dist/op/lifecycle-receipt-store.d.ts +34 -0
- package/dist/op/lifecycle-receipt-store.d.ts.map +1 -0
- package/dist/op/local-executor.d.ts +48 -4
- package/dist/op/local-executor.d.ts.map +1 -1
- package/dist/op/local-output.d.ts +2 -0
- package/dist/op/local-output.d.ts.map +1 -1
- package/dist/op/op-ir.d.ts +12 -1
- 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 +126 -1
- package/dist/op/operator.d.ts.map +1 -1
- package/dist/op/runtime.d.ts +46 -5
- package/dist/op/runtime.d.ts.map +1 -1
- package/dist/op/runtimes/local.d.ts.map +1 -1
- package/dist/op/source-archive.d.ts +63 -0
- package/dist/op/source-archive.d.ts.map +1 -0
- package/dist/op/step-output-ref.d.ts +2 -2
- package/dist/op/step-output-ref.d.ts.map +1 -1
- package/dist/op/steward-points.d.ts +136 -0
- package/dist/op/steward-points.d.ts.map +1 -0
- package/dist/op/steward-turn.d.ts +53 -0
- package/dist/op/steward-turn.d.ts.map +1 -0
- package/dist/op/steward.d.ts +161 -0
- package/dist/op/steward.d.ts.map +1 -0
- package/dist/op/types.d.ts +58 -0
- package/dist/op/types.d.ts.map +1 -1
- package/dist/op/work-lease-decl.d.ts +18 -0
- package/dist/op/work-lease-decl.d.ts.map +1 -0
- package/dist/op/work-lease-run.d.ts +173 -0
- package/dist/op/work-lease-run.d.ts.map +1 -0
- package/dist/workspace/__fixtures__/decide-workspace.d.ts +100 -0
- package/dist/workspace/__fixtures__/decide-workspace.d.ts.map +1 -0
- package/dist/workspace/answers-ledger.d.ts +76 -0
- package/dist/workspace/answers-ledger.d.ts.map +1 -0
- package/dist/workspace/box-isolation.d.ts +99 -0
- package/dist/workspace/box-isolation.d.ts.map +1 -0
- package/dist/workspace/changes-cli.d.ts +16 -0
- package/dist/workspace/changes-cli.d.ts.map +1 -0
- package/dist/workspace/changes.d.ts +200 -0
- package/dist/workspace/changes.d.ts.map +1 -0
- package/dist/workspace/chant-migrations/chud-lexicon-exit-fly-site.d.ts +59 -0
- package/dist/workspace/chant-migrations/chud-lexicon-exit-fly-site.d.ts.map +1 -0
- package/dist/workspace/chant-migrations/chud-lexicon-exit-live-names.d.ts +51 -0
- package/dist/workspace/chant-migrations/chud-lexicon-exit-live-names.d.ts.map +1 -0
- package/dist/workspace/chant-migrations/chud-lexicon-exit-rollback.d.ts +26 -0
- package/dist/workspace/chant-migrations/chud-lexicon-exit-rollback.d.ts.map +1 -0
- package/dist/workspace/chant-migrations/chud-lexicon-exit-ship-inputs.d.ts +59 -0
- package/dist/workspace/chant-migrations/chud-lexicon-exit-ship-inputs.d.ts.map +1 -0
- package/dist/workspace/chant-migrations/chud-lexicon-exit-ship-policy.d.ts +22 -0
- package/dist/workspace/chant-migrations/chud-lexicon-exit-ship-policy.d.ts.map +1 -0
- package/dist/workspace/chant-migrations/chud-lexicon-exit.d.ts +71 -0
- package/dist/workspace/chant-migrations/chud-lexicon-exit.d.ts.map +1 -0
- package/dist/workspace/chant-migrations.d.ts +99 -0
- package/dist/workspace/chant-migrations.d.ts.map +1 -0
- package/dist/workspace/checks/box-isolation.d.ts +18 -0
- package/dist/workspace/checks/box-isolation.d.ts.map +1 -0
- package/dist/workspace/checks/boxes.d.ts +104 -0
- package/dist/workspace/checks/boxes.d.ts.map +1 -0
- package/dist/workspace/checks/diagrams.d.ts +31 -0
- package/dist/workspace/checks/diagrams.d.ts.map +1 -0
- package/dist/workspace/checks/records.d.ts +8 -0
- package/dist/workspace/checks/records.d.ts.map +1 -1
- package/dist/workspace/checks.d.ts +13 -1
- package/dist/workspace/checks.d.ts.map +1 -1
- package/dist/workspace/compose-graph.d.ts +2 -0
- package/dist/workspace/compose-graph.d.ts.map +1 -1
- package/dist/workspace/conformance/index.d.ts.map +1 -1
- package/dist/workspace/decide.d.ts +229 -0
- package/dist/workspace/decide.d.ts.map +1 -0
- package/dist/workspace/decision-points.schema.json +137 -0
- package/dist/workspace/declaration.d.ts +103 -0
- package/dist/workspace/declaration.d.ts.map +1 -1
- package/dist/workspace/declaration.schema.json +285 -0
- package/dist/workspace/declared-kinds.d.ts +32 -0
- package/dist/workspace/declared-kinds.d.ts.map +1 -1
- package/dist/workspace/generated-files.d.ts +7 -0
- package/dist/workspace/generated-files.d.ts.map +1 -1
- package/dist/workspace/intent.d.ts.map +1 -1
- package/dist/workspace/lineage-check.d.ts +3 -0
- package/dist/workspace/lineage-check.d.ts.map +1 -1
- package/dist/workspace/lineage-upgrade.d.ts +6 -1
- package/dist/workspace/lineage-upgrade.d.ts.map +1 -1
- package/dist/workspace/ls.d.ts +39 -1
- package/dist/workspace/ls.d.ts.map +1 -1
- package/dist/workspace/point-answer.schema.json +95 -0
- package/dist/workspace/points-cli.d.ts +113 -0
- package/dist/workspace/points-cli.d.ts.map +1 -0
- package/dist/workspace/points.d.ts +320 -0
- package/dist/workspace/points.d.ts.map +1 -0
- package/dist/workspace/reason-codes.d.ts +38 -0
- package/dist/workspace/reason-codes.d.ts.map +1 -1
- package/dist/workspace/record-assets.d.ts.map +1 -1
- package/dist/workspace/records-cli.d.ts +23 -1
- package/dist/workspace/records-cli.d.ts.map +1 -1
- package/dist/workspace/records-close.d.ts +1 -1
- package/dist/workspace/records-close.d.ts.map +1 -1
- package/dist/workspace/records-write.d.ts +11 -5
- package/dist/workspace/records-write.d.ts.map +1 -1
- package/dist/workspace/records.d.ts +42 -6
- package/dist/workspace/records.d.ts.map +1 -1
- package/dist/workspace/status-gates.d.ts +7 -2
- package/dist/workspace/status-gates.d.ts.map +1 -1
- package/dist/workspace/status-stewards.d.ts +179 -0
- package/dist/workspace/status-stewards.d.ts.map +1 -0
- package/dist/workspace/status.d.ts +68 -1
- package/dist/workspace/status.d.ts.map +1 -1
- package/dist/workspace/work-cli.d.ts +189 -0
- package/dist/workspace/work-cli.d.ts.map +1 -0
- package/dist/workspace/work-evidence.d.ts +74 -0
- package/dist/workspace/work-evidence.d.ts.map +1 -0
- package/dist/workspace/work.d.ts +43 -1
- package/dist/workspace/work.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/cli/commands/audit.test.ts +9 -1
- package/src/cli/commands/build.ts +3 -2
- package/src/cli/commands/lint.ts +7 -1
- package/src/cli/handlers/components.test.ts +93 -0
- package/src/cli/handlers/components.ts +44 -3
- package/src/cli/handlers/graph.test.ts +20 -0
- package/src/cli/handlers/graph.ts +16 -2
- package/src/cli/handlers/operator.ts +141 -4
- package/src/cli/handlers/run.test.ts +173 -2
- package/src/cli/handlers/run.ts +174 -3
- package/src/cli/main.test.ts +50 -0
- package/src/cli/main.ts +97 -2
- package/src/cli/mcp/workspace-tools.test.ts +48 -19
- package/src/cli/mcp/workspace-tools.ts +110 -8
- package/src/cli/plugins.ts +11 -3
- package/src/cli/registry.ts +42 -1
- package/src/codegen/pinned-upgrade.ts +2 -1
- package/src/codegen/release-wiring.test.ts +49 -2
- package/src/components/capability-plugin-loader.ts +3 -3
- package/src/components/cli-support.ts +2 -2
- package/src/components/verbs/vuln-scan.test.ts +124 -1
- package/src/components/verbs/vuln-scan.ts +142 -1
- package/src/config.ts +11 -0
- package/src/lexicon-module.ts +56 -1
- package/src/lexicon-package.test.ts +96 -0
- package/src/lifecycle/converge-ledger.ts +20 -0
- package/src/lifecycle/git.ts +65 -15
- package/src/lifecycle/lease.test.ts +22 -0
- package/src/lifecycle/lease.ts +133 -29
- package/src/lifecycle/member-ledger.ts +3 -2
- package/src/lifecycle/plan-ledger.test.ts +148 -0
- package/src/lifecycle/plan-ledger.ts +158 -0
- package/src/lifecycle/run-ledger.ts +6 -0
- package/src/lifecycle/symptoms.ts +95 -0
- package/src/lifecycle/work-lease.test.ts +236 -0
- package/src/lifecycle/work-lease.ts +426 -0
- package/src/lint/rules/index.ts +6 -2
- package/src/lint/rules/op/index.ts +17 -4
- package/src/lint/rules/op/ops014-converge-rule-refusals.test.ts +23 -1
- package/src/lint/rules/op/ops014-converge-rule-refusals.ts +10 -6
- package/src/lint/rules/op/sys010-key-over-plain-http.test.ts +52 -0
- package/src/lint/rules/op/sys010-key-over-plain-http.ts +100 -0
- package/src/lint/rules/sys001-no-literal-key.test.ts +31 -0
- package/src/lint/rules/sys001-no-literal-key.ts +67 -0
- package/src/op/__fixtures__/decide-stub-backend.ts +143 -0
- package/src/op/activities/activity-contracts.ts +190 -1
- package/src/op/activities/apply.ts +7 -6
- package/src/op/activities/change-coverage.ts +60 -0
- package/src/op/activities/converge.test.ts +55 -1
- package/src/op/activities/converge.ts +208 -29
- package/src/op/activities/decide.live.test.ts +78 -0
- package/src/op/activities/decide.test.ts +300 -0
- package/src/op/activities/decide.ts +267 -0
- package/src/op/activities/index.ts +21 -0
- package/src/op/activities/shell.test.ts +91 -1
- package/src/op/activities/shell.ts +91 -2
- package/src/op/activities/source-release.test.ts +118 -0
- package/src/op/activities/source-release.ts +168 -0
- package/src/op/activities/source-rollback.test.ts +131 -0
- package/src/op/activities/source-rollback.ts +239 -0
- package/src/op/activities/work-evidence.ts +62 -0
- package/src/op/activity-contract-registry.test.ts +2 -0
- package/src/op/activity-contract-registry.ts +2 -2
- package/src/op/activity-contract.test.ts +12 -0
- package/src/op/activity-contract.ts +6 -1
- package/src/op/activity-profiles.ts +29 -0
- package/src/op/activity-registry.test.ts +1 -1
- package/src/op/activity-registry.ts +2 -2
- package/src/op/builders.ts +124 -5
- package/src/op/command-gate.test.ts +192 -0
- package/src/op/composites/converge-op.test.ts +27 -1
- package/src/op/composites/converge-op.ts +69 -15
- package/src/op/decide-backend.test.ts +44 -0
- package/src/op/decide-backend.ts +212 -0
- package/src/op/decide-config.test.ts +52 -0
- package/src/op/decide-config.ts +80 -0
- package/src/op/decide-read-inputs.ts +117 -0
- package/src/op/discover.ts +71 -0
- package/src/op/gate-approval.ts +2 -2
- package/src/op/gate-wait.ts +57 -0
- package/src/op/gate.ts +8 -3
- package/src/op/generate-pipeline.ts +2 -2
- package/src/op/index.ts +34 -6
- package/src/op/lifecycle-receipt-store.test.ts +60 -0
- package/src/op/lifecycle-receipt-store.ts +61 -0
- package/src/op/local-executor.ts +367 -33
- package/src/op/local-output.test.ts +33 -0
- package/src/op/local-output.ts +33 -2
- package/src/op/op-ir.test.ts +10 -0
- package/src/op/op-ir.ts +18 -0
- package/src/op/op-verb-class.ts +1 -0
- package/src/op/operator.ts +360 -10
- package/src/op/runtime.ts +37 -8
- package/src/op/runtimes/local.ts +6 -1
- package/src/op/source-archive.ts +161 -0
- package/src/op/step-output-ref.ts +6 -2
- package/src/op/steward-points.test.ts +313 -0
- package/src/op/steward-points.ts +194 -0
- package/src/op/steward-turn.ts +79 -0
- package/src/op/steward.test.ts +296 -0
- package/src/op/steward.ts +277 -0
- package/src/op/types.ts +60 -0
- package/src/op/work-lease-decl.ts +80 -0
- package/src/op/work-lease-run.test.ts +345 -0
- package/src/op/work-lease-run.ts +395 -0
- package/src/workspace/__fixtures__/decide-workspace.ts +129 -0
- package/src/workspace/__fixtures__/studio-template-84c8b21.json +67 -0
- package/src/workspace/answers-ledger.ts +118 -0
- package/src/workspace/box-isolation.test.ts +261 -0
- package/src/workspace/box-isolation.ts +205 -0
- package/src/workspace/changes-cli.ts +56 -0
- package/src/workspace/changes.schema.json +472 -0
- package/src/workspace/changes.test.ts +315 -0
- package/src/workspace/changes.ts +512 -0
- package/src/workspace/chant-migrations/chud-lexicon-exit-fly-site.ts +234 -0
- package/src/workspace/chant-migrations/chud-lexicon-exit-live-names.ts +164 -0
- package/src/workspace/chant-migrations/chud-lexicon-exit-rollback.ts +223 -0
- package/src/workspace/chant-migrations/chud-lexicon-exit-ship-inputs.ts +195 -0
- package/src/workspace/chant-migrations/chud-lexicon-exit-ship-policy.ts +108 -0
- package/src/workspace/chant-migrations/chud-lexicon-exit.ts +882 -0
- package/src/workspace/chant-migrations.e2e.test.ts +1089 -0
- package/src/workspace/chant-migrations.ts +164 -0
- package/src/workspace/check-contract.test.ts +5 -1
- package/src/workspace/check.schema.json +25 -7
- package/src/workspace/checks/box-isolation.ts +68 -0
- package/src/workspace/checks/boxes.test.ts +276 -0
- package/src/workspace/checks/boxes.ts +442 -0
- package/src/workspace/checks/diagrams.test.ts +134 -0
- package/src/workspace/checks/diagrams.ts +108 -0
- package/src/workspace/checks/records.ts +72 -0
- package/src/workspace/checks.test.ts +7 -0
- package/src/workspace/checks.ts +27 -3
- package/src/workspace/compose-graph.ts +2 -0
- package/src/workspace/conformance/__fixture__/decisions/decision.kind.mjs +10 -0
- package/src/workspace/conformance/__fixture__/decisions/decision.schema.json +26 -0
- package/src/workspace/conformance/index.ts +5 -0
- package/src/workspace/decide.test.ts +224 -0
- package/src/workspace/decide.ts +709 -0
- package/src/workspace/decision-points.schema.json +137 -0
- package/src/workspace/declaration.schema.json +285 -0
- package/src/workspace/declaration.test.ts +58 -0
- package/src/workspace/declaration.ts +241 -0
- package/src/workspace/declared-kinds.test.ts +5 -5
- package/src/workspace/declared-kinds.ts +71 -3
- package/src/workspace/generated-files.ts +14 -0
- package/src/workspace/graph-cli.ts +1 -1
- package/src/workspace/graph.schema.json +9 -1
- package/src/workspace/intent.schema.json +12 -3
- package/src/workspace/intent.ts +3 -10
- package/src/workspace/lineage-check.ts +16 -1
- package/src/workspace/lineage-upgrade-cli.ts +1 -1
- package/src/workspace/lineage-upgrade.test.ts +70 -0
- package/src/workspace/lineage-upgrade.ts +81 -10
- package/src/workspace/ls-contract.test.ts +57 -0
- package/src/workspace/ls.schema.json +46 -1
- package/src/workspace/ls.ts +62 -3
- package/src/workspace/point-answer.schema.json +95 -0
- package/src/workspace/points-cli.ts +284 -0
- package/src/workspace/points-write.schema.json +499 -0
- package/src/workspace/points.schema.json +743 -0
- package/src/workspace/points.test.ts +264 -0
- package/src/workspace/points.ts +564 -0
- package/src/workspace/read-contract.test.ts +18 -1
- package/src/workspace/reason-codes.test.ts +24 -1
- package/src/workspace/reason-codes.ts +49 -0
- package/src/workspace/record-assets.test.ts +3 -2
- package/src/workspace/record-assets.ts +4 -1
- package/src/workspace/records-amend.schema.json +2 -0
- package/src/workspace/records-cli.ts +28 -5
- package/src/workspace/records-close.schema.json +2 -0
- package/src/workspace/records-contract.test.ts +3 -2
- package/src/workspace/records-new.schema.json +2 -0
- package/src/workspace/records-review.schema.json +2 -0
- package/src/workspace/records-sessions-write.e2e.test.ts +77 -0
- package/src/workspace/records-sessions-write.test.ts +2 -34
- package/src/workspace/records-write.e2e.test.ts +108 -0
- package/src/workspace/records-write.test.ts +32 -41
- package/src/workspace/records-write.ts +33 -15
- package/src/workspace/records.schema.json +53 -2
- package/src/workspace/records.test.ts +48 -3
- package/src/workspace/records.ts +142 -6
- package/src/workspace/status-contract.test.ts +189 -6
- package/src/workspace/status-gates.ts +9 -4
- package/src/workspace/status-stewards.ts +329 -0
- package/src/workspace/status.schema.json +335 -5
- package/src/workspace/status.ts +142 -6
- package/src/workspace/work-acceptance.test.ts +274 -0
- package/src/workspace/work-cli.e2e.test.ts +180 -0
- package/src/workspace/work-cli.ts +429 -0
- package/src/workspace/work-evidence.schema.json +212 -0
- package/src/workspace/work-evidence.ts +169 -0
- package/src/workspace/work-history.schema.json +349 -0
- package/src/workspace/work-history.test.ts +156 -0
- package/src/workspace/work-lease.schema.json +233 -0
- package/src/workspace/work-readiness-chud.test.ts +145 -0
- package/src/workspace/work.test.ts +3 -2
- package/src/workspace/work.ts +100 -1
- /package/src/cli/{discovery-skip.test.ts → discovery-skip.e2e.test.ts} +0 -0
- /package/src/workspace/conformance/{conformance.test.ts → conformance.e2e.test.ts} +0 -0
- /package/src/workspace/{which-chant.test.ts → which-chant.e2e.test.ts} +0 -0
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The decide activity against the stub backend (#2740): a noul, a choice and
|
|
3
|
+
* a score answered and recorded, the run waiting on an open question until a
|
|
4
|
+
* person answers, reuse, the threshold, an unreachable backend escalating or
|
|
5
|
+
* failing as the point declares, brokered and literal keys, a steward's turn,
|
|
6
|
+
* and inputs read through the read contract.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { existsSync, readdirSync, rmSync, writeFileSync } from "node:fs";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
import { afterAll, afterEach, beforeAll, describe, expect, test } from "vitest";
|
|
12
|
+
import { enterStewardTurn, isPointWait, resetStewardTurn, type WaitingPoint } from "..";
|
|
13
|
+
import { loadActivities } from "../activity-registry";
|
|
14
|
+
import { answerPoint, type QuestionView } from "../../workspace/decide";
|
|
15
|
+
import { workspacePoints } from "../../workspace/points-cli";
|
|
16
|
+
import type { WireAnswer } from "../../workspace/points";
|
|
17
|
+
import { startStubBackend, type StubBackend } from "../__fixtures__/decide-stub-backend";
|
|
18
|
+
import { cleanScratch, MODEL, recordText, workspace } from "../../workspace/__fixtures__/decide-workspace";
|
|
19
|
+
import { DecideError, runDecide, type DecideArgs } from "./decide";
|
|
20
|
+
|
|
21
|
+
const KEY = "stub-key-not-a-secret";
|
|
22
|
+
const env = { STUB_KEY: KEY, LOBBY_TOKEN: KEY };
|
|
23
|
+
const on = "2026-09-25";
|
|
24
|
+
|
|
25
|
+
let root: string;
|
|
26
|
+
let stub: StubBackend;
|
|
27
|
+
/** What the stub answers, by question name, when a test sets it. */
|
|
28
|
+
let script: Record<string, WireAnswer> = {};
|
|
29
|
+
|
|
30
|
+
const backends = (url = stub.url) => ({ systemone: { url, key: { env: "STUB_KEY" } } });
|
|
31
|
+
const ask = (args: Omit<DecideArgs, "cwd" | "backends"> & { backends?: DecideArgs["backends"] }) => runDecide({ cwd: root, backends: backends(), ...args }, { env, on });
|
|
32
|
+
const answers = () => readdirSync(join(root, "answers")).filter((f) => f.endsWith(".md")).sort();
|
|
33
|
+
|
|
34
|
+
/** The question an open ask stopped the run on: the step threw core's PointWait. */
|
|
35
|
+
async function waitsOn(p: Promise<unknown>): Promise<WaitingPoint> {
|
|
36
|
+
const err = await p.then(
|
|
37
|
+
(r) => {
|
|
38
|
+
throw new Error(`expected the run to wait, and the step returned ${JSON.stringify(r)}`);
|
|
39
|
+
},
|
|
40
|
+
(e: unknown) => e,
|
|
41
|
+
);
|
|
42
|
+
if (!isPointWait(err)) throw err;
|
|
43
|
+
return err.question;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** The question as the read contract lists it. */
|
|
47
|
+
async function question(id: string): Promise<QuestionView> {
|
|
48
|
+
const doc = await workspacePoints({ cwd: root });
|
|
49
|
+
if ("error" in doc) throw new Error(doc.error.message);
|
|
50
|
+
const q = doc.questions.find((x) => x.id === id);
|
|
51
|
+
if (!q) throw new Error(`no question ${id}`);
|
|
52
|
+
return q;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
beforeAll(async () => {
|
|
56
|
+
root = workspace();
|
|
57
|
+
stub = await startStubBackend({ key: KEY, answers: (q) => script[q.name] ?? (undefined as unknown as WireAnswer) });
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
afterEach(() => resetStewardTurn());
|
|
61
|
+
|
|
62
|
+
afterAll(async () => {
|
|
63
|
+
await stub.close();
|
|
64
|
+
cleanScratch();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe("decide against the stub (#2740)", () => {
|
|
68
|
+
test("a noul: the model's answer at the threshold is recorded as proposed, and the run waits on it", async () => {
|
|
69
|
+
script = { triage: { type: "noul", noul: 0.91 } };
|
|
70
|
+
const w = await waitsOn(ask({ point: "triage", inputs: { "record.size": 5, "record.risky": true }, subject: "T-1" }));
|
|
71
|
+
expect(w).toMatchObject({ point: "triage", state: "proposed", subject: "T-1", steward: null });
|
|
72
|
+
const q = await question(w.id);
|
|
73
|
+
expect(q).toMatchObject({ state: "proposed", open: true, answer: true, decider: { kind: "model", backend: "systemone", model: MODEL }, confidence: 0.91, threshold: 0.8, valid: true });
|
|
74
|
+
expect(q.escalations).toEqual([{ kind: "table", reason: "no row matches these inputs" }]);
|
|
75
|
+
expect(recordText(root, w.path)).toContain(`model: "${MODEL}"`);
|
|
76
|
+
// The request went out in the wire format, with the pinned model and the bearer key.
|
|
77
|
+
expect(stub.requests[stub.requests.length - 1]).toEqual({
|
|
78
|
+
model: MODEL,
|
|
79
|
+
state: { "record.size": 5, "record.risky": true },
|
|
80
|
+
questions: { triage: { type: "noul", instructions: "Does the task need a person to look at it today?", criteria: { true: "A person looks at it today.", false: "It can wait." } } },
|
|
81
|
+
});
|
|
82
|
+
expect(stub.authorizations[stub.authorizations.length - 1]).toBe(`Bearer ${KEY}`);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("once a person confirms the proposal, the next run reads the answer and calls nothing", async () => {
|
|
86
|
+
const w = await waitsOn(ask({ point: "triage", inputs: { "record.risky": true, "record.size": 5 }, subject: "T-1" }));
|
|
87
|
+
const before = stub.requests.length;
|
|
88
|
+
const files = answers();
|
|
89
|
+
const confirmed = await answerPoint({ cwd: root, id: w.id, answer: true, by: ["alice"], on });
|
|
90
|
+
if ("error" in confirmed) throw new Error(confirmed.error.message);
|
|
91
|
+
const r = await ask({ point: "triage", inputs: { "record.size": 5, "record.risky": true }, subject: "T-1" });
|
|
92
|
+
expect(r).toMatchObject({ id: w.id, state: "answered", open: false, answer: true, decider: "model", model: MODEL, backend: "systemone", confidence: 0.91, answeredBy: ["alice"] });
|
|
93
|
+
expect(stub.requests.length).toBe(before);
|
|
94
|
+
expect(answers()).toEqual(files);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("a choice: the choice and its probabilities are recorded", async () => {
|
|
98
|
+
script = { route: { type: "choice", choice: "platform", probabilities: { platform: 0.86, app: 0.1, docs: 0.04 }, confidence: 0.84 } };
|
|
99
|
+
const w = await waitsOn(ask({ point: "route", inputs: { "record.title": "Rotate the deploy key", "record.size": 5 }, subject: "T-1" }));
|
|
100
|
+
expect(await question(w.id)).toMatchObject({ state: "proposed", answer: "platform", confidence: 0.84, probabilities: { platform: 0.86, app: 0.1, docs: 0.04 } });
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("a score: the most probable level is the answer", async () => {
|
|
104
|
+
script = { effort: { type: "score", score: 2.7, legend: { 1: "low", 2: "medium", 3: "high" }, probabilities: { low: 0.02, medium: 0.1, high: 0.88 }, confidence: 0.82 } };
|
|
105
|
+
const w = await waitsOn(ask({ point: "effort", inputs: { "record.size": 5, "member.kind": "other" }, subject: "T-1" }));
|
|
106
|
+
expect(await question(w.id)).toMatchObject({ state: "proposed", answer: "high", confidence: 0.82 });
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test("an open question asked again is reused: no second call, no second record", async () => {
|
|
110
|
+
const before = stub.requests.length;
|
|
111
|
+
const files = answers();
|
|
112
|
+
const w = await waitsOn(ask({ point: "route", inputs: { "record.size": 5, "record.title": "Rotate the deploy key" }, subject: "T-1" }));
|
|
113
|
+
expect(w.state).toBe("proposed");
|
|
114
|
+
expect(stub.requests.length).toBe(before);
|
|
115
|
+
expect(answers()).toEqual(files);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("a table row answers before the model is asked, and the step returns it", async () => {
|
|
119
|
+
const before = stub.requests.length;
|
|
120
|
+
const r = await ask({ point: "triage", inputs: { "record.size": 0, "record.risky": false } });
|
|
121
|
+
expect(r).toMatchObject({ state: "answered", open: false, answer: false, decider: "table", model: null });
|
|
122
|
+
expect(stub.requests.length).toBe(before);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test("below the threshold the question escalates to people, with the model's lean", async () => {
|
|
126
|
+
script = { triage: { type: "noul", noul: 0.6 } };
|
|
127
|
+
const w = await waitsOn(ask({ point: "triage", inputs: { "record.size": 3, "record.risky": false } }));
|
|
128
|
+
expect(w.state).toBe("escalated");
|
|
129
|
+
const q = await question(w.id);
|
|
130
|
+
expect(q.model).toMatchObject({ answer: true, confidence: 0.6, threshold: 0.8, observed: false });
|
|
131
|
+
expect(q.escalations[1].reason).toContain("below the threshold");
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test("a dry run asks, writes nothing, and returns the question whatever its state", async () => {
|
|
135
|
+
script = { triage: { type: "noul", noul: 0.05 } };
|
|
136
|
+
const files = answers();
|
|
137
|
+
const r = await ask({ point: "triage", inputs: { "record.size": 8, "record.risky": false }, dryRun: true });
|
|
138
|
+
expect(r).toMatchObject({ state: "proposed", open: true, answer: false, decider: "model" });
|
|
139
|
+
expect(answers()).toEqual(files);
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
describe("an unreachable backend follows the point's unreachable", () => {
|
|
144
|
+
let gone: string;
|
|
145
|
+
beforeAll(async () => {
|
|
146
|
+
const s = await startStubBackend();
|
|
147
|
+
gone = s.url;
|
|
148
|
+
await s.close();
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test("escalate (the default): the question is open for people, with why", async () => {
|
|
152
|
+
const w = await waitsOn(ask({ point: "triage", inputs: { "record.size": 2, "record.risky": true }, backends: backends(gone) }));
|
|
153
|
+
expect(w.state).toBe("escalated");
|
|
154
|
+
const e = (await question(w.id)).escalations[1];
|
|
155
|
+
expect(e).toMatchObject({ kind: "model", backend: "systemone", model: MODEL });
|
|
156
|
+
expect(e.reason).toMatch(/^not observed: systemone could not answer: systemone at http:\/\/127\.0\.0\.1:\d+\/v1\/systemone/);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test("fail: nothing is written and the step fails", async () => {
|
|
160
|
+
const files = answers();
|
|
161
|
+
const err = await ask({ point: "route", inputs: { "record.title": "Write the release notes", "record.size": 2 }, backends: backends(gone) }).catch((e: unknown) => e);
|
|
162
|
+
expect(err).toBeInstanceOf(DecideError);
|
|
163
|
+
expect((err as DecideError).code).toBe("point-decider-failed");
|
|
164
|
+
expect((err as Error).message).toContain('unreachable: "fail"');
|
|
165
|
+
expect(answers()).toEqual(files);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test("a 429 from the backend is unreachable too", async () => {
|
|
169
|
+
const busy = await startStubBackend({ status: 429 });
|
|
170
|
+
try {
|
|
171
|
+
const w = await waitsOn(runDecide({ cwd: root, point: "triage", inputs: { "record.size": 4, "record.risky": true }, backends: { systemone: { url: busy.url } } }, { on }));
|
|
172
|
+
expect((await question(w.id)).escalations[1].reason).toContain("answered 429");
|
|
173
|
+
} finally {
|
|
174
|
+
await busy.close();
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test("an unset key variable is unreachable, and the point decides", async () => {
|
|
179
|
+
const w = await waitsOn(runDecide({ cwd: root, point: "triage", inputs: { "record.size": 6, "record.risky": true }, backends: backends() }, { env: {}, on }));
|
|
180
|
+
expect((await question(w.id)).escalations[1].reason).toContain("the environment variable STUB_KEY is not set");
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
describe("the key", () => {
|
|
185
|
+
test("a brokered capability declared on the box: the broker's variable carries the key", async () => {
|
|
186
|
+
script = { triage: { type: "noul", noul: 0.97 } };
|
|
187
|
+
const w = await waitsOn(ask({ point: "triage", inputs: { "record.size": 9, "record.risky": true }, backends: { systemone: { url: stub.url, key: { capability: "inference", member: "box", env: "LOBBY_TOKEN" } } } }));
|
|
188
|
+
expect(w.state).toBe("proposed");
|
|
189
|
+
expect(stub.authorizations[stub.authorizations.length - 1]).toBe(`Bearer ${KEY}`);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
test("a brokered capability without env sends no key: the broker is the endpoint", async () => {
|
|
193
|
+
const open = await startStubBackend();
|
|
194
|
+
try {
|
|
195
|
+
const w = await waitsOn(ask({ point: "triage", inputs: { "record.size": 10, "record.risky": true }, backends: { systemone: { url: open.url, key: { capability: "inference" } } } }));
|
|
196
|
+
expect(w.state).toBe("proposed");
|
|
197
|
+
expect(open.authorizations).toEqual([null]);
|
|
198
|
+
} finally {
|
|
199
|
+
await open.close();
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
test.each([
|
|
204
|
+
["a capability with no broker", { capability: "raw", member: "box", env: "LOBBY_TOKEN" }, "names no broker"],
|
|
205
|
+
["a capability no box declares", { capability: "fountain" }, "no member's box declares the capability fountain"],
|
|
206
|
+
["a literal string", "sk-not-a-real-key", "is a literal string"],
|
|
207
|
+
])("%s is refused before anything is asked or written", async (_label, key, message) => {
|
|
208
|
+
const before = stub.requests.length;
|
|
209
|
+
const files = answers();
|
|
210
|
+
const err = await ask({ point: "triage", inputs: { "record.size": 11, "record.risky": true }, backends: { systemone: { url: stub.url, key } } as never }).catch((e: unknown) => e);
|
|
211
|
+
expect(err).toBeInstanceOf(DecideError);
|
|
212
|
+
expect((err as DecideError).code).toBe("backend-invalid");
|
|
213
|
+
expect((err as Error).message).toContain(message);
|
|
214
|
+
expect(stub.requests.length).toBe(before);
|
|
215
|
+
expect(answers()).toEqual(files);
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
test("a backend the point names but nobody configured is refused", async () => {
|
|
219
|
+
const err = await ask({ point: "triage", inputs: { "record.size": 12, "record.risky": true }, backends: { other: { url: stub.url } } }).catch((e: unknown) => e);
|
|
220
|
+
expect((err as DecideError).code).toBe("backend-invalid");
|
|
221
|
+
expect((err as Error).message).toContain("names the backend systemone, and none is configured (decide.backends: other)");
|
|
222
|
+
});
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
describe("in a steward's turn (#2749)", () => {
|
|
226
|
+
const brokered = { systemone: { url: "", key: { capability: "inference", member: "box", env: "LOBBY_TOKEN" } } };
|
|
227
|
+
|
|
228
|
+
test("the call goes through a capability the steward declares, and the question names the steward", async () => {
|
|
229
|
+
enterStewardTurn({ steward: "keeper", capabilities: ["inference"], run: "run-1" });
|
|
230
|
+
script = { triage: { type: "noul", noul: 0.95 } };
|
|
231
|
+
const before = stub.requests.length;
|
|
232
|
+
const w = await waitsOn(ask({ point: "triage", inputs: { "record.size": 14, "record.risky": true }, backends: { systemone: { ...brokered.systemone, url: stub.url } } }));
|
|
233
|
+
expect(w).toMatchObject({ state: "proposed", steward: "keeper" });
|
|
234
|
+
expect(stub.requests.length).toBe(before + 1);
|
|
235
|
+
expect((await question(w.id)).askedBy).toEqual({ steward: "keeper", run: "run-1" });
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
test("a steward that does not declare the capability makes no call, and the question goes to people", async () => {
|
|
239
|
+
enterStewardTurn({ steward: "keeper", capabilities: [] });
|
|
240
|
+
const before = stub.requests.length;
|
|
241
|
+
const w = await waitsOn(ask({ point: "triage", inputs: { "record.size": 15, "record.risky": true }, backends: { systemone: { ...brokered.systemone, url: stub.url } } }));
|
|
242
|
+
expect(w.state).toBe("escalated");
|
|
243
|
+
expect(stub.requests.length).toBe(before);
|
|
244
|
+
expect((await question(w.id)).escalations[1].reason).toContain("does not name the brokered capability inference");
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
test("a backend keyed by an environment variable is not called: the steward would hold the key", async () => {
|
|
248
|
+
enterStewardTurn({ steward: "keeper", capabilities: ["inference"] });
|
|
249
|
+
const before = stub.requests.length;
|
|
250
|
+
const w = await waitsOn(ask({ point: "triage", inputs: { "record.size": 16, "record.risky": true } }));
|
|
251
|
+
expect(w.state).toBe("escalated");
|
|
252
|
+
expect(stub.requests.length).toBe(before);
|
|
253
|
+
expect((await question(w.id)).escalations[1].reason).toContain("key is the environment variable STUB_KEY");
|
|
254
|
+
});
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
describe("inputs read through the read contract", () => {
|
|
258
|
+
test("a record by id gives each record.* input its field, and a member by name its own", async () => {
|
|
259
|
+
script = { effort: { type: "score", probabilities: { low: 0.9, medium: 0.08, high: 0.02 }, confidence: 0.85 } };
|
|
260
|
+
const w = await waitsOn(ask({ point: "effort", read: { record: "T-2", member: "app" }, subject: "T-2" }));
|
|
261
|
+
expect(stub.requests[stub.requests.length - 1].state).toEqual({ "record.size": 1, "member.kind": "other" });
|
|
262
|
+
expect(await question(w.id)).toMatchObject({ answer: "low", inputs: { "record.size": 1, "member.kind": "other" } });
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
test("an input passed as a value wins over the one read", async () => {
|
|
266
|
+
script = { triage: { type: "noul", noul: 0.02 } };
|
|
267
|
+
await waitsOn(ask({ point: "triage", read: { record: "T-2" }, inputs: { "record.risky": true } }));
|
|
268
|
+
expect(stub.requests[stub.requests.length - 1].state).toEqual({ "record.size": 1, "record.risky": true });
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
test("a record that is not there, and an output not read by id, are refused", async () => {
|
|
272
|
+
await expect(ask({ point: "triage", read: { record: "T-99" } })).rejects.toMatchObject({ code: "point-inputs-invalid", message: expect.stringContaining("no declared record kind holds a record with id T-99") });
|
|
273
|
+
await expect(ask({ point: "triage", read: { finding: "f-1" } })).rejects.toMatchObject({ code: "point-inputs-invalid", message: expect.stringContaining("finding is not read by id here") });
|
|
274
|
+
});
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
describe("the activity as an Op step runs it", () => {
|
|
278
|
+
test("loadActivities() registers decide with no lexicon configured", async () => {
|
|
279
|
+
const activities = await loadActivities([]);
|
|
280
|
+
expect(typeof activities.get("decide")).toBe("function");
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
test("backends come from decide.backends in the project's chant.config", async () => {
|
|
284
|
+
writeFileSync(join(root, "chant.config.json"), JSON.stringify({ decide: { backends: { systemone: { url: stub.url, key: { env: "STUB_KEY" } } } } }));
|
|
285
|
+
try {
|
|
286
|
+
script = { triage: { type: "noul", noul: 0.93 } };
|
|
287
|
+
const before = stub.requests.length;
|
|
288
|
+
const w = await waitsOn(runDecide({ cwd: root, point: "triage", inputs: { "record.size": 13, "record.risky": true } }, { env, on }));
|
|
289
|
+
expect(w.state).toBe("proposed");
|
|
290
|
+
expect(stub.requests.length).toBe(before + 1);
|
|
291
|
+
} finally {
|
|
292
|
+
rmSync(join(root, "chant.config.json"));
|
|
293
|
+
}
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
test("an unknown point is refused", async () => {
|
|
297
|
+
await expect(ask({ point: "nope" })).rejects.toMatchObject({ code: "point-unknown" });
|
|
298
|
+
expect(existsSync(join(root, "answers"))).toBe(true);
|
|
299
|
+
});
|
|
300
|
+
});
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `decide` Op activity (ws-058, #2740): ask a decision point for one set
|
|
3
|
+
* of inputs, calling the backend its model decider names, and record the
|
|
4
|
+
* answer. Core's own since #2828, next to the decision points it asks; the
|
|
5
|
+
* typed step builder is `decide` in `../builders.ts`.
|
|
6
|
+
*
|
|
7
|
+
* The point is asked through core's `askPointInRun` (#2749), which writes the
|
|
8
|
+
* answer with `askPoint` (`points ask`, #2739), the same path `chant workspace
|
|
9
|
+
* points ask --response` takes. So the chain, the threshold, a model's answer
|
|
10
|
+
* being `proposed` and never `answered`, the reuse of an answer the same point,
|
|
11
|
+
* declaration and inputs already have, the escalation to people, and what an
|
|
12
|
+
* open question does to the run all come from core:
|
|
13
|
+
*
|
|
14
|
+
* - an answered question (a table row, or a person) is the step's result, and
|
|
15
|
+
* the Op goes on;
|
|
16
|
+
* - an escalated or proposed question is open, so the step throws core's
|
|
17
|
+
* `PointWait` and the run ends `waiting`. A person answers with `points
|
|
18
|
+
* answer` or through hud, and the next run reads the answer from the record.
|
|
19
|
+
*
|
|
20
|
+
* This activity adds three things: reading the inputs through the read
|
|
21
|
+
* contract (`../decide-read-inputs.ts`), the call to a `POST /v1/systemone`
|
|
22
|
+
* backend (`../decide-backend.ts`), and refusing a misconfigured backend before
|
|
23
|
+
* anything is asked or written. The backends are the step's own `backends`, or
|
|
24
|
+
* `decide.backends` in chant.config (`../decide-config.ts`).
|
|
25
|
+
*
|
|
26
|
+
* With the backend unreachable, the point's `unreachable` decides: `escalate`
|
|
27
|
+
* (the default) records the question open for people with the reason, and
|
|
28
|
+
* `fail` writes nothing and fails the step.
|
|
29
|
+
*
|
|
30
|
+
* In a steward's turn the model call goes through the broker (#2749): core
|
|
31
|
+
* makes it only through a capability the steward declares, and a backend whose
|
|
32
|
+
* key is an environment variable is not called, since that key would be a
|
|
33
|
+
* credential the steward holds.
|
|
34
|
+
*
|
|
35
|
+
* chant's reads never call a model (ws-052): this runs only as an Op step.
|
|
36
|
+
*
|
|
37
|
+
* The workspace code, the client and the read-contract reads load on first
|
|
38
|
+
* call, so importing core's activities loads nothing under `workspace/`
|
|
39
|
+
* (#2525 rule 5).
|
|
40
|
+
*/
|
|
41
|
+
|
|
42
|
+
import { loadChantConfig } from "../../config";
|
|
43
|
+
import { askPointInRun, brokeredModelAsk, isPointWait, type BrokeredModelAsk } from "../steward-points";
|
|
44
|
+
import { currentStewardTurn } from "../steward-turn";
|
|
45
|
+
import type { QuestionView } from "../../workspace/decide";
|
|
46
|
+
import type { PointView } from "../../workspace/points-cli";
|
|
47
|
+
import type { Escalation } from "../../workspace/points";
|
|
48
|
+
import { backendSchema, type DecideBackend } from "../decide-config";
|
|
49
|
+
import type { BackendAskOptions, BackendResponse } from "../decide-backend";
|
|
50
|
+
|
|
51
|
+
export interface DecideArgs {
|
|
52
|
+
/** The point's name, as its points file declares it. */
|
|
53
|
+
point: string;
|
|
54
|
+
/** Input values by the point's input names, such as `{ "work-item.fits_small": false }`. They win over values read with `read`. */
|
|
55
|
+
inputs?: Record<string, unknown>;
|
|
56
|
+
/** What to read through the read contract, by output: `{ "work-item": "W-002" }` reads work item W-002 for every `work-item.*` input. */
|
|
57
|
+
read?: Record<string, string>;
|
|
58
|
+
/** What the question is about, such as a work item's id. Written to the answer's `constrains`. */
|
|
59
|
+
subject?: string;
|
|
60
|
+
/** The answer kind file, or a declared kind's name. Without it, the declared answer kind whose points file declares the point. */
|
|
61
|
+
kind?: string;
|
|
62
|
+
/** Where the workspace is found. Defaults to the working directory. */
|
|
63
|
+
cwd?: string;
|
|
64
|
+
/** Backends by name, in place of `decide.backends` in chant.config. */
|
|
65
|
+
backends?: Record<string, DecideBackend>;
|
|
66
|
+
/** Ask, but write nothing, and return the question whatever its state: an open one does not stop the run. */
|
|
67
|
+
dryRun?: boolean;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface DecideResult {
|
|
71
|
+
/** The answer record's id: the point's name and the first 12 hex digits of the inputs hash. */
|
|
72
|
+
id: string;
|
|
73
|
+
/** The record file, from the repository root. */
|
|
74
|
+
path: string;
|
|
75
|
+
/** `answered`, unless this was a dry run, which returns a question in any state. */
|
|
76
|
+
state: "escalated" | "proposed" | "answered";
|
|
77
|
+
open: boolean;
|
|
78
|
+
/** The answer, or null while the question is escalated. */
|
|
79
|
+
answer: string | boolean | null;
|
|
80
|
+
/** The decider that gave the recorded state: table, model (a proposal, or one a person confirmed) or quorum. */
|
|
81
|
+
decider: string;
|
|
82
|
+
/** For a model's answer: the pinned model id, the backend, its confidence and the threshold it met. */
|
|
83
|
+
model: string | null;
|
|
84
|
+
backend: string | null;
|
|
85
|
+
confidence: number | null;
|
|
86
|
+
threshold: number | null;
|
|
87
|
+
/** The people who answered, when people did. */
|
|
88
|
+
answeredBy: string[];
|
|
89
|
+
/** Each decider that was asked and did not answer, and why. */
|
|
90
|
+
escalations: Escalation[];
|
|
91
|
+
/** Declared inputs the read value did not have. */
|
|
92
|
+
missing: string[];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Seams a test swaps. */
|
|
96
|
+
export interface DecideDeps {
|
|
97
|
+
/** The HTTP call, in place of the global one. */
|
|
98
|
+
transport?: BackendAskOptions["transport"];
|
|
99
|
+
env?: NodeJS.ProcessEnv;
|
|
100
|
+
/** The date written as asked_on, YYYY-MM-DD. */
|
|
101
|
+
on?: string;
|
|
102
|
+
onResponse?: (backend: string, response: BackendResponse) => void;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** The step failed: what core refused, or a configuration this activity refuses. */
|
|
106
|
+
export class DecideError extends Error {
|
|
107
|
+
constructor(
|
|
108
|
+
readonly code: string,
|
|
109
|
+
message: string,
|
|
110
|
+
) {
|
|
111
|
+
super(message);
|
|
112
|
+
this.name = "DecideError";
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** The backends the args give, or `decide.backends` from the chant.config in `cwd`. Each is checked; a literal key is refused. */
|
|
117
|
+
async function backendsFor(args: DecideArgs, cwd: string): Promise<Record<string, DecideBackend>> {
|
|
118
|
+
const { checkKey, DecideConfigError } = await import("../decide-backend");
|
|
119
|
+
let raw: unknown = args.backends;
|
|
120
|
+
if (raw === undefined) {
|
|
121
|
+
const { config } = await loadChantConfig(cwd);
|
|
122
|
+
raw = config.decide?.backends ?? {};
|
|
123
|
+
}
|
|
124
|
+
if (raw === null || typeof raw !== "object" || Array.isArray(raw)) throw new DecideConfigError("backends must be an object of backend name to { url, key? }");
|
|
125
|
+
const out: Record<string, DecideBackend> = {};
|
|
126
|
+
for (const [name, b] of Object.entries(raw as Record<string, unknown>)) {
|
|
127
|
+
checkKey((b as { key?: unknown } | null)?.key, `backend ${name}'s key`, cwd);
|
|
128
|
+
const parsed = backendSchema.safeParse(b);
|
|
129
|
+
if (!parsed.success) throw new DecideConfigError(`backend ${name} is not { url, key?, timeoutMs? }: ${parsed.error.issues.map((i) => `${i.path.join(".") || "(backend)"} ${i.message}`).join("; ")}`);
|
|
130
|
+
out[name] = parsed.data;
|
|
131
|
+
}
|
|
132
|
+
return out;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** The point as `points --json` lists it, or a refusal. */
|
|
136
|
+
async function pointFor(args: DecideArgs, cwd: string): Promise<PointView> {
|
|
137
|
+
const { workspacePoints } = await import("../../workspace/points-cli");
|
|
138
|
+
const doc = await workspacePoints({ cwd, ...(args.kind !== undefined ? { kind: args.kind } : {}) });
|
|
139
|
+
if ("error" in doc) throw new DecideError(doc.error.code, doc.error.message);
|
|
140
|
+
const point = doc.points.find((p) => p.name === args.point);
|
|
141
|
+
if (!point) {
|
|
142
|
+
const bad = doc.sources.find((s) => s.reason);
|
|
143
|
+
throw new DecideError("point-unknown", `no points file declares ${JSON.stringify(args.point)}${bad ? ` (${bad.kind}: ${bad.reason!.message})` : doc.points.length ? ` (the points are ${doc.points.map((p) => p.name).join(", ")})` : ""}`);
|
|
144
|
+
}
|
|
145
|
+
return point;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function result(q: QuestionView, missing: string[]): DecideResult {
|
|
149
|
+
const decider = typeof q.decider.kind === "string" ? q.decider.kind : "quorum";
|
|
150
|
+
const byModel = decider === "model";
|
|
151
|
+
return {
|
|
152
|
+
id: q.id,
|
|
153
|
+
path: q.path,
|
|
154
|
+
state: q.state,
|
|
155
|
+
open: q.open,
|
|
156
|
+
answer: q.answer,
|
|
157
|
+
decider,
|
|
158
|
+
model: byModel ? (q.model?.model ?? null) : null,
|
|
159
|
+
backend: byModel ? (q.model?.backend ?? null) : null,
|
|
160
|
+
confidence: byModel ? q.confidence : null,
|
|
161
|
+
threshold: byModel ? q.threshold : null,
|
|
162
|
+
answeredBy: q.answeredBy,
|
|
163
|
+
escalations: q.escalations,
|
|
164
|
+
missing,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** Run the activity with its seams. {@link decide} is this with the real ones. */
|
|
169
|
+
export async function runDecide(args: DecideArgs, deps: DecideDeps = {}, signal?: AbortSignal): Promise<DecideResult> {
|
|
170
|
+
if (typeof args?.point !== "string" || args.point === "") throw new DecideError("write-usage-invalid", "decide needs the point's name");
|
|
171
|
+
const cwd = args.cwd ?? process.cwd();
|
|
172
|
+
const { backendAsk, brokeredCapability, checkKey, DecideConfigError } = await import("../decide-backend");
|
|
173
|
+
const configError = <T>(f: () => T): T => {
|
|
174
|
+
try {
|
|
175
|
+
return f();
|
|
176
|
+
} catch (err) {
|
|
177
|
+
if (err instanceof DecideConfigError) throw new DecideError("backend-invalid", err.message);
|
|
178
|
+
throw err;
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
let backends: Record<string, DecideBackend>;
|
|
182
|
+
try {
|
|
183
|
+
backends = await backendsFor(args, cwd);
|
|
184
|
+
} catch (err) {
|
|
185
|
+
if (err instanceof DecideConfigError) throw new DecideError("backend-invalid", err.message);
|
|
186
|
+
throw err;
|
|
187
|
+
}
|
|
188
|
+
const point = await pointFor(args, cwd);
|
|
189
|
+
|
|
190
|
+
// Every model decider's backend is configured, and a brokered key's
|
|
191
|
+
// capability is declared with a broker, before anything is asked. The first
|
|
192
|
+
// brokered one names the capability a steward's call goes through.
|
|
193
|
+
let via: { capability: string; broker: string } | undefined;
|
|
194
|
+
for (const d of point.deciders) {
|
|
195
|
+
if (d.kind !== "model") continue;
|
|
196
|
+
const backend = backends[d.backend];
|
|
197
|
+
if (!backend) {
|
|
198
|
+
throw new DecideError("backend-invalid", `${args.point}'s model decider names the backend ${d.backend}, and none is configured (decide.backends${Object.keys(backends).length ? `: ${Object.keys(backends).join(", ")}` : " is empty"})`);
|
|
199
|
+
}
|
|
200
|
+
configError(() => checkKey(backend.key, `backend ${d.backend}'s key`, cwd));
|
|
201
|
+
if (via === undefined && backend.key && "capability" in backend.key) {
|
|
202
|
+
const key = backend.key;
|
|
203
|
+
via = { capability: key.capability, broker: configError(() => brokeredCapability(key, cwd)).broker };
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
let read = { inputs: {} as Record<string, unknown>, missing: [] as string[] };
|
|
208
|
+
if (args.read && Object.keys(args.read).length > 0) {
|
|
209
|
+
try {
|
|
210
|
+
const { readInputs } = await import("../decide-read-inputs");
|
|
211
|
+
read = await readInputs(point.inputs.map((i) => i.name), args.read, cwd);
|
|
212
|
+
} catch (err) {
|
|
213
|
+
throw new DecideError("point-inputs-invalid", err instanceof Error ? err.message : String(err));
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
const inputs = { ...read.inputs, ...(args.inputs ?? {}) };
|
|
217
|
+
const missing = read.missing.filter((m) => !(m in inputs));
|
|
218
|
+
|
|
219
|
+
const call = backendAsk({ backends, cwd, ...(deps.transport ? { transport: deps.transport } : {}), ...(deps.env ? { env: deps.env } : {}), ...(signal ? { signal } : {}), ...(deps.onResponse ? { onResponse: deps.onResponse } : {}) });
|
|
220
|
+
const turn = currentStewardTurn();
|
|
221
|
+
const ask: BrokeredModelAsk = async (request) => {
|
|
222
|
+
const key = backends[request.backend]?.key;
|
|
223
|
+
if (turn && key && !("capability" in key)) {
|
|
224
|
+
throw new Error(`in the steward ${turn.steward}'s turn a model call goes through the broker, and backend ${request.backend}'s key is the environment variable ${key.env}, a credential the steward would hold`);
|
|
225
|
+
}
|
|
226
|
+
return call(request);
|
|
227
|
+
};
|
|
228
|
+
const common = {
|
|
229
|
+
cwd,
|
|
230
|
+
point: args.point,
|
|
231
|
+
inputs,
|
|
232
|
+
...(args.subject !== undefined ? { subject: args.subject } : {}),
|
|
233
|
+
...(args.kind !== undefined ? { kind: args.kind } : {}),
|
|
234
|
+
...(deps.on ? { on: deps.on } : {}),
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
if (args.dryRun) {
|
|
238
|
+
const { askPoint } = await import("../../workspace/decide");
|
|
239
|
+
const doc = await askPoint({
|
|
240
|
+
...common,
|
|
241
|
+
ask: await brokeredModelAsk(ask, { ...(via ? via : {}), turn: turn ?? null, cwd }),
|
|
242
|
+
...(turn ? { steward: { name: turn.steward, ...(turn.run ? { run: turn.run } : {}) } } : { client: { name: "chant decide" } }),
|
|
243
|
+
dryRun: true,
|
|
244
|
+
});
|
|
245
|
+
if ("error" in doc) throw new DecideError(doc.error.code, `${doc.error.code}: ${doc.error.message}`);
|
|
246
|
+
return result(doc.question, missing);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
try {
|
|
250
|
+
const { question } = await askPointInRun({ ...common, ask, ...(via ? { capability: via.capability, broker: via.broker } : {}), turn: turn ?? null });
|
|
251
|
+
return result(question, missing);
|
|
252
|
+
} catch (err) {
|
|
253
|
+
// An open question is not a failure: the run waits on it (#2749).
|
|
254
|
+
if (isPointWait(err)) throw err;
|
|
255
|
+
const coded = err instanceof Error ? /^decision point [^:]+: ([a-z0-9-]+): /.exec(err.message) : null;
|
|
256
|
+
if (coded) throw new DecideError(coded[1], err instanceof Error ? err.message : String(err));
|
|
257
|
+
throw err;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Ask a decision point and record the answer. The Op activity: an answered
|
|
263
|
+
* question is the step's result, and an open one stops the run `waiting`.
|
|
264
|
+
*/
|
|
265
|
+
export async function decide(args: DecideArgs, signal?: AbortSignal): Promise<DecideResult> {
|
|
266
|
+
return runDecide(args, {}, signal);
|
|
267
|
+
}
|
|
@@ -17,6 +17,11 @@ export { shellCmd } from "./shell";
|
|
|
17
17
|
export type { ShellCmdArgs, ShellCmdResult } from "./shell";
|
|
18
18
|
|
|
19
19
|
export { httpCheck, statusOk } from "./http-check";
|
|
20
|
+
|
|
21
|
+
export { sourceArchive, releasePlan, releaseRecord } from "./source-release";
|
|
22
|
+
export type { SourceArchiveArgs, SourceArchiveResult, ReleasePlanArgs, ReleasePlanResult, ReleaseRecordArgs, ReleaseRecordResult } from "./source-release";
|
|
23
|
+
export { releaseRollbackPlan, releaseRollbackRecord } from "./source-rollback";
|
|
24
|
+
export type { ReleaseRollbackPlanArgs, ReleaseRollbackPlanResult, ReleaseRollbackRecordArgs, ReleaseRollbackRecordResult } from "./source-rollback";
|
|
20
25
|
export type { HttpCheckArgs, HttpFetch } from "./http-check";
|
|
21
26
|
|
|
22
27
|
export { lifecycleSnapshot, lifecycleDiff } from "./lifecycle";
|
|
@@ -78,6 +83,9 @@ export type { PolicyGateArgs } from "./policy";
|
|
|
78
83
|
export { guardValidate, parseGuardFindings } from "./guard-validate";
|
|
79
84
|
export type { GuardValidateArgs, GuardValidateResult, GuardFinding } from "./guard-validate";
|
|
80
85
|
|
|
86
|
+
export { workEvidence } from "./work-evidence";
|
|
87
|
+
export type { WorkEvidenceArgs, WorkEvidenceResult } from "./work-evidence";
|
|
88
|
+
|
|
81
89
|
export { workflowSupplyChainAudit, collectAuditRefs, defaultActionRefResolver } from "./workflow-audit";
|
|
82
90
|
export type {
|
|
83
91
|
WorkflowAuditArgs,
|
|
@@ -157,3 +165,16 @@ export type {
|
|
|
157
165
|
ProposeUpgradeMode,
|
|
158
166
|
CommandRunner,
|
|
159
167
|
} from "./propose-upgrade";
|
|
168
|
+
|
|
169
|
+
// The forward coverage check over an Op's own diff (#2773). Loads the
|
|
170
|
+
// workspace code on first call only, like proposeWorkspaceUpgrade.
|
|
171
|
+
export { changeCoverage } from "./change-coverage";
|
|
172
|
+
export type { ChangeCoverageArgs, ChangeCoverageResult } from "./change-coverage";
|
|
173
|
+
|
|
174
|
+
// Ask a decision point and record the answer (ws-058, #2740). Core's since
|
|
175
|
+
// #2828, next to the decision points: the systemone lexicon that held it is
|
|
176
|
+
// gone. Only the activity is re-exported, since the registry collects every
|
|
177
|
+
// exported function; `runDecide` and its seams stay on the module, which
|
|
178
|
+
// loads the workspace code on first call.
|
|
179
|
+
export { decide } from "./decide";
|
|
180
|
+
export type { DecideArgs, DecideResult } from "./decide";
|