@intentius/chant 0.91.0 → 0.93.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/commands/build.d.ts.map +1 -1
- package/dist/cli/commands/lint.d.ts.map +1 -1
- package/dist/cli/handlers/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-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/changes-cli.d.ts +16 -0
- package/dist/workspace/changes-cli.d.ts.map +1 -0
- package/dist/workspace/changes.d.ts +200 -0
- package/dist/workspace/changes.d.ts.map +1 -0
- package/dist/workspace/chant-migrations/chud-lexicon-exit-fly-site.d.ts +59 -0
- package/dist/workspace/chant-migrations/chud-lexicon-exit-fly-site.d.ts.map +1 -0
- package/dist/workspace/chant-migrations/chud-lexicon-exit-live-names.d.ts +51 -0
- package/dist/workspace/chant-migrations/chud-lexicon-exit-live-names.d.ts.map +1 -0
- package/dist/workspace/chant-migrations/chud-lexicon-exit-rollback.d.ts +26 -0
- package/dist/workspace/chant-migrations/chud-lexicon-exit-rollback.d.ts.map +1 -0
- package/dist/workspace/chant-migrations/chud-lexicon-exit-ship-inputs.d.ts +59 -0
- package/dist/workspace/chant-migrations/chud-lexicon-exit-ship-inputs.d.ts.map +1 -0
- package/dist/workspace/chant-migrations/chud-lexicon-exit-ship-policy.d.ts +22 -0
- package/dist/workspace/chant-migrations/chud-lexicon-exit-ship-policy.d.ts.map +1 -0
- package/dist/workspace/chant-migrations/chud-lexicon-exit.d.ts +71 -0
- package/dist/workspace/chant-migrations/chud-lexicon-exit.d.ts.map +1 -0
- package/dist/workspace/chant-migrations.d.ts +99 -0
- package/dist/workspace/chant-migrations.d.ts.map +1 -0
- package/dist/workspace/checks/boxes.d.ts +34 -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 +3 -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/declaration.d.ts +52 -0
- package/dist/workspace/declaration.d.ts.map +1 -1
- package/dist/workspace/declaration.schema.json +114 -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.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.map +1 -1
- package/dist/workspace/points.d.ts +1 -1
- package/dist/workspace/reason-codes.d.ts +14 -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 +59 -1
- package/dist/workspace/status-stewards.d.ts.map +1 -1
- package/dist/workspace/status.d.ts +16 -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 +19 -0
- 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/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 +300 -0
- package/src/op/activities/decide.ts +267 -0
- package/src/op/activities/index.ts +21 -0
- package/src/op/activities/shell.test.ts +91 -1
- package/src/op/activities/shell.ts +91 -2
- package/src/op/activities/source-release.test.ts +118 -0
- package/src/op/activities/source-release.ts +168 -0
- package/src/op/activities/source-rollback.test.ts +131 -0
- package/src/op/activities/source-rollback.ts +239 -0
- package/src/op/activities/work-evidence.ts +62 -0
- package/src/op/activity-contract-registry.test.ts +2 -0
- package/src/op/activity-contract-registry.ts +2 -2
- package/src/op/activity-contract.test.ts +12 -0
- package/src/op/activity-contract.ts +6 -1
- package/src/op/activity-profiles.ts +29 -0
- package/src/op/activity-registry.test.ts +1 -1
- package/src/op/activity-registry.ts +2 -2
- package/src/op/builders.ts +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 +313 -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/changes-cli.ts +56 -0
- package/src/workspace/changes.schema.json +472 -0
- package/src/workspace/changes.test.ts +315 -0
- package/src/workspace/changes.ts +512 -0
- package/src/workspace/chant-migrations/chud-lexicon-exit-fly-site.ts +234 -0
- package/src/workspace/chant-migrations/chud-lexicon-exit-live-names.ts +164 -0
- package/src/workspace/chant-migrations/chud-lexicon-exit-rollback.ts +223 -0
- package/src/workspace/chant-migrations/chud-lexicon-exit-ship-inputs.ts +195 -0
- package/src/workspace/chant-migrations/chud-lexicon-exit-ship-policy.ts +108 -0
- package/src/workspace/chant-migrations/chud-lexicon-exit.ts +882 -0
- package/src/workspace/chant-migrations.e2e.test.ts +1089 -0
- package/src/workspace/chant-migrations.ts +164 -0
- package/src/workspace/check-contract.test.ts +4 -2
- package/src/workspace/check.schema.json +15 -5
- package/src/workspace/checks/boxes.test.ts +80 -1
- package/src/workspace/checks/boxes.ts +136 -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 +8 -1
- 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/declaration.schema.json +114 -1
- package/src/workspace/declaration.test.ts +58 -0
- package/src/workspace/declaration.ts +104 -0
- package/src/workspace/declared-kinds.test.ts +5 -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.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 +14 -3
- package/src/workspace/points-write.schema.json +11 -1
- package/src/workspace/points.schema.json +33 -0
- package/src/workspace/points.test.ts +1 -1
- package/src/workspace/points.ts +1 -1
- package/src/workspace/read-contract.test.ts +1 -1
- package/src/workspace/reason-codes.test.ts +11 -1
- package/src/workspace/reason-codes.ts +18 -2
- package/src/workspace/records-amend.schema.json +2 -0
- package/src/workspace/records-cli.ts +13 -3
- package/src/workspace/records-close.schema.json +2 -0
- package/src/workspace/records-new.schema.json +2 -0
- package/src/workspace/records-review.schema.json +2 -0
- package/src/workspace/records-sessions-write.e2e.test.ts +77 -0
- package/src/workspace/records-sessions-write.test.ts +2 -34
- package/src/workspace/records-write.e2e.test.ts +108 -0
- package/src/workspace/records-write.test.ts +32 -41
- package/src/workspace/records-write.ts +33 -15
- package/src/workspace/records.schema.json +36 -2
- package/src/workspace/records.test.ts +48 -3
- package/src/workspace/records.ts +109 -3
- package/src/workspace/status-contract.test.ts +13 -8
- package/src/workspace/status-gates.ts +9 -4
- package/src/workspace/status-stewards.ts +108 -4
- package/src/workspace/status.schema.json +111 -7
- package/src/workspace/status.ts +36 -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.test.ts +3 -2
- package/src/workspace/work.ts +100 -1
- /package/src/cli/{discovery-skip.test.ts → discovery-skip.e2e.test.ts} +0 -0
- /package/src/workspace/conformance/{conformance.test.ts → conformance.e2e.test.ts} +0 -0
- /package/src/workspace/{which-chant.test.ts → which-chant.e2e.test.ts} +0 -0
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Acceptance criteria on work records (#2772), on a workspace built from the
|
|
3
|
+
* reference workspace's decision and work kinds in a throwaway git repository.
|
|
4
|
+
*
|
|
5
|
+
* - W-001 is done, and passing evidence of each criterion's verification
|
|
6
|
+
* meets both of them, the manual one by bob while alice owns the item: met.
|
|
7
|
+
* - W-002 is done, and its only evidence is a unit test for an e2e
|
|
8
|
+
* criterion: unmet.
|
|
9
|
+
* - W-003 is done, and its manual criterion's verdict is by alice, its owner:
|
|
10
|
+
* the verdict does not count, so it is unmet as well.
|
|
11
|
+
* - W-004 is done and lists no criteria: a legacy item, read as before.
|
|
12
|
+
* - W-005 is in progress. A steward's Op attaches evidence to it under its
|
|
13
|
+
* work lease, and is refused on the manual criterion.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { readFileSync } from "node:fs";
|
|
17
|
+
import { join } from "node:path";
|
|
18
|
+
import { afterAll, beforeAll, describe, expect, test } from "vitest";
|
|
19
|
+
import { cleanScratch, contract, git, REPO, repo } from "./__fixtures__/contract-repo";
|
|
20
|
+
import { runDeclarationChecks } from "./checks";
|
|
21
|
+
import { listWorkspaceWithKinds } from "./ls";
|
|
22
|
+
import lsSchema from "./ls.schema.json";
|
|
23
|
+
import { queryRecords } from "./records-cli";
|
|
24
|
+
import recordsSchema from "./records.schema.json";
|
|
25
|
+
import { workspaceStatus } from "./status";
|
|
26
|
+
import statusSchema from "./status.schema.json";
|
|
27
|
+
import { attachWorkEvidence } from "./work-evidence";
|
|
28
|
+
import workEvidenceSchema from "./work-evidence.schema.json";
|
|
29
|
+
import { workEvidence } from "../op/builders";
|
|
30
|
+
import { workEvidence as workEvidenceActivity } from "../op/activities";
|
|
31
|
+
import { ACTIVITY_PROFILES } from "../op/activity-profiles";
|
|
32
|
+
import { OpRunFailure, runOpLocally, type OpRunResult } from "../op/local-executor";
|
|
33
|
+
import type { ActivityFn } from "../op/activity-registry";
|
|
34
|
+
import type { OpConfig } from "../op/types";
|
|
35
|
+
import { declareSteward } from "../op/steward";
|
|
36
|
+
import { claimWorkLease, releaseWorkLease } from "../lifecycle/work-lease";
|
|
37
|
+
|
|
38
|
+
const REF = join(REPO, "reference-workspace");
|
|
39
|
+
const ref = (path: string) => readFileSync(join(REF, path), "utf-8");
|
|
40
|
+
|
|
41
|
+
function work(id: string, fields: Record<string, unknown>): string {
|
|
42
|
+
const data = {
|
|
43
|
+
schema: 1,
|
|
44
|
+
id,
|
|
45
|
+
title: `Work ${id}`,
|
|
46
|
+
state: "done",
|
|
47
|
+
implements: [],
|
|
48
|
+
needs: [],
|
|
49
|
+
constrains: ["member:app"],
|
|
50
|
+
evidence: [],
|
|
51
|
+
owner: "alice",
|
|
52
|
+
opened_on: "2026-09-25",
|
|
53
|
+
closed_on: "2026-09-25",
|
|
54
|
+
source: { kind: "workspace", member: "app" },
|
|
55
|
+
supersedes: [],
|
|
56
|
+
...fields,
|
|
57
|
+
};
|
|
58
|
+
return `---\n${JSON.stringify(data, null, 2)}\n---\n\n# ${id}\n`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const link = (criterion: string, verification: string, result: string, extra: Record<string, unknown> = {}) => ({
|
|
62
|
+
title: `${criterion} ${verification}`,
|
|
63
|
+
url: `https://example.com/${criterion}`,
|
|
64
|
+
criterion,
|
|
65
|
+
verification,
|
|
66
|
+
result,
|
|
67
|
+
...extra,
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
const KIND = "work/work.kind.mjs";
|
|
71
|
+
let root: string;
|
|
72
|
+
|
|
73
|
+
beforeAll(() => {
|
|
74
|
+
root = repo({
|
|
75
|
+
"chant.workspace.json": JSON.stringify(
|
|
76
|
+
{
|
|
77
|
+
name: "studio",
|
|
78
|
+
schema: 1,
|
|
79
|
+
members: [{ name: "app", dir: "app", kind: "other", because: "a plain Node server" }],
|
|
80
|
+
records: [{ kind: "decisions/decision.kind.mjs" }, { kind: KIND }],
|
|
81
|
+
},
|
|
82
|
+
null,
|
|
83
|
+
2,
|
|
84
|
+
),
|
|
85
|
+
"app/server.mjs": "export const port = 8080;\n",
|
|
86
|
+
"decisions/decision.kind.mjs": ref("decisions/decision.kind.mjs"),
|
|
87
|
+
"decisions/decision.schema.json": ref("decisions/decision.schema.json"),
|
|
88
|
+
"decisions/ref-001-how-the-app-is-deployed.md": ref("decisions/ref-001-how-the-app-is-deployed.md"),
|
|
89
|
+
[KIND]: ref(KIND),
|
|
90
|
+
"work/work.schema.json": ref("work/work.schema.json"),
|
|
91
|
+
"work/W-001-met.md": work("W-001", {
|
|
92
|
+
acceptance: [
|
|
93
|
+
{ id: "AC-1", text: "The server answers on its port", verification: "unit" },
|
|
94
|
+
{ id: "AC-2", text: "A designer finds the page matches the spec", verification: "manual" },
|
|
95
|
+
],
|
|
96
|
+
evidence: [link("AC-1", "unit", "fail"), link("AC-1", "unit", "pass"), link("AC-2", "manual", "pass", { by: "bob" })],
|
|
97
|
+
}),
|
|
98
|
+
"work/W-002-unmet.md": work("W-002", {
|
|
99
|
+
acceptance: [{ id: "AC-1", text: "The page loads in a browser", verification: "e2e" }],
|
|
100
|
+
evidence: [link("AC-1", "unit", "pass")],
|
|
101
|
+
}),
|
|
102
|
+
"work/W-003-self.md": work("W-003", {
|
|
103
|
+
acceptance: [{ id: "AC-1", text: "Someone else signs off", verification: "manual" }],
|
|
104
|
+
evidence: [link("AC-1", "manual", "pass", { by: "Alice" })],
|
|
105
|
+
}),
|
|
106
|
+
"work/W-004-legacy.md": work("W-004", { evidence: [{ title: "The review", url: "https://example.com/review" }] }),
|
|
107
|
+
"work/W-005-leased.md": work("W-005", {
|
|
108
|
+
state: "in-progress",
|
|
109
|
+
closed_on: null,
|
|
110
|
+
acceptance: [
|
|
111
|
+
{ id: "AC-1", text: "The server's test passes", verification: "unit" },
|
|
112
|
+
{ id: "AC-2", text: "A person tries it", verification: "manual" },
|
|
113
|
+
],
|
|
114
|
+
}),
|
|
115
|
+
});
|
|
116
|
+
git(root, "config", "user.name", "t");
|
|
117
|
+
git(root, "config", "user.email", "t@example.com");
|
|
118
|
+
git(root, "config", "commit.gpgsign", "false");
|
|
119
|
+
git(root, "add", "-A");
|
|
120
|
+
git(root, "commit", "-q", "-m", "the work");
|
|
121
|
+
});
|
|
122
|
+
afterAll(cleanScratch);
|
|
123
|
+
|
|
124
|
+
const records = contract(recordsSchema);
|
|
125
|
+
|
|
126
|
+
async function read() {
|
|
127
|
+
const doc = await queryRecords({ kind: join(root, KIND), cwd: root, workGaps: false });
|
|
128
|
+
records.expectValid(doc);
|
|
129
|
+
if ("error" in doc || !("records" in doc)) throw new Error(JSON.stringify(doc));
|
|
130
|
+
return doc.records;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const codes = (r: { warnings: { code: string }[] }) => r.warnings.map((w) => w.code);
|
|
134
|
+
|
|
135
|
+
describe("acceptance criteria on read (#2772)", () => {
|
|
136
|
+
test("met: each criterion has passing evidence of its verification, and the manual verdict is someone else's", async () => {
|
|
137
|
+
const w1 = (await read()).find((r) => r.id === "W-001")!;
|
|
138
|
+
expect(w1.valid).toBe(true);
|
|
139
|
+
expect(w1.acceptance).toEqual({
|
|
140
|
+
met: 2,
|
|
141
|
+
total: 2,
|
|
142
|
+
criteria: [
|
|
143
|
+
{ id: "AC-1", verification: "unit", met: true },
|
|
144
|
+
{ id: "AC-2", verification: "manual", met: true },
|
|
145
|
+
],
|
|
146
|
+
});
|
|
147
|
+
expect(codes(w1)).toEqual([]);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test("unmet: evidence of another verification does not count, and the done item is warned", async () => {
|
|
151
|
+
const w2 = (await read()).find((r) => r.id === "W-002")!;
|
|
152
|
+
expect(w2.acceptance).toMatchObject({ met: 0, total: 1 });
|
|
153
|
+
expect(codes(w2)).toEqual(["work-acceptance-unmet"]);
|
|
154
|
+
expect(w2.warnings[0].message).toMatch(/AC-1 \(e2e\)/);
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test("manual by the implementer: the verdict is refused, warned, and the criterion stays unmet", async () => {
|
|
158
|
+
const w3 = (await read()).find((r) => r.id === "W-003")!;
|
|
159
|
+
expect(w3.acceptance).toMatchObject({ met: 0, total: 1 });
|
|
160
|
+
expect(codes(w3)).toEqual(["work-acceptance-self-verified", "work-acceptance-unmet"]);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test("a legacy item without criteria is valid, reads acceptance null and gets no acceptance warning", async () => {
|
|
164
|
+
const w4 = (await read()).find((r) => r.id === "W-004")!;
|
|
165
|
+
expect(w4.valid).toBe(true);
|
|
166
|
+
expect(w4.acceptance).toBeNull();
|
|
167
|
+
expect(codes(w4)).toEqual([]);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
test("an open item with criteria unmet is not warned: only done needs them met", async () => {
|
|
171
|
+
const w5 = (await read()).find((r) => r.id === "W-005")!;
|
|
172
|
+
expect(w5.acceptance).toMatchObject({ met: 0, total: 2 });
|
|
173
|
+
expect(codes(w5)).toEqual([]);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
test("check fails with WSP117 and the reason code on each done item with a criterion unmet", async () => {
|
|
177
|
+
const report = await runDeclarationChecks(root);
|
|
178
|
+
const wsp117 = report.diagnostics.filter((d) => d.ruleId === "WSP117");
|
|
179
|
+
expect(wsp117.map((d) => [d.file, d.code, d.severity])).toEqual([
|
|
180
|
+
["work/W-002-unmet.md", "work-acceptance-unmet", "error"],
|
|
181
|
+
["work/W-003-self.md", "work-acceptance-unmet", "error"],
|
|
182
|
+
]);
|
|
183
|
+
expect(report.ok).toBe(false);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test("ls --json and status --json count each item's criteria, met of total, in the read contract", async () => {
|
|
187
|
+
const ls = await listWorkspaceWithKinds({ cwd: root });
|
|
188
|
+
contract(lsSchema).expectValid(ls);
|
|
189
|
+
if ("error" in ls) throw new Error(ls.error.message);
|
|
190
|
+
const kinds = Object.fromEntries(ls.workspace.records.map((r) => [r.path, r.acceptance]));
|
|
191
|
+
expect(kinds["decisions/decision.kind.mjs"]).toBeNull();
|
|
192
|
+
expect(kinds[KIND]).toEqual([
|
|
193
|
+
{ item: "W-001", state: "done", met: 2, total: 2 },
|
|
194
|
+
{ item: "W-002", state: "done", met: 0, total: 1 },
|
|
195
|
+
{ item: "W-003", state: "done", met: 0, total: 1 },
|
|
196
|
+
{ item: "W-005", state: "in-progress", met: 0, total: 2 },
|
|
197
|
+
]);
|
|
198
|
+
|
|
199
|
+
const status = await workspaceStatus({ cwd: root, env: "dev" });
|
|
200
|
+
contract(statusSchema).expectValid(status);
|
|
201
|
+
if ("error" in status) throw new Error(status.error.message);
|
|
202
|
+
expect(status.acceptance.map((a) => [a.member, a.kind, a.item, `${a.met}/${a.total}`])).toEqual([
|
|
203
|
+
[null, KIND, "W-001", "2/2"],
|
|
204
|
+
[null, KIND, "W-002", "0/1"],
|
|
205
|
+
[null, KIND, "W-003", "0/1"],
|
|
206
|
+
[null, KIND, "W-005", "0/2"],
|
|
207
|
+
]);
|
|
208
|
+
});
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
describe("a steward Op under workLease attaches evidence to a criterion (#2772)", () => {
|
|
212
|
+
const evidenceDoc = contract(workEvidenceSchema);
|
|
213
|
+
|
|
214
|
+
async function run(config: OpConfig, holder: string): Promise<OpRunResult> {
|
|
215
|
+
const activities = new Map<string, ActivityFn>([["workEvidence", workEvidenceActivity as unknown as ActivityFn]]);
|
|
216
|
+
try {
|
|
217
|
+
return await runOpLocally(config, activities, ACTIVITY_PROFILES, undefined, { cwd: root, ledger: { cwd: root }, work: { holder } });
|
|
218
|
+
} catch (err) {
|
|
219
|
+
if (err instanceof OpRunFailure) return err.result;
|
|
220
|
+
throw err;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const op = (step: ReturnType<typeof workEvidence>): OpConfig => ({
|
|
225
|
+
name: "verify",
|
|
226
|
+
overview: "attach the test run to its criterion",
|
|
227
|
+
workLease: { item: "W-005", kind: KIND },
|
|
228
|
+
phases: [{ name: "Verify", steps: [step] }],
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
test("the run holding the lease appends passing evidence, and the criterion is met", async () => {
|
|
232
|
+
const config = op(workEvidence("AC-1", { result: "pass", title: "the server test", url: "https://example.com/ci/1", cwd: root }));
|
|
233
|
+
expect(declareSteward({ name: "box", ops: [config] }).ops).toHaveLength(1);
|
|
234
|
+
const result = await run(config, "box/verify@h1");
|
|
235
|
+
expect(result.status).toBe("ok");
|
|
236
|
+
expect(result.workLease).toMatchObject({ item: "W-005", released: true });
|
|
237
|
+
const w5 = (await read()).find((r) => r.id === "W-005")!;
|
|
238
|
+
expect(w5.acceptance).toMatchObject({ met: 1, total: 2 });
|
|
239
|
+
expect(w5.data!.evidence).toEqual([
|
|
240
|
+
expect.objectContaining({ url: "https://example.com/ci/1", criterion: "AC-1", verification: "unit", result: "pass", by: "box/verify@h1" }),
|
|
241
|
+
]);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
test("a manual criterion is refused: the run holding the lease is the implementer", async () => {
|
|
245
|
+
const result = await run(op(workEvidence("AC-2", { result: "pass", title: "tried it", url: "https://example.com/tried", cwd: root })), "box/verify@h2");
|
|
246
|
+
expect(result.status).toBe("fail");
|
|
247
|
+
expect(JSON.stringify(result.records)).toMatch(/work-acceptance-self-verified/);
|
|
248
|
+
expect((await read()).find((r) => r.id === "W-005")!.acceptance).toMatchObject({ met: 1, total: 2 });
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
test("without the live lease, or with another token or criterion, nothing is written", async () => {
|
|
252
|
+
const ask = (over: Partial<Parameters<typeof attachWorkEvidence>[0]>) =>
|
|
253
|
+
attachWorkEvidence({ cwd: root, item: "W-005", holder: "me", token: "t", criterion: "AC-1", result: "pass", title: "x", url: "https://example.com/x", ...over });
|
|
254
|
+
const unleased = await ask({});
|
|
255
|
+
evidenceDoc.expectValid(unleased);
|
|
256
|
+
expect("error" in unleased && unleased.error.code).toBe("lease-not-held");
|
|
257
|
+
|
|
258
|
+
const claimed = await claimWorkLease("W-005", "me", { cwd: join(root, "work") });
|
|
259
|
+
if (!claimed.ok) throw new Error(claimed.message);
|
|
260
|
+
try {
|
|
261
|
+
expect(await ask({ token: "other" })).toMatchObject({ error: { code: "lease-token-mismatch" } });
|
|
262
|
+
expect(await ask({ holder: "you", token: claimed.lease.token })).toMatchObject({ error: { code: "lease-held" } });
|
|
263
|
+
expect(await ask({ token: claimed.lease.token, criterion: "AC-9" })).toMatchObject({ error: { code: "work-criterion-unknown" } });
|
|
264
|
+
const ok = await ask({ token: claimed.lease.token, result: "fail" });
|
|
265
|
+
evidenceDoc.expectValid(ok);
|
|
266
|
+
expect(ok).toMatchObject({ item: "W-005", kind: KIND, path: "work/W-005-leased.md", acceptance: { met: 1, total: 2 } });
|
|
267
|
+
} finally {
|
|
268
|
+
await releaseWorkLease("W-005", "me", { cwd: join(root, "work"), token: claimed.lease.token });
|
|
269
|
+
}
|
|
270
|
+
expect(await attachWorkEvidence({ cwd: root, item: "W-004", holder: "me", token: "t", criterion: "AC-1", result: "pass", title: "x", url: "https://example.com/x" })).toMatchObject({
|
|
271
|
+
error: { code: "work-item-closed" },
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
});
|
|
@@ -158,7 +158,7 @@ describe("chant workspace work (#2732)", () => {
|
|
|
158
158
|
expect(json(closed).error.code).toBe("work-item-closed");
|
|
159
159
|
const verb = await chant(root, ["workspace", "work", "grab", "W-001", "--holder", "a"]);
|
|
160
160
|
expect(verb.code).toBe(1);
|
|
161
|
-
expect(verb.stderr).toContain("claim, renew or
|
|
161
|
+
expect(verb.stderr).toContain("claim, renew, release or history");
|
|
162
162
|
const noHolder = await chant(root, ["workspace", "work", "claim", "W-001"]);
|
|
163
163
|
expect(noHolder.code).toBe(1);
|
|
164
164
|
expect(noHolder.stderr).toContain("--holder");
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `chant workspace work claim|renew|release <id> --holder <name>` (#2732,
|
|
3
|
-
* ws-055): the work lease, who is working on which work item.
|
|
3
|
+
* ws-055): the work lease, who is working on which work item. And `chant
|
|
4
|
+
* workspace work history <id>` (#2785): the item's lease history, each claim
|
|
5
|
+
* and how it ended, a read-contract output (`work-history.schema.json`).
|
|
4
6
|
*
|
|
5
7
|
* The lease itself is `../lifecycle/work-lease.ts`, the operator lease under
|
|
6
8
|
* the key `work/<id>`. This file finds the work item first: in the work kind
|
|
@@ -24,6 +26,9 @@ import type { CommandContext } from "../cli/registry";
|
|
|
24
26
|
import { parseDuration } from "../op/duration";
|
|
25
27
|
import {
|
|
26
28
|
claimWorkLease,
|
|
29
|
+
leaseHistoryPath,
|
|
30
|
+
listWorkLeases,
|
|
31
|
+
readLeaseHistory,
|
|
27
32
|
releaseWorkLease,
|
|
28
33
|
renewWorkLease,
|
|
29
34
|
workLeaseRef,
|
|
@@ -32,11 +37,13 @@ import {
|
|
|
32
37
|
type WorkLeaseRefusal,
|
|
33
38
|
type WorkLeaseResult,
|
|
34
39
|
type LeaseHistoryWrite,
|
|
40
|
+
type LeaseHistoryRecord,
|
|
41
|
+
type WorkLeaseState,
|
|
35
42
|
} from "../lifecycle/work-lease";
|
|
36
43
|
import { resolveMemberLedger } from "../lifecycle/member-ledger";
|
|
37
|
-
import { WorkspaceReadError } from "./declaration";
|
|
44
|
+
import { readerVersion, WorkspaceReadError } from "./declaration";
|
|
38
45
|
import { declaredKindFiles, readRecordsFor } from "./records-cli";
|
|
39
|
-
import { loadRecordKind, RecordReadError, type LoadedRecordKind } from "./records";
|
|
46
|
+
import { loadRecordKind, RecordReadError, type LoadedRecordKind, type RecordEntry } from "./records";
|
|
40
47
|
import { gitRoot } from "./record-source";
|
|
41
48
|
import type { ReasonCode } from "./reason-codes";
|
|
42
49
|
|
|
@@ -48,7 +55,10 @@ export const WORK_LEASE_OUTPUT_SCHEMA_ID = "https://intentius.io/chant/schemas/w
|
|
|
48
55
|
|
|
49
56
|
export { WORK_LEASE_REFUSALS } from "../lifecycle/work-lease";
|
|
50
57
|
|
|
51
|
-
const USAGE =
|
|
58
|
+
const USAGE = [
|
|
59
|
+
"chant workspace work claim|renew|release <id> --holder <name> [--kind <kind file>] [--ttl <seconds|duration>] [--token <token>] [--outcome <text>] [--note <text>] [--json]",
|
|
60
|
+
"chant workspace work history <id> [--kind <kind file>] [--json]",
|
|
61
|
+
].join("\n");
|
|
52
62
|
|
|
53
63
|
/** Exit code when the lease was refused rather than the command failing. */
|
|
54
64
|
export const EXIT_LEASE_REFUSED = 2;
|
|
@@ -94,7 +104,8 @@ export type WorkLeaseDocument = DocHead &
|
|
|
94
104
|
| { error: { code: WorkErrorCode; message: string } }
|
|
95
105
|
);
|
|
96
106
|
|
|
97
|
-
|
|
107
|
+
/** Why a work command could not run, with its code. */
|
|
108
|
+
export class WorkError extends Error {
|
|
98
109
|
constructor(
|
|
99
110
|
readonly code: WorkErrorCode,
|
|
100
111
|
message: string,
|
|
@@ -103,8 +114,19 @@ class WorkError extends Error {
|
|
|
103
114
|
}
|
|
104
115
|
}
|
|
105
116
|
|
|
106
|
-
/**
|
|
107
|
-
|
|
117
|
+
/** A work item found by id: its kind, its state, its record, and the repository root its path is from. */
|
|
118
|
+
export interface FoundWorkItem {
|
|
119
|
+
loaded: LoadedRecordKind;
|
|
120
|
+
state: string | null;
|
|
121
|
+
record: RecordEntry;
|
|
122
|
+
root: string;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* The work kind holding `id`: the one `kind` names, or the one declared work
|
|
127
|
+
* kind with a record `id`. Throws a {@link WorkError}.
|
|
128
|
+
*/
|
|
129
|
+
export async function findWorkItem(id: string, cwd: string, kind: string | undefined): Promise<FoundWorkItem> {
|
|
108
130
|
let candidates: string[];
|
|
109
131
|
if (kind !== undefined) {
|
|
110
132
|
candidates = [kind];
|
|
@@ -116,7 +138,7 @@ async function findWorkItem(id: string, cwd: string, kind: string | undefined):
|
|
|
116
138
|
throw new WorkError("work-kind-missing", `${err.code}: ${err.describe()}; without --kind, the declaration names the work kind`);
|
|
117
139
|
}
|
|
118
140
|
}
|
|
119
|
-
const found:
|
|
141
|
+
const found: FoundWorkItem[] = [];
|
|
120
142
|
let workKinds = 0;
|
|
121
143
|
for (const file of candidates) {
|
|
122
144
|
let loaded: LoadedRecordKind;
|
|
@@ -140,7 +162,7 @@ async function findWorkItem(id: string, cwd: string, kind: string | undefined):
|
|
|
140
162
|
throw err;
|
|
141
163
|
}
|
|
142
164
|
const record = read.result.records.find((r) => r.id === id);
|
|
143
|
-
if (record) found.push({ loaded, state: record.state, root: read.root });
|
|
165
|
+
if (record) found.push({ loaded, state: record.state, record, root: read.root });
|
|
144
166
|
}
|
|
145
167
|
if (workKinds === 0) throw new WorkError("work-kind-missing", "the declaration names no work kind; pass --kind <kind file>");
|
|
146
168
|
if (found.length === 0) throw new WorkError("work-item-unknown", `no work record has the id ${id}`);
|
|
@@ -199,16 +221,177 @@ function strip(l: WorkLease): Omit<WorkLease, "item"> {
|
|
|
199
221
|
return { holder: l.holder, token: l.token, acquiredAt: l.acquiredAt, expiresAt: l.expiresAt };
|
|
200
222
|
}
|
|
201
223
|
|
|
224
|
+
// ── work history (#2785) ────────────────────────────────────────────────────
|
|
225
|
+
|
|
226
|
+
/** The version of the `work history` output this chant writes: read contract 1. */
|
|
227
|
+
export const WORK_HISTORY_CONTRACT_VERSION = 1;
|
|
228
|
+
|
|
229
|
+
/** `$id` of the JSON Schema for `work history --json`, shipped beside this file. */
|
|
230
|
+
export const WORK_HISTORY_OUTPUT_SCHEMA_ID = "https://intentius.io/chant/schemas/workspace/work-history/v1/work-history.schema.json";
|
|
231
|
+
|
|
232
|
+
/** Why `work history` read nothing. Closed. */
|
|
233
|
+
export const WORK_HISTORY_ERROR_CODES = [
|
|
234
|
+
"kind-unreadable",
|
|
235
|
+
"work-kind-missing",
|
|
236
|
+
"work-kind-ambiguous",
|
|
237
|
+
"work-item-unknown",
|
|
238
|
+
"not-a-git-repository",
|
|
239
|
+
] as const satisfies readonly ReasonCode[];
|
|
240
|
+
export type WorkHistoryErrorCode = (typeof WORK_HISTORY_ERROR_CODES)[number];
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* How a claim ended. `released`: its holder, or someone closing out an
|
|
244
|
+
* expired lease, gave it back, with an outcome when one was given. `held`: it
|
|
245
|
+
* is the live lease now. `expired`: it ran out and nobody released it or
|
|
246
|
+
* claimed the item since. `lost`: it ran out unreleased and another claim
|
|
247
|
+
* took the item.
|
|
248
|
+
*/
|
|
249
|
+
export type ClaimEnd = "released" | "held" | "expired" | "lost";
|
|
250
|
+
|
|
251
|
+
/** One claim of a work item: one fencing token, from its claim to how it ended. */
|
|
252
|
+
export interface LeaseClaim {
|
|
253
|
+
token: string;
|
|
254
|
+
holder: string;
|
|
255
|
+
acquiredAt: string;
|
|
256
|
+
/** The expiry its last claim or renew set. */
|
|
257
|
+
expiresAt: string;
|
|
258
|
+
renewals: number;
|
|
259
|
+
ended: ClaimEnd;
|
|
260
|
+
/** The release, when it was released. */
|
|
261
|
+
release: { by: string; at: string; outcome: string | null; note: string | null } | null;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/** What `work history --json` prints. */
|
|
265
|
+
export type WorkHistoryDocument =
|
|
266
|
+
| {
|
|
267
|
+
$schema: string;
|
|
268
|
+
contract: number;
|
|
269
|
+
chant: string;
|
|
270
|
+
item: string;
|
|
271
|
+
/** The work kind file, from the repository root. */
|
|
272
|
+
kind: string;
|
|
273
|
+
/** The lease ref. */
|
|
274
|
+
ref: string;
|
|
275
|
+
/** The history file on the branch, under the member's prefix when the kind's member has one. */
|
|
276
|
+
ledger: { branch: string; path: string };
|
|
277
|
+
/** The lease ref's record now, live or expired, or null when there is none. Read from local refs, without fetching. */
|
|
278
|
+
lease: (Omit<WorkLease, "item"> & { state: "active" | "expired" }) | null;
|
|
279
|
+
/** Each claim, oldest first. */
|
|
280
|
+
claims: LeaseClaim[];
|
|
281
|
+
/** Every line of the history, oldest first. */
|
|
282
|
+
events: LeaseHistoryRecord[];
|
|
283
|
+
/** Lines of the history that are not lease events, left out. */
|
|
284
|
+
malformed: number;
|
|
285
|
+
summary: { claims: number; released: number; held: number; expired: number; lost: number; outcomes: Record<string, number> };
|
|
286
|
+
}
|
|
287
|
+
| { $schema: string; contract: number; chant: string; error: { code: WorkHistoryErrorCode; message: string } };
|
|
288
|
+
|
|
289
|
+
/** Fold a history into its claims: one per token, in the order they were first seen. */
|
|
290
|
+
export function claimsOf(events: LeaseHistoryRecord[], live: Pick<WorkLeaseState, "token" | "state"> | null): LeaseClaim[] {
|
|
291
|
+
const byToken = new Map<string, LeaseClaim>();
|
|
292
|
+
const order: string[] = [];
|
|
293
|
+
for (const e of events) {
|
|
294
|
+
let c = byToken.get(e.token);
|
|
295
|
+
if (!c) {
|
|
296
|
+
c = { token: e.token, holder: e.holder, acquiredAt: e.acquiredAt, expiresAt: e.expiresAt, renewals: 0, ended: "expired", release: null };
|
|
297
|
+
byToken.set(e.token, c);
|
|
298
|
+
order.push(e.token);
|
|
299
|
+
}
|
|
300
|
+
if (e.event === "renew") c.renewals++;
|
|
301
|
+
if (e.event !== "release") c.expiresAt = e.expiresAt;
|
|
302
|
+
if (e.event === "release") c.release = { by: e.by, at: e.timestamp, outcome: e.outcome ?? null, note: e.note ?? null };
|
|
303
|
+
}
|
|
304
|
+
return order.map((token, i) => {
|
|
305
|
+
const c = byToken.get(token)!;
|
|
306
|
+
if (c.release) c.ended = "released";
|
|
307
|
+
else if (live && live.token === token) c.ended = live.state === "active" ? "held" : "expired";
|
|
308
|
+
else if (i < order.length - 1) c.ended = "lost";
|
|
309
|
+
else c.ended = "expired";
|
|
310
|
+
return c;
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Read work item `id`'s lease history (#2785): every claim, renew and release
|
|
316
|
+
* on `_leases/<id>.jsonl` in the ledger of the member owning the work kind,
|
|
317
|
+
* folded into claims. Read from the local `chant/lifecycle` branch and lease
|
|
318
|
+
* refs; it never fetches, like the other read-contract reads.
|
|
319
|
+
*/
|
|
320
|
+
export async function workHistory(req: { id: string; cwd: string; kind?: string }): Promise<WorkHistoryDocument> {
|
|
321
|
+
const head = { $schema: WORK_HISTORY_OUTPUT_SCHEMA_ID, contract: WORK_HISTORY_CONTRACT_VERSION, chant: readerVersion() };
|
|
322
|
+
try {
|
|
323
|
+
if (!WORK_ITEM_ID_PATTERN.test(req.id) || req.id.includes("..")) throw new WorkError("work-item-unknown", `${JSON.stringify(req.id)} is not a work item id`);
|
|
324
|
+
if (!gitRoot(req.cwd)) throw new WorkError("not-a-git-repository", "a work lease's history is on the chant/lifecycle branch, and this directory is not in a git repository");
|
|
325
|
+
const { loaded, root } = await findWorkItem(req.id, req.cwd, req.kind);
|
|
326
|
+
const leaseCwd = dirname(loaded.file);
|
|
327
|
+
const { prefix } = await resolveMemberLedger(leaseCwd);
|
|
328
|
+
const { records: events, malformed } = await readLeaseHistory(req.id, { cwd: leaseCwd });
|
|
329
|
+
const live = (await listWorkLeases({ cwd: leaseCwd, memberPrefix: prefix })).find((l) => l.item === req.id) ?? null;
|
|
330
|
+
const claims = claimsOf(events, live);
|
|
331
|
+
const count = (end: ClaimEnd) => claims.filter((c) => c.ended === end).length;
|
|
332
|
+
const outcomes: Record<string, number> = {};
|
|
333
|
+
for (const c of claims) if (c.release?.outcome) outcomes[c.release.outcome] = (outcomes[c.release.outcome] ?? 0) + 1;
|
|
334
|
+
return {
|
|
335
|
+
...head,
|
|
336
|
+
item: req.id,
|
|
337
|
+
kind: relative(root, loaded.file).split("\\").join("/"),
|
|
338
|
+
ref: workLeaseRef(req.id, prefix),
|
|
339
|
+
ledger: { branch: "chant/lifecycle", path: leaseHistoryPath(req.id, prefix) },
|
|
340
|
+
lease: live ? { holder: live.holder, token: live.token, acquiredAt: live.acquiredAt, expiresAt: live.expiresAt, state: live.state } : null,
|
|
341
|
+
claims,
|
|
342
|
+
events,
|
|
343
|
+
malformed,
|
|
344
|
+
summary: { claims: claims.length, released: count("released"), held: count("held"), expired: count("expired"), lost: count("lost"), outcomes },
|
|
345
|
+
};
|
|
346
|
+
} catch (err) {
|
|
347
|
+
if (err instanceof WorkError) return { ...head, error: { code: err.code as WorkHistoryErrorCode, message: err.message } };
|
|
348
|
+
throw err;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/** The history as lines for a person. */
|
|
353
|
+
export function formatWorkHistory(doc: Extract<WorkHistoryDocument, { claims: unknown }>): string {
|
|
354
|
+
const lines = [`${doc.item}: ${doc.ledger.branch}:${doc.ledger.path}`];
|
|
355
|
+
for (const c of doc.claims) {
|
|
356
|
+
const how = c.release ? `released${c.release.outcome ? ` ${c.release.outcome}` : ""} by ${c.release.by} at ${c.release.at}` : c.ended === "held" ? `held until ${c.expiresAt}` : `${c.ended}, never released (expired ${c.expiresAt})`;
|
|
357
|
+
lines.push(` ${c.acquiredAt} ${c.holder} token ${c.token} ${c.renewals} renewal${c.renewals === 1 ? "" : "s"} ${how}`);
|
|
358
|
+
}
|
|
359
|
+
if (doc.claims.length === 0) lines.push(" never claimed");
|
|
360
|
+
if (doc.malformed > 0) lines.push(` ${doc.malformed} malformed line${doc.malformed === 1 ? "" : "s"} left out`);
|
|
361
|
+
return lines.join("\n");
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
async function runWorkHistory(ctx: CommandContext): Promise<number> {
|
|
365
|
+
const { args } = ctx;
|
|
366
|
+
const id = args.extraPositional2;
|
|
367
|
+
if (!id) {
|
|
368
|
+
console.error(formatError({ message: "history needs a work item id", hint: USAGE }));
|
|
369
|
+
return 1;
|
|
370
|
+
}
|
|
371
|
+
for (const [flag, v] of [["--holder", args.holder], ["--ttl", args.ttl], ["--token", args.token], ["--outcome", args.outcome], ["--note", args.note]] as const) {
|
|
372
|
+
if (v !== undefined) {
|
|
373
|
+
console.error(formatError({ message: `history is a read and takes no ${flag}`, hint: USAGE }));
|
|
374
|
+
return 1;
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
const doc = await workHistory({ id, cwd: process.cwd(), kind: args.kind });
|
|
378
|
+
if (args.json) console.log(JSON.stringify(doc, null, 2));
|
|
379
|
+
else if ("error" in doc) console.error(formatError({ message: `${doc.error.code}: ${doc.error.message}`, hint: USAGE }));
|
|
380
|
+
else console.log(formatWorkHistory(doc));
|
|
381
|
+
return "error" in doc ? 1 : 0;
|
|
382
|
+
}
|
|
383
|
+
|
|
202
384
|
export async function runWorkspaceWork(ctx: CommandContext): Promise<number> {
|
|
203
385
|
const { args } = ctx;
|
|
204
386
|
const verb = args.extraPositional;
|
|
387
|
+
if (verb === "history") return runWorkHistory(ctx);
|
|
205
388
|
const fail = (message: string): number => {
|
|
206
389
|
if (args.json) console.log(JSON.stringify({ $schema: WORK_LEASE_OUTPUT_SCHEMA_ID, contract: WORK_LEASE_CONTRACT_VERSION, error: { code: "write-usage-invalid", message } }, null, 2));
|
|
207
390
|
else console.error(formatError({ message, hint: USAGE }));
|
|
208
391
|
return 1;
|
|
209
392
|
};
|
|
210
393
|
if (verb !== "claim" && verb !== "renew" && verb !== "release") {
|
|
211
|
-
return fail(verb ? `chant workspace work takes claim, renew or
|
|
394
|
+
return fail(verb ? `chant workspace work takes claim, renew, release or history, not ${verb}` : "chant workspace work needs claim, renew, release or history");
|
|
212
395
|
}
|
|
213
396
|
const id = args.extraPositional2;
|
|
214
397
|
if (!id) return fail(`${verb} needs a work item id`);
|