@intentius/chant 0.91.0 → 0.94.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/graph.d.ts.map +1 -1
- package/dist/cli/handlers/operator.d.ts +6 -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-plugins.d.ts +6 -6
- package/dist/cli/mcp/workspace-tools.d.ts +2 -1
- 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 +12 -2
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/codegen/pinned-upgrade.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/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/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 +201 -0
- 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/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 +11 -4
- package/dist/op/index.d.ts.map +1 -1
- package/dist/op/local-executor.d.ts +29 -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 +2 -0
- package/dist/op/op-ir.d.ts.map +1 -1
- package/dist/op/op-verb-class.d.ts.map +1 -1
- package/dist/op/operator.d.ts +97 -1
- package/dist/op/operator.d.ts.map +1 -1
- package/dist/op/runtime.d.ts +37 -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/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 +22 -1
- package/dist/op/steward.d.ts.map +1 -1
- package/dist/op/types.d.ts +7 -0
- package/dist/op/types.d.ts.map +1 -1
- package/dist/op/work-lease-run.d.ts +1 -1
- package/dist/op/work-lease-run.d.ts.map +1 -1
- 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-intent.d.ts +85 -0
- package/dist/workspace/box-intent.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 +104 -0
- package/dist/workspace/chant-migrations.d.ts.map +1 -0
- package/dist/workspace/checks/boxes.d.ts +47 -1
- package/dist/workspace/checks/boxes.d.ts.map +1 -1
- 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 +7 -0
- package/dist/workspace/checks/records.d.ts.map +1 -1
- package/dist/workspace/checks.d.ts +7 -0
- 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 +49 -4
- package/dist/workspace/decide.d.ts.map +1 -1
- package/dist/workspace/decision-points.schema.json +3 -3
- package/dist/workspace/declaration.d.ts +58 -0
- package/dist/workspace/declaration.d.ts.map +1 -1
- package/dist/workspace/declaration.schema.json +119 -1
- package/dist/workspace/declared-kinds.d.ts +20 -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-joins.d.ts +5 -5
- 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 +2 -0
- package/dist/workspace/points-cli.d.ts.map +1 -1
- package/dist/workspace/points.d.ts +4 -6
- package/dist/workspace/points.d.ts.map +1 -1
- package/dist/workspace/reason-codes.d.ts +16 -2
- package/dist/workspace/reason-codes.d.ts.map +1 -1
- package/dist/workspace/records-cli.d.ts +11 -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 +34 -4
- 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 +65 -1
- package/dist/workspace/status-stewards.d.ts.map +1 -1
- package/dist/workspace/status.d.ts +24 -0
- package/dist/workspace/status.d.ts.map +1 -1
- package/dist/workspace/work-cli.d.ts +113 -2
- package/dist/workspace/work-cli.d.ts.map +1 -1
- 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/graph.test.ts +20 -0
- package/src/cli/handlers/graph.ts +16 -2
- package/src/cli/handlers/operator.ts +34 -2
- package/src/cli/handlers/run.test.ts +154 -2
- package/src/cli/handlers/run.ts +121 -2
- package/src/cli/main.test.ts +10 -0
- package/src/cli/main.ts +23 -3
- package/src/cli/mcp/workspace-plugins.ts +6 -6
- package/src/cli/mcp/workspace-tools.test.ts +35 -19
- package/src/cli/mcp/workspace-tools.ts +62 -4
- package/src/cli/plugins.ts +11 -3
- package/src/cli/registry.ts +12 -2
- package/src/cli/serve-mcp-workspace.test.ts +6 -6
- 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/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/run-ledger.ts +6 -0
- package/src/lifecycle/symptoms.ts +95 -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 +308 -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 +119 -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/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 +18 -3
- package/src/op/local-executor.ts +151 -15
- package/src/op/local-output.test.ts +33 -0
- package/src/op/local-output.ts +20 -2
- package/src/op/op-ir.test.ts +10 -0
- package/src/op/op-ir.ts +4 -0
- package/src/op/op-verb-class.ts +1 -0
- package/src/op/operator.ts +290 -11
- package/src/op/runtime.ts +31 -8
- package/src/op/runtimes/local.ts +3 -1
- package/src/op/source-archive.ts +161 -0
- package/src/op/steward-points.test.ts +364 -0
- package/src/op/steward-points.ts +194 -0
- package/src/op/steward-turn.ts +79 -0
- package/src/op/steward.test.ts +87 -3
- package/src/op/steward.ts +25 -1
- package/src/op/types.ts +7 -0
- package/src/op/work-lease-run.test.ts +20 -1
- package/src/op/work-lease-run.ts +2 -2
- 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-intent.test.ts +205 -0
- package/src/workspace/box-intent.ts +159 -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 +169 -0
- package/src/workspace/check-contract.test.ts +4 -2
- package/src/workspace/check.schema.json +19 -5
- package/src/workspace/checks/boxes.test.ts +82 -2
- package/src/workspace/checks/boxes.ts +202 -1
- package/src/workspace/checks/diagrams.test.ts +134 -0
- package/src/workspace/checks/diagrams.ts +108 -0
- package/src/workspace/checks/records.ts +47 -0
- package/src/workspace/checks.ts +20 -2
- 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 +1 -1
- package/src/workspace/decide.ts +156 -23
- package/src/workspace/decision-points.schema.json +3 -3
- package/src/workspace/declaration.schema.json +119 -1
- package/src/workspace/declaration.test.ts +58 -0
- package/src/workspace/declaration.ts +112 -0
- package/src/workspace/declared-kinds.test.ts +31 -5
- package/src/workspace/declared-kinds.ts +46 -1
- 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-joins.test.ts +5 -5
- package/src/workspace/intent-joins.ts +5 -5
- package/src/workspace/intent.schema.json +8 -2
- 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/points-cli.ts +17 -3
- package/src/workspace/points-write.schema.json +11 -1
- package/src/workspace/points.schema.json +51 -0
- package/src/workspace/points.test.ts +16 -15
- package/src/workspace/points.ts +5 -17
- package/src/workspace/read-contract.test.ts +9 -1
- package/src/workspace/reason-codes.test.ts +18 -8
- package/src/workspace/reason-codes.ts +21 -2
- package/src/workspace/records-amend.schema.json +2 -0
- package/src/workspace/records-cli.ts +27 -4
- package/src/workspace/records-close.schema.json +2 -0
- package/src/workspace/records-formats.test.ts +15 -15
- 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-since.test.ts +8 -7
- 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 +36 -2
- package/src/workspace/records.test.ts +49 -4
- package/src/workspace/records.ts +109 -3
- package/src/workspace/status-contract.test.ts +14 -8
- package/src/workspace/status-gates.ts +9 -4
- package/src/workspace/status-stewards.ts +155 -5
- package/src/workspace/status.schema.json +129 -8
- package/src/workspace/status.ts +46 -1
- package/src/workspace/work-acceptance.test.ts +274 -0
- package/src/workspace/{work-cli.test.ts → work-cli.e2e.test.ts} +1 -1
- package/src/workspace/work-cli.ts +193 -10
- 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-readiness-chud.test.ts → work-readiness.test.ts} +30 -32
- package/src/workspace/work.test.ts +20 -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
package/src/workspace/decide.ts
CHANGED
|
@@ -66,6 +66,10 @@ import { policyAtBase, resolveBase } from "./trust/provenance";
|
|
|
66
66
|
import { emptyPolicy, type TrustPolicy } from "./trust/policy";
|
|
67
67
|
import { AGENT_ROLE } from "./records-cli";
|
|
68
68
|
import type { SourceVia } from "./source-block";
|
|
69
|
+
import type { RecordSource } from "./record-source";
|
|
70
|
+
import { currentStewardTurn } from "../op/steward-turn";
|
|
71
|
+
import { RefCASConflictError } from "../lifecycle/git";
|
|
72
|
+
import { readLedgerAnswers, refreshLedger, withLedgerAnswers, writeLedgerAnswer, type LedgerAnswers } from "./answers-ledger";
|
|
69
73
|
|
|
70
74
|
/** The version of the write documents `points ask` and `points answer` print. */
|
|
71
75
|
export const POINTS_WRITE_CONTRACT_VERSION = 1;
|
|
@@ -93,6 +97,8 @@ export const POINTS_WRITE_ERROR_CODES = [
|
|
|
93
97
|
"record-not-found",
|
|
94
98
|
"record-closed",
|
|
95
99
|
"record-id-taken",
|
|
100
|
+
/** The answer was given during a steward's turn (#2749): a steward never answers a question, its own or another's. */
|
|
101
|
+
"answer-in-steward-turn",
|
|
96
102
|
...RECORD_REASON_CODES,
|
|
97
103
|
] as const satisfies readonly ReasonCode[];
|
|
98
104
|
export type PointsWriteErrorCode = (typeof POINTS_WRITE_ERROR_CODES)[number];
|
|
@@ -141,12 +147,47 @@ export interface QuestionView {
|
|
|
141
147
|
answeredBy: string[];
|
|
142
148
|
askedOn: string | null;
|
|
143
149
|
answeredOn: string | null;
|
|
150
|
+
/**
|
|
151
|
+
* The steward whose turn asked the question (#2749), and the run it was in,
|
|
152
|
+
* or null when no steward asked it. The steward waits on the question and
|
|
153
|
+
* never answers it: a person does, through hud or `points answer`.
|
|
154
|
+
*/
|
|
155
|
+
askedBy: { steward: string; run: string | null } | null;
|
|
156
|
+
/**
|
|
157
|
+
* Where the record is held when it is on the lifecycle ledger rather than
|
|
158
|
+
* in the working tree (#2786): `chant/lifecycle:<path>`, which `git show`
|
|
159
|
+
* reads. A question asked in a steward's turn is held there, since a
|
|
160
|
+
* steward never writes the checkout. null for a record in the tree; `path`
|
|
161
|
+
* is where the record reads as being either way.
|
|
162
|
+
*/
|
|
163
|
+
ledger: string | null;
|
|
144
164
|
valid: boolean;
|
|
145
165
|
warnings: RecordWarning[];
|
|
146
166
|
}
|
|
147
167
|
|
|
148
|
-
/**
|
|
149
|
-
export
|
|
168
|
+
/** The harness a steward's question names in its source block (#2749). */
|
|
169
|
+
export const STEWARD_HARNESS = "chant-steward";
|
|
170
|
+
|
|
171
|
+
/** The steward that asked, from a record's source block, or null. */
|
|
172
|
+
export function askedByOf(data: Record<string, unknown>): QuestionView["askedBy"] {
|
|
173
|
+
const source = data.source;
|
|
174
|
+
if (source === null || typeof source !== "object" || Array.isArray(source)) return null;
|
|
175
|
+
const s = source as Record<string, unknown>;
|
|
176
|
+
if (s.harness !== STEWARD_HARNESS) return null;
|
|
177
|
+
const client = s.client as Record<string, unknown> | undefined;
|
|
178
|
+
const steward = client && typeof client.name === "string" ? client.name : null;
|
|
179
|
+
if (steward === null) return null;
|
|
180
|
+
const session = s.session as Record<string, unknown> | string | undefined;
|
|
181
|
+
const run = typeof session === "string" ? session : session && typeof session === "object" && typeof session.id === "string" ? session.id : null;
|
|
182
|
+
return { steward, run };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/**
|
|
186
|
+
* One answer record as a {@link QuestionView}, or null when its front matter
|
|
187
|
+
* can't be read. `point` is its point as declared now, when there is one, and
|
|
188
|
+
* `ledger` where the record is held when it is on the lifecycle ledger.
|
|
189
|
+
*/
|
|
190
|
+
export function questionView(entry: RecordEntry, point: Point | undefined, ledger: string | null = null): QuestionView | null {
|
|
150
191
|
const d = entry.data;
|
|
151
192
|
if (d === null || entry.id === null) return null;
|
|
152
193
|
const str = (v: unknown): string | null => (typeof v === "string" ? v : null);
|
|
@@ -187,6 +228,8 @@ export function questionView(entry: RecordEntry, point: Point | undefined): Ques
|
|
|
187
228
|
answeredBy: Array.isArray(d.answered_by) ? d.answered_by.filter((b): b is string => typeof b === "string") : [],
|
|
188
229
|
askedOn: str(d.asked_on),
|
|
189
230
|
answeredOn: str(d.answered_on),
|
|
231
|
+
askedBy: askedByOf(d),
|
|
232
|
+
ledger,
|
|
190
233
|
valid: entry.valid,
|
|
191
234
|
warnings: entry.warnings,
|
|
192
235
|
};
|
|
@@ -276,6 +319,21 @@ export interface AskPointOptions {
|
|
|
276
319
|
via?: SourceVia;
|
|
277
320
|
/** The client that asked, for its source block. */
|
|
278
321
|
client?: { name: string; version?: string };
|
|
322
|
+
/**
|
|
323
|
+
* The steward whose turn asks (#2749), and its run. Recorded in the source
|
|
324
|
+
* block (harness `chant-steward`, the steward as the client, the run as the
|
|
325
|
+
* session), so the question says who is waiting on it and `points answer`
|
|
326
|
+
* never counts the steward toward its quorum. Takes the place of `client`.
|
|
327
|
+
*/
|
|
328
|
+
steward?: { name: string; run?: string };
|
|
329
|
+
/**
|
|
330
|
+
* Where a new record goes: the answer kind's directory in the working tree,
|
|
331
|
+
* or the lifecycle ledger (#2786). Defaults to the ledger in a steward's
|
|
332
|
+
* turn (`steward` given, or this process a steward's turn), since a steward
|
|
333
|
+
* never writes the checkout, and to the tree otherwise. A question already
|
|
334
|
+
* held on the ledger is rewritten there whatever this says.
|
|
335
|
+
*/
|
|
336
|
+
store?: "tree" | "ledger";
|
|
279
337
|
/** The date written as asked_on, and answered_on for a table's answer, YYYY-MM-DD. Defaults to today, in UTC. */
|
|
280
338
|
on?: string;
|
|
281
339
|
dryRun?: boolean;
|
|
@@ -319,9 +377,34 @@ function checkInputs(name: string, point: Point, inputs: unknown): Record<string
|
|
|
319
377
|
return inputs as Record<string, unknown>;
|
|
320
378
|
}
|
|
321
379
|
|
|
322
|
-
/**
|
|
323
|
-
|
|
324
|
-
|
|
380
|
+
/** Where a write goes: the working tree, or the lifecycle ledger (#2786). */
|
|
381
|
+
interface WriteTarget {
|
|
382
|
+
/** The records as read: the tree with the ledger's answers laid over it. */
|
|
383
|
+
source: RecordSource;
|
|
384
|
+
ledger: LedgerAnswers;
|
|
385
|
+
/** Write to the ledger rather than the tree. */
|
|
386
|
+
onLedger: boolean;
|
|
387
|
+
message: string;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Write `text` as the record at `path`, after reading it back with every
|
|
392
|
+
* other record: into the working tree, or onto the ledger. Returns the
|
|
393
|
+
* record's warnings and, for a ledger write, where it went.
|
|
394
|
+
*/
|
|
395
|
+
async function write(o: Opened, before: RecordEntry[], path: string, text: string, create: boolean, dryRun: boolean, target: WriteTarget): Promise<{ warnings: RecordWarning[]; ledger: string | null }> {
|
|
396
|
+
const warnings = await validateWrite(o, before, path, text, target.source);
|
|
397
|
+
const held = target.ledger.byPath.get(path);
|
|
398
|
+
if (target.onLedger) {
|
|
399
|
+
if (dryRun) return { warnings, ledger: held?.ledger ?? null };
|
|
400
|
+
try {
|
|
401
|
+
const { ledger } = await writeLedgerAnswer(target.ledger, path, text, target.message, held?.sha ?? null);
|
|
402
|
+
return { warnings, ledger };
|
|
403
|
+
} catch (err) {
|
|
404
|
+
if (err instanceof RefCASConflictError) throw new PointsWriteError("record-id-taken", `${path} was written on chant/lifecycle by another ask or answer at the same time: ask again to read it`);
|
|
405
|
+
throw err;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
325
408
|
if (!dryRun) {
|
|
326
409
|
try {
|
|
327
410
|
writeFileSync(abs(o, path), text, create ? { flag: "wx" } : undefined);
|
|
@@ -330,7 +413,14 @@ async function write(o: Opened, before: RecordEntry[], path: string, text: strin
|
|
|
330
413
|
throw err;
|
|
331
414
|
}
|
|
332
415
|
}
|
|
333
|
-
return warnings;
|
|
416
|
+
return { warnings, ledger: null };
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/** The kind's answers on the ledger, and its records read with them laid over the tree. */
|
|
420
|
+
async function readWithLedger(o: Opened): Promise<{ ledger: LedgerAnswers; source: RecordSource; before: RecordEntry[] }> {
|
|
421
|
+
const ledger = await readLedgerAnswers({ file: o.loaded.file, name: o.loaded.kind.name, dirRel: o.dirRel });
|
|
422
|
+
const source = withLedgerAnswers(o.source, ledger);
|
|
423
|
+
return { ledger, source, before: await readAll(o, source) };
|
|
334
424
|
}
|
|
335
425
|
|
|
336
426
|
function recordData(o: Opened, fields: Record<string, unknown>): Record<string, unknown> {
|
|
@@ -355,15 +445,21 @@ function resultFields(result: ChainResult): Record<string, unknown> {
|
|
|
355
445
|
*/
|
|
356
446
|
export async function askPoint(opts: AskPointOptions): Promise<PointsWriteDocument> {
|
|
357
447
|
try {
|
|
448
|
+
// A steward's turn (#2749): named by the caller, or this process's, as when an Op shells out to `points ask`.
|
|
449
|
+
const turn = opts.steward ? undefined : currentStewardTurn();
|
|
450
|
+
const steward = opts.steward ?? (turn ? { name: turn.steward, ...(turn.run ? { run: turn.run } : {}) } : undefined);
|
|
451
|
+
const toLedger = opts.store !== undefined ? opts.store === "ledger" : steward !== undefined;
|
|
358
452
|
const kinds = await answerKindFiles(opts.cwd, opts.kind);
|
|
359
453
|
const { o, point } = await findPoint(kinds, opts.point, opts.cwd);
|
|
360
454
|
const inputs = checkInputs(opts.point, point, opts.inputs);
|
|
361
455
|
const version = pointVersion(point);
|
|
362
456
|
const hash = inputsHash(opts.point, version, inputs);
|
|
363
457
|
const id = answerId(opts.point, hash);
|
|
364
|
-
|
|
458
|
+
if (toLedger && !opts.dryRun) await refreshLedger(o.loaded.file);
|
|
459
|
+
const { ledger, source: read, before } = await readWithLedger(o);
|
|
365
460
|
const existing = before.find((e) => e.id === id);
|
|
366
|
-
const
|
|
461
|
+
const heldAt = (entry: RecordEntry): string | null => ledger.byPath.get(entry.path)?.ledger ?? null;
|
|
462
|
+
const done = (entry: RecordEntry, extra: { reused: boolean; written: boolean; text?: string; ledger?: string | null }): PointsWriteDocument => ({
|
|
367
463
|
$schema: POINTS_WRITE_SCHEMA_ID,
|
|
368
464
|
contract: POINTS_WRITE_CONTRACT_VERSION,
|
|
369
465
|
verb: "ask",
|
|
@@ -373,7 +469,7 @@ export async function askPoint(opts: AskPointOptions): Promise<PointsWriteDocume
|
|
|
373
469
|
reused: extra.reused,
|
|
374
470
|
written: extra.written,
|
|
375
471
|
dryRun: !!opts.dryRun,
|
|
376
|
-
question: questionView(entry, point)!,
|
|
472
|
+
question: questionView(entry, point, extra.ledger !== undefined ? extra.ledger : heldAt(entry))!,
|
|
377
473
|
...(extra.text !== undefined ? { text: extra.text } : {}),
|
|
378
474
|
});
|
|
379
475
|
if (existing && existing.data !== null && (existing.state === "answered" || existing.state === "proposed")) return done(existing, { reused: true, written: false });
|
|
@@ -393,7 +489,14 @@ export async function askPoint(opts: AskPointOptions): Promise<PointsWriteDocume
|
|
|
393
489
|
const answer = result.status === "escalated" ? undefined : result.answer;
|
|
394
490
|
const title = titleFor(point, opts.subject, state, answer);
|
|
395
491
|
const modelId = result.status === "proposed" ? result.decider.model : undefined;
|
|
396
|
-
const
|
|
492
|
+
const client = steward ? { name: steward.name } : opts.client;
|
|
493
|
+
const source = {
|
|
494
|
+
via: opts.via ?? "cli",
|
|
495
|
+
...(steward ? { harness: STEWARD_HARNESS } : {}),
|
|
496
|
+
...(client ? { client } : {}),
|
|
497
|
+
...(modelId ? { model: modelId } : {}),
|
|
498
|
+
...(steward?.run ? { session: { id: steward.run } } : {}),
|
|
499
|
+
};
|
|
397
500
|
const data = recordData(o, {
|
|
398
501
|
id,
|
|
399
502
|
title,
|
|
@@ -411,16 +514,19 @@ export async function askPoint(opts: AskPointOptions): Promise<PointsWriteDocume
|
|
|
411
514
|
});
|
|
412
515
|
const path = existing ? existing.path : o.dirRel === "." ? `${id}.md` : `${o.dirRel}/${id}.md`;
|
|
413
516
|
const text = renderRecord(data, body(point, title));
|
|
414
|
-
|
|
415
|
-
const
|
|
416
|
-
|
|
517
|
+
// A steward's question goes on the ledger, and one already held there stays there (#2786).
|
|
518
|
+
const onLedger = toLedger || ledger.byPath.has(path);
|
|
519
|
+
const message = `Decision point ${opts.point}: ${state}${opts.subject ? ` (${opts.subject})` : ""}${steward ? `, asked by ${steward.name}` : ""}`;
|
|
520
|
+
const wrote = await write(o, before, path, text, !existing, !!opts.dryRun, { source: read, ledger, onLedger, message });
|
|
521
|
+
const entry: RecordEntry = { ...(existing ?? emptyEntry(path)), id, path, state, data, valid: true, reasons: [], warnings: wrote.warnings };
|
|
522
|
+
return done(entry, { reused: false, written: !opts.dryRun, ledger: wrote.ledger, ...(opts.dryRun ? { text } : {}) });
|
|
417
523
|
} catch (err) {
|
|
418
524
|
return failure("ask", err);
|
|
419
525
|
}
|
|
420
526
|
}
|
|
421
527
|
|
|
422
528
|
function emptyEntry(path: string): RecordEntry {
|
|
423
|
-
return { id: null, path, state: null, valid: true, reasons: [], supersededBy: null, data: null, assets: [], warnings: [], digest: "" };
|
|
529
|
+
return { id: null, path, state: null, valid: true, reasons: [], supersededBy: null, remediatedBy: [], data: null, assets: [], warnings: [], digest: "" };
|
|
424
530
|
}
|
|
425
531
|
|
|
426
532
|
// ── points answer ────────────────────────────────────────────────────────────
|
|
@@ -453,9 +559,16 @@ function policyFor(root: string): TrustPolicy {
|
|
|
453
559
|
* leaving out anyone holding the agent role in the trust policy at base, and,
|
|
454
560
|
* when the quorum names roles, anyone holding none of them.
|
|
455
561
|
*/
|
|
456
|
-
export function tallyQuorum(
|
|
562
|
+
export function tallyQuorum(
|
|
563
|
+
by: string[],
|
|
564
|
+
quorum: { count: number; roles?: string[] },
|
|
565
|
+
policy: TrustPolicy,
|
|
566
|
+
/** The steward that asked the question (#2749): it never counts toward the answer. */
|
|
567
|
+
steward?: string,
|
|
568
|
+
): { counted: string[]; left: { name: string; why: string }[]; met: boolean } {
|
|
457
569
|
const holders = (role: string) => new Set((policy.roles[role] ?? []).map(normalisePrincipal));
|
|
458
570
|
const agents = holders(AGENT_ROLE);
|
|
571
|
+
const asker = steward !== undefined ? normalisePrincipal(steward) : undefined;
|
|
459
572
|
const counted: string[] = [];
|
|
460
573
|
const seen = new Set<string>();
|
|
461
574
|
const left: { name: string; why: string }[] = [];
|
|
@@ -463,6 +576,10 @@ export function tallyQuorum(by: string[], quorum: { count: number; roles?: strin
|
|
|
463
576
|
const p = normalisePrincipal(name);
|
|
464
577
|
if (p === "" || seen.has(p)) continue;
|
|
465
578
|
seen.add(p);
|
|
579
|
+
if (asker !== undefined && p === asker) {
|
|
580
|
+
left.push({ name, why: "is the steward that asked" });
|
|
581
|
+
continue;
|
|
582
|
+
}
|
|
466
583
|
if (agents.has(p)) {
|
|
467
584
|
left.push({ name, why: "holds the agent role" });
|
|
468
585
|
continue;
|
|
@@ -484,20 +601,34 @@ export function tallyQuorum(by: string[], quorum: { count: number; roles?: strin
|
|
|
484
601
|
*/
|
|
485
602
|
export async function answerPoint(opts: AnswerPointOptions): Promise<PointsWriteDocument> {
|
|
486
603
|
try {
|
|
604
|
+
// A steward waits on a question and never answers one (#2749), as it never
|
|
605
|
+
// clears a gate: the answer has to come from a person, through hud or a
|
|
606
|
+
// shell, not from the steward's own turn or a process it started.
|
|
607
|
+
const turn = currentStewardTurn();
|
|
608
|
+
if (turn) {
|
|
609
|
+
throw new PointsWriteError(
|
|
610
|
+
"answer-in-steward-turn",
|
|
611
|
+
`this is the steward ${turn.steward}'s turn, and a steward never answers a decision point: a person answers ${opts.id} through hud or \`chant workspace points answer\` at a shell`,
|
|
612
|
+
);
|
|
613
|
+
}
|
|
487
614
|
const kinds = await answerKindFiles(opts.cwd, opts.kind);
|
|
488
615
|
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");
|
|
489
|
-
let found:
|
|
616
|
+
let found:
|
|
617
|
+
| { opened: OpenedPoints; before: RecordEntry[]; read: Awaited<ReturnType<typeof readWithLedger>>; target: RecordEntry & { data: Record<string, unknown> } }
|
|
618
|
+
| undefined;
|
|
490
619
|
for (const k of kinds) {
|
|
491
620
|
const opened = await openAnswers(k, opts.cwd);
|
|
492
|
-
|
|
493
|
-
const
|
|
621
|
+
// A question a steward asked is on the ledger (#2786), and people's answer goes back there.
|
|
622
|
+
const read = await readWithLedger(opened.o);
|
|
623
|
+
const target = read.before.find((e) => e.id === opts.id);
|
|
494
624
|
if (target && target.data !== null) {
|
|
495
|
-
found = { opened, before, target: target as RecordEntry & { data: Record<string, unknown> } };
|
|
625
|
+
found = { opened, before: read.before, read, target: target as RecordEntry & { data: Record<string, unknown> } };
|
|
496
626
|
break;
|
|
497
627
|
}
|
|
498
628
|
}
|
|
499
629
|
if (!found) throw new PointsWriteError("record-not-found", `no answer record has id ${opts.id}`);
|
|
500
630
|
const { opened, before, target } = found;
|
|
631
|
+
const ledger = found.read.ledger;
|
|
501
632
|
const { o } = opened;
|
|
502
633
|
const d = target.data;
|
|
503
634
|
if (target.state === "answered") throw new PointsWriteError("record-closed", `${opts.id} is answered, and an answer never changes: ask again with other inputs, or change the point, which asks the question anew`);
|
|
@@ -511,7 +642,7 @@ export async function answerPoint(opts: AnswerPointOptions): Promise<PointsWrite
|
|
|
511
642
|
throw new PointsWriteError("answer-not-candidate", `${opts.id} takes one of ${allowed.map((c) => JSON.stringify(c)).join(", ")}, not ${JSON.stringify(opts.answer)}`);
|
|
512
643
|
}
|
|
513
644
|
const quorum = quorumOf(point);
|
|
514
|
-
const tally = tallyQuorum(opts.by, quorum, policyFor(o.root));
|
|
645
|
+
const tally = tallyQuorum(opts.by, quorum, policyFor(o.root), askedByOf(d)?.steward);
|
|
515
646
|
if (!tally.met) {
|
|
516
647
|
const got = tally.counted.length;
|
|
517
648
|
const left = tally.left.length ? `; not counted: ${tally.left.map((l) => `${l.name}, who ${l.why}`).join("; ")}` : "";
|
|
@@ -555,8 +686,10 @@ export async function answerPoint(opts: AnswerPointOptions): Promise<PointsWrite
|
|
|
555
686
|
fields.title = titleFor(point, subject, "answered", value);
|
|
556
687
|
const data = recordData(o, fields);
|
|
557
688
|
const text = renderRecord(data, body(point, String(fields.title)));
|
|
558
|
-
const
|
|
559
|
-
const
|
|
689
|
+
const onLedger = ledger.byPath.has(target.path);
|
|
690
|
+
const message = `Decision point ${name}: answered ${show(value, type)} by ${tally.counted.join(", ")}`;
|
|
691
|
+
const wrote = await write(o, before, target.path, text, false, !!opts.dryRun, { source: found.read.source, ledger, onLedger, message });
|
|
692
|
+
const entry: RecordEntry = { ...target, state: "answered", data, valid: true, reasons: [], warnings: wrote.warnings };
|
|
560
693
|
return {
|
|
561
694
|
$schema: POINTS_WRITE_SCHEMA_ID,
|
|
562
695
|
contract: POINTS_WRITE_CONTRACT_VERSION,
|
|
@@ -567,7 +700,7 @@ export async function answerPoint(opts: AnswerPointOptions): Promise<PointsWrite
|
|
|
567
700
|
reused: false,
|
|
568
701
|
written: !opts.dryRun,
|
|
569
702
|
dryRun: !!opts.dryRun,
|
|
570
|
-
question: questionView(entry, point)!,
|
|
703
|
+
question: questionView(entry, point, onLedger ? (wrote.ledger ?? ledger.byPath.get(target.path)?.ledger ?? null) : null)!,
|
|
571
704
|
...(opts.dryRun ? { text } : {}),
|
|
572
705
|
};
|
|
573
706
|
} catch (err) {
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
3
|
"$id": "https://intentius.io/chant/schemas/workspace/decision-points/v1/decision-points.schema.json",
|
|
4
4
|
"title": "Decision points",
|
|
5
|
-
"description": "A workspace's decision points (ws-058, #2738): the recurring questions it asks of its own graph, declared as data in a JSON file that an answer record kind names in `answers.points`. Each point is a typed question (`noul`, `choice` with 2 to 255 options, or `score` with 2 to 10 ordered levels, the question types of the POST /v1/systemone wire format, #2491), the inputs it reads, each named as a read-contract output, and an ordered chain of deciders: `table` rows, `model` deciders with a backend, a pinned model id and a threshold, and a `quorum` of people, always last.
|
|
5
|
+
"description": "A workspace's decision points (ws-058, #2738): the recurring questions it asks of its own graph, declared as data in a JSON file that an answer record kind names in `answers.points`. Each point is a typed question (`noul`, `choice` with 2 to 255 options, or `score` with 2 to 10 ordered levels, the question types of the POST /v1/systemone wire format, #2491), the inputs it reads, each named as a read-contract output, and an ordered chain of deciders: `table` rows, `model` deciders with a backend, a pinned model id and a threshold, and a `quorum` of people, always last. What JSON Schema cannot say is checked in code (`points.ts`): an input's output is one the read contract has, a table row tests declared inputs and answers a candidate, a field belongs to its decider's kind, a model id is pinned rather than an alias, and the chain ends in its one quorum. Unknown fields are refused, except fields whose names start with x-.",
|
|
6
6
|
"type": "object",
|
|
7
7
|
"required": ["points"],
|
|
8
8
|
"properties": {
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
"type": "object",
|
|
45
45
|
"required": ["type", "instructions", "criteria"],
|
|
46
46
|
"properties": {
|
|
47
|
-
"type": { "enum": ["noul", "
|
|
47
|
+
"type": { "enum": ["noul", "choice", "score"] },
|
|
48
48
|
"instructions": { "$ref": "#/$defs/text" },
|
|
49
49
|
"criteria": {
|
|
50
50
|
"description": "The candidates. noul: {true, false} descriptions. choice: option to description, 2 to 255 options. score: 2 to 10 ordered levels.",
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"additionalProperties": false,
|
|
55
55
|
"allOf": [
|
|
56
56
|
{
|
|
57
|
-
"if": { "properties": { "type": { "
|
|
57
|
+
"if": { "properties": { "type": { "const": "noul" } } },
|
|
58
58
|
"then": {
|
|
59
59
|
"properties": {
|
|
60
60
|
"criteria": {
|
|
@@ -63,6 +63,13 @@
|
|
|
63
63
|
"$ref": "#/$defs/host"
|
|
64
64
|
}
|
|
65
65
|
},
|
|
66
|
+
"diagrams": {
|
|
67
|
+
"type": "array",
|
|
68
|
+
"description": "The workspace's own diagram artifacts (#2764): the diagrams that belong to no one member, such as a diagrams directory at the root. A member names its own in its diagrams. chant workspace ls lists every declared diagram, and chant workspace check reports one whose render is stale for its source when the entry records a sourceHash. Added in schema 1 by chant 0.91.0, so a declaration that uses it sets minReader to 0.91.0 or newer.",
|
|
69
|
+
"items": {
|
|
70
|
+
"$ref": "#/$defs/diagram"
|
|
71
|
+
}
|
|
72
|
+
},
|
|
66
73
|
"checks": {
|
|
67
74
|
"type": "object",
|
|
68
75
|
"description": "Severity for the declaration checks that allow it, keyed by WSP id (#2535, ws-028): error, warning, info or off. chant workspace check refuses an id it does not know, and one whose severity is fixed.",
|
|
@@ -72,6 +79,28 @@
|
|
|
72
79
|
"additionalProperties": {
|
|
73
80
|
"$ref": "#/$defs/severity"
|
|
74
81
|
}
|
|
82
|
+
},
|
|
83
|
+
"changes": {
|
|
84
|
+
"type": "object",
|
|
85
|
+
"additionalProperties": false,
|
|
86
|
+
"description": "The forward coverage check, chant workspace check --changes <base>..<head> (#2773): each path a diff changes is mapped to the current records whose constrains cover it. severity says what a finding does: off reports none, warn (the default) reports them and passes, fail reports them and fails. ignore lists globs over files whose changes need no record, such as lockfiles and generated output. Added in schema 1 by chant 0.92.0, so a declaration that uses it sets minReader to 0.92.0 or newer.",
|
|
87
|
+
"properties": {
|
|
88
|
+
"severity": {
|
|
89
|
+
"enum": [
|
|
90
|
+
"off",
|
|
91
|
+
"warn",
|
|
92
|
+
"fail"
|
|
93
|
+
],
|
|
94
|
+
"description": "What a change-uncovered or change-out-of-scope finding does. warn when absent."
|
|
95
|
+
},
|
|
96
|
+
"ignore": {
|
|
97
|
+
"type": "array",
|
|
98
|
+
"description": "Globs over file paths from the workspace root: * and ? within one segment, ** across segments. A changed path one matches is listed as ignored and never reported.",
|
|
99
|
+
"items": {
|
|
100
|
+
"$ref": "#/$defs/fileGlob"
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
75
104
|
}
|
|
76
105
|
},
|
|
77
106
|
"patternProperties": {
|
|
@@ -276,6 +305,13 @@
|
|
|
276
305
|
"$ref": "#/$defs/recordKind"
|
|
277
306
|
}
|
|
278
307
|
},
|
|
308
|
+
"diagrams": {
|
|
309
|
+
"type": "array",
|
|
310
|
+
"description": "The diagram artifacts this member holds (#2764). chant workspace ls lists every declared diagram, and chant workspace check reports one whose render is stale for its source when the entry records a sourceHash. Added in schema 1 by chant 0.91.0, so a declaration that uses it sets minReader to 0.91.0 or newer.",
|
|
311
|
+
"items": {
|
|
312
|
+
"$ref": "#/$defs/diagram"
|
|
313
|
+
}
|
|
314
|
+
},
|
|
279
315
|
"box": {
|
|
280
316
|
"$ref": "#/$defs/box"
|
|
281
317
|
},
|
|
@@ -366,8 +402,13 @@
|
|
|
366
402
|
},
|
|
367
403
|
"box": {
|
|
368
404
|
"type": "object",
|
|
369
|
-
"description": "The member is a box, or holds a box's declarations (#2726). A box holds no credential: each capability it needs outside itself is reached through a broker, a runtime such as a lobby or a door, which holds the credential and enforces the scope. chant workspace check fails when a file in the member's directory carries a literal secret (WSP121) and when a capability names no broker (WSP122). chant workspace status --json lists the capabilities, so a broker can read the scopes. Its host, slot, ports, state and cookies state the box's isolation (#2727): chant derives each value from the box's identity, its host, the member's name and its slot, and prints them in chant workspace status --json, so a runtime reads them instead of choosing its own. chant workspace check fails when two boxes on a host resolve to the same value (WSP123) and when a box hard-codes a machine path (WSP124). Added in schema 1 by chant 0.91.0, so a declaration that uses it sets minReader to 0.91.0 or newer.",
|
|
405
|
+
"description": "The member is a box, or holds a box's declarations (#2726). A box holds no credential: each capability it needs outside itself is reached through a broker, a runtime such as a lobby or a door, which holds the credential and enforces the scope. chant workspace check fails when a file in the member's directory carries a literal secret (WSP121) and when a capability names no broker (WSP122). chant workspace status --json lists the capabilities, so a broker can read the scopes. Its host, slot, ports, state and cookies state the box's isolation (#2727): chant derives each value from the box's identity, its host, the member's name and its slot, and prints them in chant workspace status --json, so a runtime reads them instead of choosing its own. chant workspace check fails when two boxes on a host resolve to the same value (WSP123) and when a box hard-codes a machine path (WSP124). A member that builds the fountain lexicon's Box declares the callback token fountain gives its persistent sandbox as the capability fountain-callback, brokered by fountain, with scope owner, or chant workspace check fails (WSP125, #2780). Its intent names the decision record that says what the box is for (#2850): chant workspace status --json reports the record's state and answer, and chant workspace check fails when no decision record has the id (WSP126) and warns when the record constrains no member or path of this workspace (WSP127, #2857). Added in schema 1 by chant 0.91.0, so a declaration that uses it sets minReader to 0.91.0 or newer.",
|
|
370
406
|
"properties": {
|
|
407
|
+
"intent": {
|
|
408
|
+
"type": "string",
|
|
409
|
+
"minLength": 1,
|
|
410
|
+
"description": "The id of the decision record that says what the box is for (#2850), a record of a declared kind named decision. A new box starts as a question: the record is proposed with a null choice until the person who answers it decides it. The record constrains a member or path of this workspace, with member:<a member's name> or a path: entry at, above or inside a member's directory. It need not be this member: the intent can constrain the app the box runs when that app is another member (#2857). When it constrains this member, chant workspace graph --intent shows it for the box's files. Added by chant 0.94.0, so a declaration that uses it sets minReader to 0.94.0 or newer."
|
|
411
|
+
},
|
|
371
412
|
"capabilities": {
|
|
372
413
|
"type": "array",
|
|
373
414
|
"description": "Each capability the box reaches outside itself, named once. None when omitted.",
|
|
@@ -493,6 +534,78 @@
|
|
|
493
534
|
},
|
|
494
535
|
"additionalProperties": false
|
|
495
536
|
},
|
|
537
|
+
"diagram": {
|
|
538
|
+
"type": "object",
|
|
539
|
+
"description": "A diagram artifact the workspace declares (#2764): a rendered image with a source, pinned to the renderer that made it. chant never runs the renderer.",
|
|
540
|
+
"required": [
|
|
541
|
+
"name",
|
|
542
|
+
"title",
|
|
543
|
+
"render",
|
|
544
|
+
"renderer"
|
|
545
|
+
],
|
|
546
|
+
"properties": {
|
|
547
|
+
"name": {
|
|
548
|
+
"$ref": "#/$defs/name",
|
|
549
|
+
"description": "Unique across the declaration."
|
|
550
|
+
},
|
|
551
|
+
"title": {
|
|
552
|
+
"type": "string",
|
|
553
|
+
"minLength": 1,
|
|
554
|
+
"description": "Shown above the diagram."
|
|
555
|
+
},
|
|
556
|
+
"source": {
|
|
557
|
+
"oneOf": [
|
|
558
|
+
{ "$ref": "#/$defs/path" },
|
|
559
|
+
{ "type": "null" }
|
|
560
|
+
],
|
|
561
|
+
"description": "The source file, from the workspace root, with / separators. Null for an SVG with no source. Default null."
|
|
562
|
+
},
|
|
563
|
+
"render": {
|
|
564
|
+
"$ref": "#/$defs/path",
|
|
565
|
+
"description": "The rendered image, from the workspace root, with / separators."
|
|
566
|
+
},
|
|
567
|
+
"renderer": {
|
|
568
|
+
"$ref": "#/$defs/diagramRenderer"
|
|
569
|
+
},
|
|
570
|
+
"sourceHash": {
|
|
571
|
+
"type": ["string", "null"],
|
|
572
|
+
"pattern": "^[0-9a-f]{64}$",
|
|
573
|
+
"description": "sha256 hex of the source's bytes when render was last produced from it. chant workspace check compares this against the source in the tree read and reports drift (WSP133) on a mismatch; without it, or without a source, the render is never checked for drift. Default null."
|
|
574
|
+
}
|
|
575
|
+
},
|
|
576
|
+
"patternProperties": {
|
|
577
|
+
"^x-": true
|
|
578
|
+
},
|
|
579
|
+
"additionalProperties": false
|
|
580
|
+
},
|
|
581
|
+
"diagramRenderer": {
|
|
582
|
+
"type": "object",
|
|
583
|
+
"description": "The renderer a diagram's render was made with. chant records the pin; it never installs or runs the renderer (ws-052).",
|
|
584
|
+
"required": [
|
|
585
|
+
"tool",
|
|
586
|
+
"version"
|
|
587
|
+
],
|
|
588
|
+
"properties": {
|
|
589
|
+
"tool": {
|
|
590
|
+
"enum": ["d2", "mermaid", "graphviz"]
|
|
591
|
+
},
|
|
592
|
+
"version": {
|
|
593
|
+
"$ref": "#/$defs/version",
|
|
594
|
+
"description": "The exact release the render was made with, such as 0.9.0."
|
|
595
|
+
},
|
|
596
|
+
"args": {
|
|
597
|
+
"type": "array",
|
|
598
|
+
"description": "Passed before the input and output paths, in order, such as [\"--layout=elk\", \"--theme=0\", \"--pad=40\", \"--omit-version\"]. Empty when absent.",
|
|
599
|
+
"items": {
|
|
600
|
+
"type": "string"
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
},
|
|
604
|
+
"patternProperties": {
|
|
605
|
+
"^x-": true
|
|
606
|
+
},
|
|
607
|
+
"additionalProperties": false
|
|
608
|
+
},
|
|
496
609
|
"generatedFile": {
|
|
497
610
|
"type": "object",
|
|
498
611
|
"description": "One generated file of a member (#2524 D14, #2541).",
|
|
@@ -641,6 +754,11 @@
|
|
|
641
754
|
"type": "integer",
|
|
642
755
|
"minimum": 1,
|
|
643
756
|
"maximum": 65535
|
|
757
|
+
},
|
|
758
|
+
"fileGlob": {
|
|
759
|
+
"type": "string",
|
|
760
|
+
"description": "A glob over file paths, relative to the workspace root: * and ? within one segment, ** across segments, [...] classes and {a,b} alternatives. No leading /, no . or .. segments.",
|
|
761
|
+
"pattern": "^(?!\\.\\.?(/|$))[^/\\\\]+(/(?!\\.\\.?(/|$))[^/\\\\]+)*$"
|
|
644
762
|
}
|
|
645
763
|
}
|
|
646
764
|
}
|
|
@@ -6,6 +6,7 @@ import { afterAll, describe, expect, test } from "vitest";
|
|
|
6
6
|
import {
|
|
7
7
|
compareVersions,
|
|
8
8
|
DECLARATION_SCHEMA_ID,
|
|
9
|
+
declaredDiagrams,
|
|
9
10
|
ownerOf,
|
|
10
11
|
parseDeclaration,
|
|
11
12
|
readDeclaration,
|
|
@@ -345,3 +346,60 @@ describe("generated files (#2541)", () => {
|
|
|
345
346
|
expect(failure(base([{ name: "g", kind: "examples", glob: "examples/*", generated: [] }])).message).toContain('unknown field "generated"');
|
|
346
347
|
});
|
|
347
348
|
});
|
|
349
|
+
|
|
350
|
+
describe("diagram artifacts (#2764)", () => {
|
|
351
|
+
const renderer = { tool: "d2", version: "0.9.0", args: ["--layout=elk", "--theme=0", "--pad=40", "--omit-version"] };
|
|
352
|
+
const diagram = (extra: Record<string, unknown> = {}) => ({
|
|
353
|
+
name: "architecture",
|
|
354
|
+
title: "Studio architecture",
|
|
355
|
+
source: "docs/diagrams/architecture.d2",
|
|
356
|
+
render: "docs/diagrams/architecture.svg",
|
|
357
|
+
renderer,
|
|
358
|
+
...extra,
|
|
359
|
+
});
|
|
360
|
+
const member = (diagrams: unknown[], extra: Record<string, unknown> = {}) => ({ name: "docs", dir: "docs", kind: "other", because: "the docs site", diagrams, ...extra });
|
|
361
|
+
|
|
362
|
+
test("a member's diagram is read with the workspace-relative source and render, and a null source is the default", () => {
|
|
363
|
+
const d = parse(base([member([diagram()])]));
|
|
364
|
+
expect(d.members[0].diagrams).toEqual([
|
|
365
|
+
{ name: "architecture", title: "Studio architecture", source: "docs/diagrams/architecture.d2", render: "docs/diagrams/architecture.svg", renderer, sourceHash: null, member: "docs", pointer: "/members/0/diagrams/0" },
|
|
366
|
+
]);
|
|
367
|
+
const noSource = parse(base([member([diagram({ source: undefined })])]));
|
|
368
|
+
expect(noSource.members[0].diagrams[0].source).toBeNull();
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
test("the workspace's own diagrams are in the top-level diagrams, with member null", () => {
|
|
372
|
+
const d = parse(base([{ name: "x", dir: "x", kind: "chant" }], { diagrams: [diagram({ name: "boundary" })] }));
|
|
373
|
+
expect(d.diagrams).toEqual([{ ...diagram({ name: "boundary" }), sourceHash: null, member: null, pointer: "/diagrams/0" }]);
|
|
374
|
+
expect(declaredDiagrams(d)).toEqual(d.diagrams);
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
test("declaredDiagrams lists the workspace's own first, then each member's, in file order", () => {
|
|
378
|
+
const d = parse(base([member([diagram()]), { name: "app", dir: "app", kind: "chant" }], { diagrams: [diagram({ name: "boundary", render: "docs/diagrams/boundary.svg" })] }));
|
|
379
|
+
expect(declaredDiagrams(d).map((x) => x.name)).toEqual(["boundary", "architecture"]);
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
test("a diagram name is unique across the declaration, workspace and members together", () => {
|
|
383
|
+
const clash = failure(base([member([diagram()])], { diagrams: [diagram()] }));
|
|
384
|
+
expect(clash.code).toBe("declaration-invalid");
|
|
385
|
+
expect(clash.message).toContain('the diagram name "architecture" is already used by the diagram at /diagrams/0');
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
test("renderer.tool is a closed list, and title, render and renderer are required", () => {
|
|
389
|
+
expect(failure(base([member([diagram({ renderer: { tool: "mscgen", version: "1.0.0" } })])])).code).toBe("declaration-invalid");
|
|
390
|
+
expect(failure(base([member([{ name: "a", source: null, render: "a.svg", renderer }])])).message).toContain('missing required field "title"');
|
|
391
|
+
expect(failure(base([member([{ name: "a", title: "A", source: null, renderer }])])).message).toContain('missing required field "render"');
|
|
392
|
+
expect(failure(base([member([{ name: "a", title: "A", source: null, render: "a.svg" }])])).message).toContain('missing required field "renderer"');
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
test("a sourceHash is a 64-character lowercase hex string, or absent", () => {
|
|
396
|
+
const hash = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
|
|
397
|
+
expect(parse(base([member([diagram({ sourceHash: hash })])])).members[0].diagrams[0].sourceHash).toBe(hash);
|
|
398
|
+
expect(failure(base([member([diagram({ sourceHash: "not-a-hash" })])])).code).toBe("declaration-invalid");
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
test("source and render stay inside the workspace, like every other path", () => {
|
|
402
|
+
expect(failure(base([member([diagram({ source: "/etc/passwd" })])])).code).toBe("declaration-invalid");
|
|
403
|
+
expect(failure(base([member([diagram({ render: "../out.svg" })])])).code).toBe("declaration-invalid");
|
|
404
|
+
});
|
|
405
|
+
});
|