@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
|
@@ -5,13 +5,12 @@
|
|
|
5
5
|
* is not open and appends the verdict to the session too, and
|
|
6
6
|
* `records --since <session id>` compares the session's own revisions. The
|
|
7
7
|
* fixture is a copy of the reference workspace's decisions and design
|
|
8
|
-
* member with a declaration naming both kinds.
|
|
8
|
+
* member with a declaration naming both kinds. The CLI run of close and
|
|
9
|
+
* --since is in records-sessions-write.e2e.test.ts (#2817).
|
|
9
10
|
*/
|
|
10
11
|
|
|
11
|
-
import { spawnSync } from "node:child_process";
|
|
12
12
|
import { cpSync, readFileSync, writeFileSync } from "node:fs";
|
|
13
13
|
import { join } from "node:path";
|
|
14
|
-
import { pathToFileURL } from "node:url";
|
|
15
14
|
import { afterAll, describe, expect, test } from "vitest";
|
|
16
15
|
import { cleanScratch, commitAll, contract, git, REPO, scratchDir } from "./__fixtures__/contract-repo";
|
|
17
16
|
import { declareSessions, DECISIONS_KIND, newSessionFields, sessionsRepo, SESSIONS_KIND } from "./__fixtures__/sessions";
|
|
@@ -240,35 +239,4 @@ describe("records --since <session id> (#2693)", () => {
|
|
|
240
239
|
git(root, "tag", "v-1");
|
|
241
240
|
expect((await sinceDoc({ kind: DECISIONS_KIND, since: "v-1", cwd: root })).session).toBeUndefined();
|
|
242
241
|
});
|
|
243
|
-
|
|
244
|
-
test(
|
|
245
|
-
"through the CLI: close with the declared session kind, and --since <session id> without --kind",
|
|
246
|
-
async () => {
|
|
247
|
-
const root = declared();
|
|
248
|
-
await opened(root);
|
|
249
|
-
const run = (...args: string[]) =>
|
|
250
|
-
spawnSync(process.execPath, ["--import", pathToFileURL(join(REPO, "node_modules/tsx/dist/loader.mjs")).href, join(REPO, "packages/core/src/cli/main.ts"), "workspace", "records", ...args], {
|
|
251
|
-
cwd: root,
|
|
252
|
-
encoding: "utf-8",
|
|
253
|
-
env: { ...process.env, NO_COLOR: "1" },
|
|
254
|
-
timeout: 60_000,
|
|
255
|
-
});
|
|
256
|
-
const closed = run("close", "S-0002");
|
|
257
|
-
expect(closed.status, closed.stderr).toBe(0);
|
|
258
|
-
close.expectValid(JSON.parse(closed.stdout));
|
|
259
|
-
const again = run("close", "S-0002");
|
|
260
|
-
expect(again.status).toBe(1);
|
|
261
|
-
expect(JSON.parse(again.stdout).error.code).toBe("record-closed");
|
|
262
|
-
commitAll(root, "close");
|
|
263
|
-
const set = run("--since", "S-0002", "--json");
|
|
264
|
-
expect(set.status, set.stderr).toBe(0);
|
|
265
|
-
const kinds = JSON.parse(set.stdout).kinds as RecordsSinceDocument[];
|
|
266
|
-
for (const k of kinds) since.expectValid(k);
|
|
267
|
-
expect(kinds.map((k) => ("session" in k ? k.session?.id : null))).toEqual(["S-0002", "S-0002"]);
|
|
268
|
-
const text = run("--kind", SESSIONS_KIND, "--since", "S-0002");
|
|
269
|
-
expect(text.stdout).toContain("session S-0002 (closed)");
|
|
270
|
-
expect(text.stdout).toContain("S-0002 new, closed");
|
|
271
|
-
},
|
|
272
|
-
120_000,
|
|
273
|
-
);
|
|
274
242
|
});
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
* and the comparison. The fixture is a repository with commits for a review
|
|
4
4
|
* session's open and close: between them the session closes with two
|
|
5
5
|
* verdicts, ref-001 gains the two reviews that name it and is ratified,
|
|
6
|
-
* a new decision
|
|
6
|
+
* a new decision (ref-004, after the reference workspace's own ref-003)
|
|
7
|
+
* supersedes ref-002, and ref-002's pin moves. Every document
|
|
7
8
|
* validates against records-since.schema.json.
|
|
8
9
|
*/
|
|
9
10
|
|
|
@@ -56,8 +57,8 @@ function sessionHistory(): { root: string; before: string; open: string; close:
|
|
|
56
57
|
const text = readFileSync(ref002, "utf-8");
|
|
57
58
|
writeFileSync(ref002, text.replace(/sha256: "[0-9a-f]{64}"/, `sha256: "${"a".repeat(64)}"`));
|
|
58
59
|
writeFileSync(
|
|
59
|
-
join(root, "decisions", "ref-
|
|
60
|
-
text.replace(/^id: .*$/m, 'id: "ref-
|
|
60
|
+
join(root, "decisions", "ref-004-a-successor.md"),
|
|
61
|
+
text.replace(/^id: .*$/m, 'id: "ref-004"').replace(/^supersedes: \[\]$/m, 'supersedes:\n - decision: "ref-002"'),
|
|
61
62
|
);
|
|
62
63
|
const close = commitAll(root, "close S-0002");
|
|
63
64
|
return { root, before, open, close };
|
|
@@ -85,11 +86,11 @@ describe("records --since (#2673)", () => {
|
|
|
85
86
|
expect(doc.at).toBe(h.close);
|
|
86
87
|
expect(doc.kind).toEqual({ name: "decision", schema: "urn:intentius:chant:decision:1", file: "decisions/decision.kind.mjs" });
|
|
87
88
|
expect(doc.changes).toEqual([
|
|
88
|
-
{ change: "new", id: "ref-
|
|
89
|
+
{ change: "new", id: "ref-004", path: "decisions/ref-004-a-successor.md", state: "decided" },
|
|
89
90
|
{ change: "state", id: "ref-001", from: "decided", to: "ratified" },
|
|
90
91
|
{ change: "verdict", id: "ref-001", principal: "alice", verdict: "agree", index: 0, session: "S-0002" },
|
|
91
92
|
{ change: "verdict", id: "ref-001", principal: "bob", verdict: "agree", index: 1, session: "S-0002" },
|
|
92
|
-
{ change: "supersession", id: "ref-
|
|
93
|
+
{ change: "supersession", id: "ref-004", supersedes: "ref-002" },
|
|
93
94
|
{ change: "pin", id: "ref-002", path: "design/screens/home.json", from: expect.stringMatching(/^[0-9a-f]{64}$/), to: "a".repeat(64) },
|
|
94
95
|
]);
|
|
95
96
|
expect(doc.summary).toEqual({ new: 1, removed: 0, state: 1, verdict: 2, supersession: 1, pin: 1 });
|
|
@@ -105,11 +106,11 @@ describe("records --since (#2673)", () => {
|
|
|
105
106
|
});
|
|
106
107
|
|
|
107
108
|
test("without --at it compares with the working tree, and a record deleted there is removed", async () => {
|
|
108
|
-
rmSync(join(h.root, "decisions", "ref-
|
|
109
|
+
rmSync(join(h.root, "decisions", "ref-004-a-successor.md"));
|
|
109
110
|
try {
|
|
110
111
|
const doc = ok(await since({ kind: DECISIONS_KIND, since: h.close, cwd: h.root }));
|
|
111
112
|
expect(doc.at).toBeNull();
|
|
112
|
-
expect(doc.changes).toEqual([{ change: "removed", id: "ref-
|
|
113
|
+
expect(doc.changes).toEqual([{ change: "removed", id: "ref-004", path: "decisions/ref-004-a-successor.md", state: "decided" }]);
|
|
113
114
|
} finally {
|
|
114
115
|
git(h.root, "checkout", "--", "decisions");
|
|
115
116
|
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `chant workspace records new|amend|review` (#2670) through the command
|
|
3
|
+
* line: standard input, exit codes and the refusal document, from the CLI a
|
|
4
|
+
* user runs. Each test spawns the CLI several times, which put them near 20s
|
|
5
|
+
* on CI, over the unit shards' per-test budget, so they run in the test-e2e
|
|
6
|
+
* job (#2817). The commands' own behaviour is covered in records-write.test.ts.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { execFileSync } from "node:child_process";
|
|
10
|
+
import { cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
11
|
+
import { tmpdir } from "node:os";
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
import { pathToFileURL } from "node:url";
|
|
14
|
+
import { afterEach, beforeEach, describe, expect, test } from "vitest";
|
|
15
|
+
import { parseFrontMatter } from "./records";
|
|
16
|
+
import { renderRecord } from "./records-write";
|
|
17
|
+
|
|
18
|
+
const REPO = join(import.meta.dirname, "..", "..", "..", "..");
|
|
19
|
+
const DECISIONS = join(REPO, "docs", "design", "decisions");
|
|
20
|
+
const KIND = "decisions/decision.kind.mjs";
|
|
21
|
+
|
|
22
|
+
type Data = Record<string, unknown>;
|
|
23
|
+
|
|
24
|
+
const SAMPLE = (() => {
|
|
25
|
+
const fm = parseFrontMatter(readFileSync(join(DECISIONS, "ws-003-seal-scope.md"), "utf-8"));
|
|
26
|
+
if (!fm.ok) throw new Error(fm.message);
|
|
27
|
+
return fm.value;
|
|
28
|
+
})();
|
|
29
|
+
|
|
30
|
+
/** A decided decision like ws-003, with `over` laid on top. */
|
|
31
|
+
function decision(over: Data = {}): Data {
|
|
32
|
+
return { ...structuredClone(SAMPLE), ...over };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** A proposed decision: no choice, nobody decided. */
|
|
36
|
+
function proposal(over: Data = {}): Data {
|
|
37
|
+
return decision({ state: "proposed", choice: null, decided_by: null, decided_on: null, ...over });
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
let dir: string;
|
|
41
|
+
beforeEach(() => {
|
|
42
|
+
dir = realpathSync(mkdtempSync(join(tmpdir(), "chant-records-write-")));
|
|
43
|
+
mkdirSync(join(dir, "decisions"));
|
|
44
|
+
cpSync(join(DECISIONS, "decision.kind.mjs"), join(dir, "decisions", "decision.kind.mjs"));
|
|
45
|
+
cpSync(join(DECISIONS, "decision.schema.json"), join(dir, "decisions", "decision.schema.json"));
|
|
46
|
+
put("ws-001-one.md", decision({ id: "ws-001", title: "One" }));
|
|
47
|
+
put("ws-002-two.md", proposal({ id: "ws-002", title: "Two" }));
|
|
48
|
+
put("ws-003-three.md", decision({ id: "ws-003", title: "Three", state: "ratified" }));
|
|
49
|
+
});
|
|
50
|
+
afterEach(() => rmSync(dir, { recursive: true, force: true }));
|
|
51
|
+
|
|
52
|
+
function put(name: string, data: Data): void {
|
|
53
|
+
writeFileSync(join(dir, "decisions", name), renderRecord(data, `\n# ${String(data.title)}\n`));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
describe("the command line", () => {
|
|
57
|
+
const cli = (args: string[], input?: string) => {
|
|
58
|
+
const loader = pathToFileURL(join(REPO, "node_modules", "tsx", "dist", "loader.mjs")).href;
|
|
59
|
+
try {
|
|
60
|
+
const out = execFileSync(process.execPath, ["--import", loader, join(REPO, "packages", "core", "src", "cli", "main.ts"), "workspace", "records", ...args], {
|
|
61
|
+
cwd: dir,
|
|
62
|
+
input,
|
|
63
|
+
encoding: "utf-8",
|
|
64
|
+
env: { ...process.env, TSX_DISABLE_CACHE: "1" },
|
|
65
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
66
|
+
});
|
|
67
|
+
return { status: 0, doc: JSON.parse(out) };
|
|
68
|
+
} catch (err) {
|
|
69
|
+
const e = err as { status: number; stdout: string };
|
|
70
|
+
return { status: e.status, doc: JSON.parse(e.stdout) };
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
test(
|
|
75
|
+
"new reads fields from standard input, review appends, and a refusal exits 1 with its code",
|
|
76
|
+
() => {
|
|
77
|
+
const { id: _id, ...rest } = decision({ title: "From stdin" });
|
|
78
|
+
const made = cli(["new", KIND, "--from", "-"], JSON.stringify(rest));
|
|
79
|
+
expect(made).toMatchObject({ status: 0, doc: { id: "ws-004", path: "decisions/ws-004-from-stdin.md" } });
|
|
80
|
+
expect(existsSync(join(dir, "decisions", "ws-004-from-stdin.md"))).toBe(true);
|
|
81
|
+
const reviewed = cli(["review", "ws-004", "--kind", KIND, "--verdict", "agree", "--by", "alice"]);
|
|
82
|
+
expect(reviewed).toMatchObject({ status: 0, doc: { review: { reviewer: "alice", verdict: "agree" } } });
|
|
83
|
+
const refused = cli(["review", "ws-004", "--kind", KIND, "--verdict", "dissent", "--by", "bob"]);
|
|
84
|
+
expect(refused).toMatchObject({ status: 1, doc: { error: { code: "review-note-required" } } });
|
|
85
|
+
writeFileSync(join(dir, "patch.json"), JSON.stringify({ title: "Changed" }));
|
|
86
|
+
expect(cli(["amend", "ws-004", "--kind", KIND, "--set", "patch.json"])).toMatchObject({ status: 1, doc: { error: { code: "amend-supersede-instead" } } });
|
|
87
|
+
expect(cli(["amend", "ws-004", "--set", "patch.json"])).toMatchObject({ status: 1, doc: { error: { code: "write-usage-invalid" } } });
|
|
88
|
+
},
|
|
89
|
+
120_000,
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
test(
|
|
93
|
+
"new --by writes the proposer, not the decider, and amend --by is refused rather than dropped (#2756)",
|
|
94
|
+
() => {
|
|
95
|
+
const { id: _id, ...rest } = decision({ title: "Kept by a session", state: "proposed", choice: null, decided_by: null, decided_on: null });
|
|
96
|
+
const made = cli(["new", KIND, "--from", "-", "--by", "hud-session"], JSON.stringify(rest));
|
|
97
|
+
expect(made).toMatchObject({ status: 0, doc: { id: "ws-004" } });
|
|
98
|
+
const fm = parseFrontMatter(readFileSync(join(dir, "decisions", "ws-004-kept-by-a-session.md"), "utf-8"));
|
|
99
|
+
if (!fm.ok) throw new Error(fm.message);
|
|
100
|
+
expect(fm.value).toMatchObject({ proposed_by: "hud-session", decided_by: null });
|
|
101
|
+
|
|
102
|
+
writeFileSync(join(dir, "patch.json"), JSON.stringify({ title: "Changed" }));
|
|
103
|
+
const refused = cli(["amend", "ws-004", "--kind", KIND, "--set", "patch.json", "--by", "alice"]);
|
|
104
|
+
expect(refused).toMatchObject({ status: 1, doc: { error: { code: "write-usage-invalid", message: expect.stringContaining("--by is not taken by amend") } } });
|
|
105
|
+
},
|
|
106
|
+
120_000,
|
|
107
|
+
);
|
|
108
|
+
});
|
|
@@ -2,14 +2,13 @@
|
|
|
2
2
|
* `chant workspace records new|amend|review` (#2670): each command validates
|
|
3
3
|
* before it writes, writes exactly one file or none, and refuses with a closed
|
|
4
4
|
* code. Run in a directory outside git holding a copy of the decision kind.
|
|
5
|
+
* The same commands through the CLI are in records-write.e2e.test.ts (#2817).
|
|
5
6
|
*/
|
|
6
7
|
|
|
7
|
-
import { execFileSync } from "node:child_process";
|
|
8
8
|
import { createHash } from "node:crypto";
|
|
9
|
-
import { cpSync,
|
|
9
|
+
import { cpSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
10
10
|
import { tmpdir } from "node:os";
|
|
11
11
|
import { join } from "node:path";
|
|
12
|
-
import { pathToFileURL } from "node:url";
|
|
13
12
|
import { afterEach, beforeEach, describe, expect, test } from "vitest";
|
|
14
13
|
import { parseFrontMatter, recordTextDigest } from "./records";
|
|
15
14
|
import { allocateId, amendRecord, newRecord, renderRecord, reviewRecord, toYaml } from "./records-write";
|
|
@@ -187,7 +186,7 @@ describe("records new", () => {
|
|
|
187
186
|
});
|
|
188
187
|
|
|
189
188
|
test("keeps the width of the ids it follows", () => {
|
|
190
|
-
const entry = (id: string) => ({ id, path: `d/${id}-x.md`, state: null, valid: true, reasons: [], supersededBy: null, data: {}, digest: "", assets: [], warnings: [] });
|
|
189
|
+
const entry = (id: string) => ({ id, path: `d/${id}-x.md`, state: null, valid: true, reasons: [], supersededBy: null, remediatedBy: [], data: {}, digest: "", assets: [], warnings: [] });
|
|
191
190
|
expect(allocateId([entry("s-0009"), entry("s-0010")], undefined, "k")).toBe("s-0011");
|
|
192
191
|
expect(allocateId([entry("s-999")], undefined, "k")).toBe("s-1000");
|
|
193
192
|
// Case and padding come from the records (#2683).
|
|
@@ -225,6 +224,35 @@ describe("records new", () => {
|
|
|
225
224
|
test("a kind that can't be read is its read error", async () => {
|
|
226
225
|
expect(code(await newRecord({ kind: "missing.kind.mjs", fields: fields(fresh()), cwd: dir }))).toBe("kind-unreadable");
|
|
227
226
|
});
|
|
227
|
+
|
|
228
|
+
test("--by names the proposer of a record that opens proposed, apart from decided_by (#2756)", async () => {
|
|
229
|
+
const { id: _id, ...freshProposal } = proposal({ title: "Filed by someone" });
|
|
230
|
+
const proposed = await newRecord({ kind: KIND, fields: fields(freshProposal), by: "carol", cwd: dir });
|
|
231
|
+
expect(code(proposed)).toBe("ok");
|
|
232
|
+
const proposedPath = (proposed as { path: string }).path.replace("decisions/", "");
|
|
233
|
+
expect(data(proposedPath)).toMatchObject({ proposed_by: "carol", decided_by: null });
|
|
234
|
+
|
|
235
|
+
// A conflicting proposed_by given in the fields is refused, same as a conflicting decided_by is today.
|
|
236
|
+
const conflict = await newRecord({ kind: KIND, fields: fields({ ...freshProposal, title: "Two proposers", proposed_by: "dave" }), by: "carol", cwd: dir });
|
|
237
|
+
expect(code(conflict)).toBe("write-input-invalid");
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
test("--by names the decider when a new record opens straight into a later state", async () => {
|
|
241
|
+
const { id: _id, decided_by: _db, ...freshDecided } = decision({ title: "Decided at once" });
|
|
242
|
+
const decidedDoc = await newRecord({ kind: KIND, fields: fields({ ...freshDecided, decided_by: null }), by: "dana", cwd: dir });
|
|
243
|
+
expect(code(decidedDoc)).toBe("ok");
|
|
244
|
+
const decidedPath = (decidedDoc as { path: string }).path.replace("decisions/", "");
|
|
245
|
+
expect(data(decidedPath)).toMatchObject({ decided_by: "dana" });
|
|
246
|
+
expect(data(decidedPath)).not.toHaveProperty("proposed_by");
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
test("--by is refused for a kind whose new record opens decided and declares no reviews", async () => {
|
|
250
|
+
const kindFile = join(dir, "decisions", "decision.kind.mjs");
|
|
251
|
+
writeFileSync(kindFile, readFileSync(kindFile, "utf-8").replace(/^\s*reviews: \{[^}]*\},?\n/m, ""));
|
|
252
|
+
const { id: _id, decided_by: _db, ...freshDecided } = decision({ title: "No reviews declared" });
|
|
253
|
+
const doc = await newRecord({ kind: KIND, fields: fields({ ...freshDecided, decided_by: null }), by: "erin", cwd: dir });
|
|
254
|
+
expect(code(doc)).toBe("write-usage-invalid");
|
|
255
|
+
});
|
|
228
256
|
});
|
|
229
257
|
|
|
230
258
|
describe("records amend", () => {
|
|
@@ -336,40 +364,3 @@ describe("records review", () => {
|
|
|
336
364
|
expect(code(await reviewRecord({ kind: KIND, id: "ws-001", verdict: "agree", by: "a", cwd: dir }))).toBe("review-unsupported");
|
|
337
365
|
});
|
|
338
366
|
});
|
|
339
|
-
|
|
340
|
-
describe("the command line", () => {
|
|
341
|
-
const cli = (args: string[], input?: string) => {
|
|
342
|
-
const loader = pathToFileURL(join(REPO, "node_modules", "tsx", "dist", "loader.mjs")).href;
|
|
343
|
-
try {
|
|
344
|
-
const out = execFileSync(process.execPath, ["--import", loader, join(REPO, "packages", "core", "src", "cli", "main.ts"), "workspace", "records", ...args], {
|
|
345
|
-
cwd: dir,
|
|
346
|
-
input,
|
|
347
|
-
encoding: "utf-8",
|
|
348
|
-
env: { ...process.env, TSX_DISABLE_CACHE: "1" },
|
|
349
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
350
|
-
});
|
|
351
|
-
return { status: 0, doc: JSON.parse(out) };
|
|
352
|
-
} catch (err) {
|
|
353
|
-
const e = err as { status: number; stdout: string };
|
|
354
|
-
return { status: e.status, doc: JSON.parse(e.stdout) };
|
|
355
|
-
}
|
|
356
|
-
};
|
|
357
|
-
|
|
358
|
-
test(
|
|
359
|
-
"new reads fields from standard input, review appends, and a refusal exits 1 with its code",
|
|
360
|
-
() => {
|
|
361
|
-
const { id: _id, ...rest } = decision({ title: "From stdin" });
|
|
362
|
-
const made = cli(["new", KIND, "--from", "-"], JSON.stringify(rest));
|
|
363
|
-
expect(made).toMatchObject({ status: 0, doc: { id: "ws-004", path: "decisions/ws-004-from-stdin.md" } });
|
|
364
|
-
expect(existsSync(join(dir, "decisions", "ws-004-from-stdin.md"))).toBe(true);
|
|
365
|
-
const reviewed = cli(["review", "ws-004", "--kind", KIND, "--verdict", "agree", "--by", "alice"]);
|
|
366
|
-
expect(reviewed).toMatchObject({ status: 0, doc: { review: { reviewer: "alice", verdict: "agree" } } });
|
|
367
|
-
const refused = cli(["review", "ws-004", "--kind", KIND, "--verdict", "dissent", "--by", "bob"]);
|
|
368
|
-
expect(refused).toMatchObject({ status: 1, doc: { error: { code: "review-note-required" } } });
|
|
369
|
-
writeFileSync(join(dir, "patch.json"), JSON.stringify({ title: "Changed" }));
|
|
370
|
-
expect(cli(["amend", "ws-004", "--kind", KIND, "--set", "patch.json"])).toMatchObject({ status: 1, doc: { error: { code: "amend-supersede-instead" } } });
|
|
371
|
-
expect(cli(["amend", "ws-004", "--set", "patch.json"])).toMatchObject({ status: 1, doc: { error: { code: "write-usage-invalid" } } });
|
|
372
|
-
},
|
|
373
|
-
120_000,
|
|
374
|
-
);
|
|
375
|
-
});
|
|
@@ -629,9 +629,15 @@ export interface WriteChannel {
|
|
|
629
629
|
opensInitial?: boolean;
|
|
630
630
|
}
|
|
631
631
|
|
|
632
|
-
/** What a channel's write adds to the command line's: who the author
|
|
632
|
+
/** What a channel's write adds to the command line's: who named the author, and how it came. */
|
|
633
633
|
export interface ChannelOptions {
|
|
634
|
-
/**
|
|
634
|
+
/**
|
|
635
|
+
* Who the caller names as the record's author (#2756). Written to the
|
|
636
|
+
* kind's `proposedBy` field when the kind declares one and the record
|
|
637
|
+
* opens (or stays) in its first state — a proposal naming its proposer.
|
|
638
|
+
* Otherwise written to `reviews.decider`, as before: a record opening (or
|
|
639
|
+
* moving) straight to another state names who decided it.
|
|
640
|
+
*/
|
|
635
641
|
by?: string;
|
|
636
642
|
through?: WriteChannel;
|
|
637
643
|
}
|
|
@@ -640,18 +646,8 @@ export interface ChannelOptions {
|
|
|
640
646
|
function applyChannel(o: Opened, fields: Record<string, unknown>, opts: ChannelOptions, isNew: boolean, flag: string): Record<string, unknown> {
|
|
641
647
|
const { kind } = o.loaded;
|
|
642
648
|
const out = { ...fields };
|
|
643
|
-
if (opts.by !== undefined) {
|
|
644
|
-
if (!kind.reviews) throw new RecordWriteError("write-usage-invalid", `by names a record's author, the kind's reviews.decider field, and the ${kind.name} kind declares no reviews`);
|
|
645
|
-
if (opts.by.trim() === "") throw new RecordWriteError("write-usage-invalid", "by needs the name of the person or agent that decided");
|
|
646
|
-
const f = kind.reviews.decider;
|
|
647
|
-
if (out[f] !== undefined && out[f] !== null && out[f] !== opts.by) {
|
|
648
|
-
throw new RecordWriteError("write-input-invalid", `the fields given with ${flag} set ${f} to ${JSON.stringify(out[f])}, and by names ${JSON.stringify(opts.by)}: give one author`);
|
|
649
|
-
}
|
|
650
|
-
out[f] = opts.by;
|
|
651
|
-
}
|
|
652
649
|
const through = opts.through;
|
|
653
|
-
if (
|
|
654
|
-
if (isNew && through.opensInitial && kind.states && kind.stateField !== undefined) {
|
|
650
|
+
if (isNew && through?.opensInitial && kind.states && kind.stateField !== undefined) {
|
|
655
651
|
const first = kind.states[0];
|
|
656
652
|
const state = out[kind.stateField];
|
|
657
653
|
if (state === undefined) out[kind.stateField] = first;
|
|
@@ -662,6 +658,27 @@ function applyChannel(o: Opened, fields: Record<string, unknown>, opts: ChannelO
|
|
|
662
658
|
);
|
|
663
659
|
}
|
|
664
660
|
}
|
|
661
|
+
if (opts.by !== undefined) {
|
|
662
|
+
if (opts.by.trim() === "") throw new RecordWriteError("write-usage-invalid", "by needs the name of the person or agent that proposed or decided");
|
|
663
|
+
// A new record that opens in the kind's first state is a proposal, when the kind opts in with proposedBy (#2756):
|
|
664
|
+
// by names the proposer, kept apart from reviews.decider, which stays null until the record is decided.
|
|
665
|
+
const opensInitial = isNew && !!kind.states && kind.stateField !== undefined && (out[kind.stateField] === undefined || out[kind.stateField] === kind.states[0]);
|
|
666
|
+
if (kind.proposedBy && opensInitial) {
|
|
667
|
+
const f = kind.proposedBy.field;
|
|
668
|
+
if (out[f] !== undefined && out[f] !== null && out[f] !== opts.by) {
|
|
669
|
+
throw new RecordWriteError("write-input-invalid", `the fields given with ${flag} set ${f} to ${JSON.stringify(out[f])}, and by names ${JSON.stringify(opts.by)}: give one proposer`);
|
|
670
|
+
}
|
|
671
|
+
out[f] = opts.by;
|
|
672
|
+
} else {
|
|
673
|
+
if (!kind.reviews) throw new RecordWriteError("write-usage-invalid", `by names a record's author, the kind's reviews.decider field, and the ${kind.name} kind declares no reviews`);
|
|
674
|
+
const f = kind.reviews.decider;
|
|
675
|
+
if (out[f] !== undefined && out[f] !== null && out[f] !== opts.by) {
|
|
676
|
+
throw new RecordWriteError("write-input-invalid", `the fields given with ${flag} set ${f} to ${JSON.stringify(out[f])}, and by names ${JSON.stringify(opts.by)}: give one author`);
|
|
677
|
+
}
|
|
678
|
+
out[f] = opts.by;
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
if (!through) return out;
|
|
665
682
|
if (kind.source && (isNew || kind.source.field in out)) {
|
|
666
683
|
const f = kind.source.field;
|
|
667
684
|
const block = out[f];
|
|
@@ -984,7 +1001,7 @@ async function seal(sign: string | true, cwd: string, v: { record: string; diges
|
|
|
984
1001
|
// ── The command ──────────────────────────────────────────────────────────────
|
|
985
1002
|
|
|
986
1003
|
export const WRITE_USAGE = [
|
|
987
|
-
"chant workspace records new [<kind file or declared kind>] --from <file|-> [--prefix <prefix>] [--sign [<key file>]] [--dry-run]",
|
|
1004
|
+
"chant workspace records new [<kind file or declared kind>] --from <file|-> [--prefix <prefix>] [--by <name>] [--sign [<key file>]] [--dry-run]",
|
|
988
1005
|
"chant workspace records amend <id> [--kind <kind file>] --set <file|-> [--sign [<key file>]] [--dry-run]",
|
|
989
1006
|
"chant workspace records review <id> [--kind <kind file>] --verdict agree|dissent|abstain --by <principal> [--note <text>] [--session <id>] [--sign [<key file>]] [--dry-run]",
|
|
990
1007
|
"chant workspace records close <session id> [--kind <session kind file>] [--dry-run]",
|
|
@@ -1087,7 +1104,7 @@ export async function runRecordsWrite(ctx: CommandContext): Promise<number> {
|
|
|
1087
1104
|
if (typeof kind !== "string") return print(kind);
|
|
1088
1105
|
const input = readInput(RECORDS_NEW_SCHEMA_ID, "--from", args.migrateFrom, cwd);
|
|
1089
1106
|
if (typeof input !== "string") return print(input);
|
|
1090
|
-
return print(await newRecord({ kind, fields: input, prefix: args.prefix, sign: args.sign, dryRun: args.dryRun, cwd }));
|
|
1107
|
+
return print(await newRecord({ kind, fields: input, prefix: args.prefix, by: args.by, sign: args.sign, dryRun: args.dryRun, cwd }));
|
|
1091
1108
|
}
|
|
1092
1109
|
const schema = verb === "amend" ? RECORDS_AMEND_SCHEMA_ID : RECORDS_REVIEW_SCHEMA_ID;
|
|
1093
1110
|
const id = args.extraPositional2;
|
|
@@ -1095,6 +1112,7 @@ export async function runRecordsWrite(ctx: CommandContext): Promise<number> {
|
|
|
1095
1112
|
const kind = args.kind !== undefined ? resolveWriteKind(args.kind, cwd) : declaredWriteKind(schema, cwd, "--kind <kind file> is required");
|
|
1096
1113
|
if (typeof kind !== "string") return print(kind);
|
|
1097
1114
|
if (verb === "amend") {
|
|
1115
|
+
if (args.by !== undefined) return print(usage(schema, "--by is not taken by amend: set decided_by, or a kind's proposedBy field, with --set"));
|
|
1098
1116
|
const input = readInput(schema, "--set", args.set, cwd);
|
|
1099
1117
|
if (typeof input !== "string") return print(input);
|
|
1100
1118
|
return print(await amendRecord({ kind, id, fields: input, sign: args.sign, dryRun: args.dryRun, cwd }));
|
|
@@ -116,7 +116,7 @@
|
|
|
116
116
|
},
|
|
117
117
|
"record": {
|
|
118
118
|
"type": "object",
|
|
119
|
-
"required": ["id", "path", "state", "valid", "reasons", "supersededBy", "data"],
|
|
119
|
+
"required": ["id", "path", "state", "valid", "reasons", "supersededBy", "remediatedBy", "data"],
|
|
120
120
|
"properties": {
|
|
121
121
|
"id": { "type": ["string", "null"], "description": "Null when the record could not be parsed or has no string id. For a content-addressed kind (idFrom sha256, added by #2664), the lowercase hex SHA-256 of the file's bytes, the same string a pin of the file carries." },
|
|
122
122
|
"path": { "type": "string", "description": "From the repository root, with / separators." },
|
|
@@ -127,6 +127,11 @@
|
|
|
127
127
|
"type": ["string", "null"],
|
|
128
128
|
"description": "The id of the closed record whose supersedes link replaces this one. Derived from that record's link, never from this record's own fields."
|
|
129
129
|
},
|
|
130
|
+
"remediatedBy": {
|
|
131
|
+
"description": "Added in contract 1 by #2774. The ids of records whose remediates link names this one, in path order. Unlike supersededBy, several records may remediate the same one, and a remediation never changes this record's state or supersededBy: it stays exactly as current as it was.",
|
|
132
|
+
"type": "array",
|
|
133
|
+
"items": { "type": "string" }
|
|
134
|
+
},
|
|
130
135
|
"data": {
|
|
131
136
|
"type": ["object", "null"],
|
|
132
137
|
"description": "The record's structured core as JSON: the front matter, or for a kind of format json the whole file (#2664). It follows the kind's schema when valid is true. Null when it could not be parsed."
|
|
@@ -138,7 +143,7 @@
|
|
|
138
143
|
"items": { "$ref": "#/$defs/asset" }
|
|
139
144
|
},
|
|
140
145
|
"warnings": {
|
|
141
|
-
"description": "Added in contract 1 by #2549. Findings that leave the record valid: a pinned file that changed, went missing or did not follow a superseding decision, a supersedes link that has no effect yet, (added by #2654) an evidence list that is empty, (added by #2672) a verdict that names no digest, or (added by #2683) a work record's links and dates, or (added by #2688) a record whose author seal is not attested under a signers file at base. A work record never carries record-no-evidence; a done one with no evidence carries work-done-unpinned. valid and --current do not look at them.",
|
|
146
|
+
"description": "Added in contract 1 by #2549. Findings that leave the record valid: a pinned file that changed, went missing or did not follow a superseding decision, a supersedes link that has no effect yet, (added by #2654) an evidence list that is empty, (added by #2672) a verdict that names no digest, or (added by #2683) a work record's links and dates, or (added by #2688) a record whose author seal is not attested under a signers file at base, or (added by #2772) a work record's acceptance criteria unmet when it is done, or met by a manual verdict from its implementer. A work record never carries record-no-evidence; a done one with no evidence carries work-done-unpinned. valid and --current do not look at them.",
|
|
142
147
|
"type": "array",
|
|
143
148
|
"items": { "$ref": "#/$defs/warning" }
|
|
144
149
|
},
|
|
@@ -178,6 +183,10 @@
|
|
|
178
183
|
"type": "array",
|
|
179
184
|
"items": { "$ref": "#/$defs/workLink" }
|
|
180
185
|
},
|
|
186
|
+
"acceptance": {
|
|
187
|
+
"description": "Added in contract 1 by #2772, on every parsed record of a work kind whose work block names acceptance criteria. How many of the record's criteria passing evidence meets: evidence naming the criterion, with result pass and the criterion's verification, and for a manual criterion a by other than the implementer. Null when the record lists no criteria, so a record without them reads as before.",
|
|
188
|
+
"oneOf": [{ "type": "null" }, { "$ref": "#/$defs/workAcceptance" }]
|
|
189
|
+
},
|
|
181
190
|
"lease": {
|
|
182
191
|
"description": "Added in contract 1 by #2732, on every record of a work kind with an id, read in the working tree (absent under --at). The item's active work lease (chant workspace work claim), or null when nobody holds one live. Read from the local lease refs, refs/chant/lease/[_members/<member>/]work/<id> and the remote's as last fetched, of the ledger owning the work kind file; records never fetches.",
|
|
183
192
|
"oneOf": [{ "type": "null" }, { "$ref": "#/$defs/workLease" }]
|
|
@@ -191,6 +200,27 @@
|
|
|
191
200
|
}
|
|
192
201
|
]
|
|
193
202
|
},
|
|
203
|
+
"workAcceptance": {
|
|
204
|
+
"type": "object",
|
|
205
|
+
"required": ["met", "total", "criteria"],
|
|
206
|
+
"properties": {
|
|
207
|
+
"met": { "type": "integer", "minimum": 0, "description": "How many criteria passing evidence meets." },
|
|
208
|
+
"total": { "type": "integer", "minimum": 0, "description": "How many criteria the record lists." },
|
|
209
|
+
"criteria": {
|
|
210
|
+
"type": "array",
|
|
211
|
+
"description": "Each criterion, in the record's order.",
|
|
212
|
+
"items": {
|
|
213
|
+
"type": "object",
|
|
214
|
+
"required": ["id", "verification", "met"],
|
|
215
|
+
"properties": {
|
|
216
|
+
"id": { "type": "string" },
|
|
217
|
+
"verification": { "enum": ["unit", "integration", "e2e", "runtime", "manual"], "description": "The verification the criterion expects." },
|
|
218
|
+
"met": { "type": "boolean" }
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
},
|
|
194
224
|
"workLease": {
|
|
195
225
|
"type": "object",
|
|
196
226
|
"required": ["holder", "token", "acquiredAt", "expiresAt"],
|
|
@@ -231,6 +261,8 @@
|
|
|
231
261
|
"work-done-unpinned",
|
|
232
262
|
"work-closed-without-date",
|
|
233
263
|
"work-done-gap-open",
|
|
264
|
+
"work-acceptance-unmet",
|
|
265
|
+
"work-acceptance-self-verified",
|
|
234
266
|
"answer-points-unreadable",
|
|
235
267
|
"answer-point-unknown",
|
|
236
268
|
"answer-point-changed",
|
|
@@ -372,6 +404,8 @@
|
|
|
372
404
|
"record-id-duplicate",
|
|
373
405
|
"record-supersedes-unknown",
|
|
374
406
|
"record-supersedes-conflict",
|
|
407
|
+
"record-remediates-unknown",
|
|
408
|
+
"record-remediates-not-closed",
|
|
375
409
|
"session-seal-mismatch",
|
|
376
410
|
"session-verdict-unknown-record"
|
|
377
411
|
]
|
|
@@ -11,12 +11,17 @@ const REPO = join(import.meta.dirname, "..", "..", "..", "..");
|
|
|
11
11
|
const DECISIONS = join(REPO, "docs", "design", "decisions");
|
|
12
12
|
const SAMPLE = readFileSync(join(DECISIONS, "ws-003-seal-scope.md"), "utf-8");
|
|
13
13
|
|
|
14
|
-
/** A copy of a real decision with its id, state and
|
|
15
|
-
function decision(id: string, state = "decided", supersedes: string[] = []): string {
|
|
14
|
+
/** A copy of a real decision with its id, state, supersedes and remediates links replaced. */
|
|
15
|
+
function decision(id: string, state = "decided", supersedes: string[] = [], remediates: string[] = []): string {
|
|
16
16
|
const links = supersedes.length === 0 ? "supersedes: []" : `supersedes:\n${supersedes.map((d) => ` - decision: "${d}"`).join("\n")}`;
|
|
17
|
-
|
|
17
|
+
let text = SAMPLE.replace(/^id: .*$/m, `id: "${id}"`)
|
|
18
18
|
.replace(/^state: .*$/m, `state: "${state}"`)
|
|
19
19
|
.replace(/^supersedes:(?: \[\])?\n(?: .*\n)*/m, `${links}\n`);
|
|
20
|
+
if (remediates.length > 0) {
|
|
21
|
+
const block = `remediates:\n${remediates.map((d) => ` - decision: "${d}"`).join("\n")}\n`;
|
|
22
|
+
text = text.replace(/^evidence:/m, `${block}evidence:`);
|
|
23
|
+
}
|
|
24
|
+
return text;
|
|
20
25
|
}
|
|
21
26
|
|
|
22
27
|
let dir: string;
|
|
@@ -235,6 +240,46 @@ describe("readRecords", () => {
|
|
|
235
240
|
expect(codes(records[2])).toEqual(["record-supersedes-conflict"]);
|
|
236
241
|
});
|
|
237
242
|
|
|
243
|
+
test("a remediates link to a missing id is record-remediates-unknown", async () => {
|
|
244
|
+
write("ws-002-b.md", decision("ws-002", "decided", [], ["ws-404"]));
|
|
245
|
+
const [r] = (await read()).records;
|
|
246
|
+
expect(codes(r)).toEqual(["record-remediates-unknown"]);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
test("a remediates link to an open record is record-remediates-not-closed: a record still open is amended instead", async () => {
|
|
250
|
+
write("ws-001-a.md", decision("ws-001", "decided"));
|
|
251
|
+
write("ws-002-b.md", decision("ws-002", "ratified", [], ["ws-001"]));
|
|
252
|
+
const [ws001, ws002] = (await read()).records;
|
|
253
|
+
expect(codes(ws002)).toEqual(["record-remediates-not-closed"]);
|
|
254
|
+
expect(ws001.remediatedBy).toEqual([]);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
test("remediation leaves the target current and its state unchanged, and several records may remediate the same one (#2774)", async () => {
|
|
258
|
+
write("ws-001-a.md", decision("ws-001", "ratified"));
|
|
259
|
+
write("ws-002-b.md", decision("ws-002", "ratified", [], ["ws-001"]));
|
|
260
|
+
write("ws-003-c.md", decision("ws-003", "ratified", [], ["ws-001"]));
|
|
261
|
+
const all = await read();
|
|
262
|
+
const [ws001] = all.records;
|
|
263
|
+
expect(ws001.state).toBe("ratified");
|
|
264
|
+
expect(ws001.supersededBy).toBeNull();
|
|
265
|
+
expect(ws001.remediatedBy).toEqual(["ws-002", "ws-003"]);
|
|
266
|
+
expect(all.summary.superseded).toBe(0);
|
|
267
|
+
const current = await read({ current: true });
|
|
268
|
+
expect(current.records.map((r) => r.id)).toEqual(["ws-001", "ws-002", "ws-003"]);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
test("remediates and supersedes are independent links, and a record may carry both to different targets (#2774)", async () => {
|
|
272
|
+
write("ws-001-a.md", decision("ws-001", "ratified"));
|
|
273
|
+
write("ws-002-b.md", decision("ws-002", "ratified"));
|
|
274
|
+
write("ws-003-c.md", decision("ws-003", "ratified", ["ws-002"], ["ws-001"]));
|
|
275
|
+
const all = await read();
|
|
276
|
+
expect(all.records.map((r) => [r.id, r.supersededBy, r.remediatedBy])).toEqual([
|
|
277
|
+
["ws-001", null, ["ws-003"]],
|
|
278
|
+
["ws-002", "ws-003", []],
|
|
279
|
+
["ws-003", null, []],
|
|
280
|
+
]);
|
|
281
|
+
});
|
|
282
|
+
|
|
238
283
|
test("a missing records directory is location-missing", async () => {
|
|
239
284
|
const loaded = await loadRecordKind(join(dir, "decisions", "decision.kind.mjs"));
|
|
240
285
|
const moved: LoadedRecordKind = { ...loaded, dir: join(dir, "nowhere") };
|
|
@@ -266,7 +311,7 @@ describe("a decision that originates in the workspace (#2654)", () => {
|
|
|
266
311
|
|
|
267
312
|
test("takes a session and an issue, and nothing else", async () => {
|
|
268
313
|
const base = workspaceDecision("ws-001");
|
|
269
|
-
write("ws-001-a.md", base.replace(' member: "app"\n', ' member: "app"\n session: "S-0001"\n issue: "
|
|
314
|
+
write("ws-001-a.md", base.replace(' member: "app"\n', ' member: "app"\n session: "S-0001"\n issue: "acme/example#77"\n'));
|
|
270
315
|
write("ws-002-b.md", base.replace('id: "ws-001"', 'id: "ws-002"').replace(' member: "app"\n', ' member: "app"\n session: null\n'));
|
|
271
316
|
write("ws-003-c.md", base.replace('id: "ws-001"', 'id: "ws-003"').replace(' member: "app"\n', ' member: "app"\n row: "Sort order"\n'));
|
|
272
317
|
write("ws-004-d.md", base.replace('id: "ws-001"', 'id: "ws-004"').replace(' member: "app"\n', ""));
|