@intentius/chant 0.89.0 → 0.91.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/handlers/components.d.ts +8 -0
- package/dist/cli/handlers/components.d.ts.map +1 -1
- package/dist/cli/handlers/operator.d.ts +14 -0
- package/dist/cli/handlers/operator.d.ts.map +1 -1
- package/dist/cli/handlers/run.d.ts.map +1 -1
- package/dist/cli/handlers/serve.d.ts.map +1 -1
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/mcp/server.d.ts +10 -5
- package/dist/cli/mcp/server.d.ts.map +1 -1
- package/dist/cli/mcp/types.d.ts +13 -5
- package/dist/cli/mcp/types.d.ts.map +1 -1
- package/dist/cli/mcp/workspace-tools.d.ts +55 -0
- package/dist/cli/mcp/workspace-tools.d.ts.map +1 -0
- package/dist/cli/registry.d.ts +31 -0
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/components/verbs/vuln-scan.d.ts +72 -0
- package/dist/components/verbs/vuln-scan.d.ts.map +1 -1
- package/dist/lifecycle/git.d.ts +40 -5
- package/dist/lifecycle/git.d.ts.map +1 -1
- package/dist/lifecycle/lease.d.ts +72 -16
- package/dist/lifecycle/lease.d.ts.map +1 -1
- package/dist/lifecycle/member-ledger.d.ts +3 -2
- package/dist/lifecycle/member-ledger.d.ts.map +1 -1
- package/dist/lifecycle/plan-ledger.d.ts +114 -0
- package/dist/lifecycle/plan-ledger.d.ts.map +1 -0
- package/dist/lifecycle/work-lease.d.ts +140 -0
- package/dist/lifecycle/work-lease.d.ts.map +1 -0
- package/dist/op/activities/activity-contracts.d.ts +2 -2
- package/dist/op/builders.d.ts.map +1 -1
- package/dist/op/discover.d.ts +25 -0
- package/dist/op/discover.d.ts.map +1 -1
- package/dist/op/index.d.ts +9 -3
- package/dist/op/index.d.ts.map +1 -1
- package/dist/op/lifecycle-receipt-store.d.ts +34 -0
- package/dist/op/lifecycle-receipt-store.d.ts.map +1 -0
- package/dist/op/local-executor.d.ts +19 -0
- package/dist/op/local-executor.d.ts.map +1 -1
- package/dist/op/local-output.d.ts.map +1 -1
- package/dist/op/op-ir.d.ts +10 -1
- package/dist/op/op-ir.d.ts.map +1 -1
- package/dist/op/op-verb-class.d.ts.map +1 -1
- package/dist/op/operator.d.ts +29 -0
- package/dist/op/operator.d.ts.map +1 -1
- package/dist/op/runtime.d.ts +9 -0
- package/dist/op/runtime.d.ts.map +1 -1
- package/dist/op/runtimes/local.d.ts.map +1 -1
- package/dist/op/step-output-ref.d.ts +2 -2
- package/dist/op/step-output-ref.d.ts.map +1 -1
- package/dist/op/steward.d.ts +140 -0
- package/dist/op/steward.d.ts.map +1 -0
- package/dist/op/types.d.ts +51 -0
- package/dist/op/types.d.ts.map +1 -1
- package/dist/op/work-lease-decl.d.ts +18 -0
- package/dist/op/work-lease-decl.d.ts.map +1 -0
- package/dist/op/work-lease-run.d.ts +173 -0
- package/dist/op/work-lease-run.d.ts.map +1 -0
- package/dist/workspace/box-isolation.d.ts +99 -0
- package/dist/workspace/box-isolation.d.ts.map +1 -0
- package/dist/workspace/checks/box-isolation.d.ts +18 -0
- package/dist/workspace/checks/box-isolation.d.ts.map +1 -0
- package/dist/workspace/checks/boxes.d.ts +71 -0
- package/dist/workspace/checks/boxes.d.ts.map +1 -0
- package/dist/workspace/checks/records.d.ts +1 -0
- package/dist/workspace/checks/records.d.ts.map +1 -1
- package/dist/workspace/checks.d.ts +10 -1
- package/dist/workspace/checks.d.ts.map +1 -1
- package/dist/workspace/conformance/index.d.ts +42 -2
- package/dist/workspace/conformance/index.d.ts.map +1 -1
- package/dist/workspace/conformance/vitest.d.ts.map +1 -1
- package/dist/workspace/decide.d.ts +184 -0
- package/dist/workspace/decide.d.ts.map +1 -0
- package/dist/workspace/decision-points.schema.json +137 -0
- package/dist/workspace/declaration.d.ts +51 -0
- package/dist/workspace/declaration.d.ts.map +1 -1
- package/dist/workspace/declaration.schema.json +172 -0
- package/dist/workspace/declared-kinds.d.ts +12 -0
- package/dist/workspace/declared-kinds.d.ts.map +1 -1
- package/dist/workspace/points-cli.d.ts +113 -0
- package/dist/workspace/points-cli.d.ts.map +1 -0
- package/dist/workspace/points.d.ts +320 -0
- package/dist/workspace/points.d.ts.map +1 -0
- package/dist/workspace/reason-codes.d.ts +29 -0
- package/dist/workspace/reason-codes.d.ts.map +1 -1
- package/dist/workspace/record-assets.d.ts.map +1 -1
- package/dist/workspace/records-cli.d.ts +12 -0
- package/dist/workspace/records-cli.d.ts.map +1 -1
- package/dist/workspace/records-write.d.ts +35 -3
- package/dist/workspace/records-write.d.ts.map +1 -1
- package/dist/workspace/records.d.ts +12 -3
- package/dist/workspace/records.d.ts.map +1 -1
- package/dist/workspace/source-block.d.ts +85 -0
- package/dist/workspace/source-block.d.ts.map +1 -0
- package/dist/workspace/status-stewards.d.ts +121 -0
- package/dist/workspace/status-stewards.d.ts.map +1 -0
- package/dist/workspace/status.d.ts +52 -1
- package/dist/workspace/status.d.ts.map +1 -1
- package/dist/workspace/work-cli.d.ts +78 -0
- package/dist/workspace/work-cli.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/cli/handlers/components.test.ts +93 -0
- package/src/cli/handlers/components.ts +44 -3
- package/src/cli/handlers/operator.ts +107 -2
- package/src/cli/handlers/run.test.ts +19 -0
- package/src/cli/handlers/run.ts +53 -1
- package/src/cli/handlers/serve.ts +2 -1
- package/src/cli/main.test.ts +40 -0
- package/src/cli/main.ts +78 -2
- package/src/cli/mcp/docs-parity.test.ts +20 -2
- package/src/cli/mcp/server.ts +23 -6
- package/src/cli/mcp/types.ts +15 -2
- package/src/cli/mcp/workspace-tools.test.ts +211 -0
- package/src/cli/mcp/workspace-tools.ts +449 -0
- package/src/cli/registry.ts +31 -0
- package/src/components/verbs/vuln-scan.test.ts +124 -1
- package/src/components/verbs/vuln-scan.ts +142 -1
- package/src/lifecycle/git.ts +65 -15
- package/src/lifecycle/lease.test.ts +22 -0
- package/src/lifecycle/lease.ts +133 -29
- package/src/lifecycle/member-ledger.ts +3 -2
- package/src/lifecycle/plan-ledger.test.ts +148 -0
- package/src/lifecycle/plan-ledger.ts +158 -0
- package/src/lifecycle/work-lease.test.ts +236 -0
- package/src/lifecycle/work-lease.ts +426 -0
- package/src/op/builders.ts +5 -0
- package/src/op/discover.ts +71 -0
- package/src/op/index.ts +16 -3
- package/src/op/lifecycle-receipt-store.test.ts +60 -0
- package/src/op/lifecycle-receipt-store.ts +61 -0
- package/src/op/local-executor.ts +216 -18
- package/src/op/local-output.ts +13 -0
- package/src/op/op-ir.ts +14 -0
- package/src/op/op-verb-class.ts +6 -0
- package/src/op/operator.ts +75 -4
- package/src/op/runtime.ts +6 -0
- package/src/op/runtimes/local.ts +3 -0
- package/src/op/step-output-ref.ts +6 -2
- package/src/op/steward.test.ts +212 -0
- package/src/op/steward.ts +253 -0
- package/src/op/types.ts +53 -0
- package/src/op/work-lease-decl.ts +80 -0
- package/src/op/work-lease-run.test.ts +326 -0
- package/src/op/work-lease-run.ts +395 -0
- package/src/workspace/box-isolation.test.ts +261 -0
- package/src/workspace/box-isolation.ts +205 -0
- package/src/workspace/check-contract.test.ts +3 -1
- package/src/workspace/check.schema.json +15 -7
- package/src/workspace/checks/box-isolation.ts +68 -0
- package/src/workspace/checks/boxes.test.ts +197 -0
- package/src/workspace/checks/boxes.ts +307 -0
- package/src/workspace/checks/records.ts +25 -0
- package/src/workspace/checks.test.ts +7 -0
- package/src/workspace/checks.ts +19 -2
- package/src/workspace/conformance/__fixture__/decisions/decision.kind.mjs +4 -0
- package/src/workspace/conformance/__fixture__/decisions/decision.schema.json +100 -6
- package/src/workspace/conformance/index.mjs +3 -0
- package/src/workspace/conformance/index.ts +185 -7
- package/src/workspace/conformance/vitest.ts +17 -8
- package/src/workspace/decide.test.ts +224 -0
- package/src/workspace/decide.ts +576 -0
- package/src/workspace/decision-points.schema.json +137 -0
- package/src/workspace/declaration.schema.json +172 -0
- package/src/workspace/declaration.ts +137 -0
- package/src/workspace/declared-kinds.ts +25 -2
- package/src/workspace/intent.schema.json +4 -1
- package/src/workspace/point-answer.schema.json +95 -0
- package/src/workspace/points-cli.ts +273 -0
- package/src/workspace/points-write.schema.json +489 -0
- package/src/workspace/points.schema.json +710 -0
- package/src/workspace/points.test.ts +264 -0
- package/src/workspace/points.ts +564 -0
- package/src/workspace/read-contract.test.ts +18 -1
- package/src/workspace/reason-codes.test.ts +14 -1
- package/src/workspace/reason-codes.ts +36 -0
- package/src/workspace/record-assets.test.ts +3 -2
- package/src/workspace/record-assets.ts +4 -1
- package/src/workspace/records-amend.schema.json +2 -1
- package/src/workspace/records-cli.ts +15 -2
- package/src/workspace/records-close.schema.json +2 -1
- package/src/workspace/records-contract.test.ts +3 -2
- package/src/workspace/records-new.schema.json +4 -1
- package/src/workspace/records-review.schema.json +2 -1
- package/src/workspace/records-write.ts +85 -7
- package/src/workspace/records.schema.json +18 -0
- package/src/workspace/records.ts +61 -3
- package/src/workspace/source-block.test.ts +167 -0
- package/src/workspace/source-block.ts +129 -0
- package/src/workspace/status-contract.test.ts +178 -0
- package/src/workspace/status-stewards.ts +225 -0
- package/src/workspace/status.schema.json +230 -4
- package/src/workspace/status.ts +106 -5
- package/src/workspace/work-cli.test.ts +180 -0
- package/src/workspace/work-cli.ts +246 -0
- package/src/workspace/work-lease.schema.json +233 -0
- package/src/workspace/work-readiness-chud.test.ts +145 -0
|
@@ -57,6 +57,10 @@ export const REASONS = {
|
|
|
57
57
|
"gates-no-ledger": "The checkout has no chant/lifecycle branch, so there is no gate ledger to read.",
|
|
58
58
|
"gates-no-gate-ledger": "The branch has no gate ledger for the member: no run of it has reached a gate.",
|
|
59
59
|
"gates-ledger-unreadable": "Reading the member's gate ledger failed, so no gate is listed.",
|
|
60
|
+
// A member's stewards status can't fully list (status, #2731).
|
|
61
|
+
"stewards-unreadable": "An *.op.ts file could not be imported, so a steward it declares may be missing.",
|
|
62
|
+
"stewards-conflict": "A steward was dropped: its name, or an Op it lists, belongs to another steward.",
|
|
63
|
+
"steward-runs-unreadable": "Reading an Op's run ledger failed, so its last run is null.",
|
|
60
64
|
// A record that isn't valid (records).
|
|
61
65
|
"record-unparseable": "No front matter, a YAML error or a value outside the JSON subset of YAML, or for a JSON kind a file that is not one object or repeats a member name.",
|
|
62
66
|
"record-schema-invalid": "The record's front matter, or its JSON object, does not match the kind's schema.",
|
|
@@ -73,6 +77,7 @@ export const REASONS = {
|
|
|
73
77
|
"record-supersedes-pending": "A supersedes link from a record whose state is weaker than the record it names, so the link has no effect yet.",
|
|
74
78
|
"record-no-evidence": "The record's evidence list is empty: it cites nothing and pins no file. Information for a reviewer, never an error.",
|
|
75
79
|
"review-undigested": "A verdict names no digest of the text it judged. It still counts, and an amendment does not stop it counting.",
|
|
80
|
+
"source-transcript-drift": "The record's source block pins a transcript by hash, the file it names can be read here, and its bytes hash to something else: it is not the transcript the record means.",
|
|
76
81
|
// A work record that is valid but warned about (records and graph --intent, #2683).
|
|
77
82
|
"work-needs-unknown": "A work record's needs list names a work id no record has, so the item stays blocked.",
|
|
78
83
|
"work-implements-unknown": "A work record's implements list names a decision id no decision has.",
|
|
@@ -81,6 +86,10 @@ export const REASONS = {
|
|
|
81
86
|
"work-done-unpinned": "A work record is done and its evidence list is empty: nothing shows the work was done.",
|
|
82
87
|
"work-closed-without-date": "A work record is done or dropped and has no closing date.",
|
|
83
88
|
"work-done-gap-open": "A work record is done, and the finding it came from still fires on its region. graph --intent raises it, and records does by walking that region.",
|
|
89
|
+
// An answer to a decision point that is valid but warned about (records, points, ws-058, #2739).
|
|
90
|
+
"answer-points-unreadable": "The points file the answer kind names can't be read, or is not valid.",
|
|
91
|
+
"answer-point-unknown": "The answer's point is not declared in the points file the answer kind names.",
|
|
92
|
+
"answer-point-changed": "The point's declaration changed since the question was asked, so the answer is to an older version of the question.",
|
|
84
93
|
// A verdict the quorum does not count (records, #2671, #2672).
|
|
85
94
|
"review-decider": "The verdict is the decider's own, and the quorum counts verdicts besides the decider's.",
|
|
86
95
|
"review-agent": "The reviewer holds the agent role in the trust policy at base.",
|
|
@@ -114,10 +123,20 @@ export const REASONS = {
|
|
|
114
123
|
"review-unsupported": "The kind's schema has no reviews field, so its records take no review.",
|
|
115
124
|
"review-note-required": "A dissent was given with no note: a dissent needs a reason.",
|
|
116
125
|
"review-sign-failed": "--sign was given and no seal could be made: the key can't be read or used, git names no ssh signing key, or ssh-keygen is not installed.",
|
|
126
|
+
"record-state-not-initial": "A record written through chant serve mcp gives a state other than the kind's first: a new record opens proposed, and a person moves it on.",
|
|
127
|
+
"source-harvest-not-proposed": "A harvested record (source.via harvest) was written in a state other than the kind's first: a harvest proposes, and a person decides.",
|
|
117
128
|
"record-sign-failed": "--sign was given and no author seal could be made: the record names no author, the key can't be read or used, git names no ssh signing key, or ssh-keygen is not installed.",
|
|
118
129
|
// A review given in a session (records review --session, #2693).
|
|
119
130
|
"session-unknown": "--session names no session of a session kind whose subjects are the record's kind.",
|
|
120
131
|
"session-not-open": "--session names a session in a closed state, which takes no more verdicts.",
|
|
132
|
+
// Decision points (points, points ask, points answer, ws-058, #2739).
|
|
133
|
+
"points-undeclared": "No record kind with an answers block is declared, or given with --kind, so there is no points file to ask.",
|
|
134
|
+
"points-invalid": "The points file an answer kind names can't be read, or does not match decision-points.schema.json and the rules checked in code.",
|
|
135
|
+
"point-unknown": "No points file declares the point asked, or the one an answer names.",
|
|
136
|
+
"point-inputs-invalid": "The inputs given to an ask are not a JSON object of the point's declared inputs.",
|
|
137
|
+
"point-decider-failed": "A model decider that fails closed (unreachable: fail) could not answer, so the ask wrote nothing.",
|
|
138
|
+
"answer-not-candidate": "The people's answer is not one of the question's candidates.",
|
|
139
|
+
"quorum-not-met": "Too few of the people who answered count toward the point's quorum: distinct, not holding the agent role, and holding one of its roles when it names any.",
|
|
121
140
|
// records --since that fails (#2673).
|
|
122
141
|
"since-rev-unknown": "--since names no commit, or a session with no opening revision and no commit that added it.",
|
|
123
142
|
"since-session-unknown": "--since has the shape of a session id and names no commit, and no session the kind or the declaration reads has that id.",
|
|
@@ -156,6 +175,23 @@ export const REASONS = {
|
|
|
156
175
|
"environments-none-declared": "The member's chant.config.ts declares no environments, so only local and the environments in its ledger are listed.",
|
|
157
176
|
"environments-ledger-undeclared": "The member's ledger has releases in an environment its config's environments don't cover, so chant run --env would refuse it and it is not listed.",
|
|
158
177
|
"environments-ledger-unreadable": "The chant/lifecycle branch exists and the member's ledger environments could not be listed.",
|
|
178
|
+
// A box that holds a credential (check, #2726). Each is also a WSP finding.
|
|
179
|
+
"box-credential-declared": "A file in a box member's directory carries a literal secret: a credential's shape, or a literal where a credential goes. A variable or secret-manager reference is not one.",
|
|
180
|
+
"box-capability-unbrokered": "A capability in a member's box block names no broker, so the box would hold its credential.",
|
|
181
|
+
// The work lease (chant workspace work claim|renew|release, #2732): why the command could not run.
|
|
182
|
+
"work-kind-missing": "No work kind to find the item in: --kind names a kind with no work block, or the declaration names no work kind.",
|
|
183
|
+
"work-kind-ambiguous": "More than one declared work kind has a record with the id, so --kind must name one.",
|
|
184
|
+
"work-item-unknown": "No work record has the id, so there is nothing to lease.",
|
|
185
|
+
"work-item-closed": "A claim on a work item in a closed state, such as done or dropped: there is no work left to claim.",
|
|
186
|
+
// The work lease: why a claim, renew or release was refused (exit 2).
|
|
187
|
+
"lease-held": "Someone holds a live lease on the work item: another worker, or, for a claim, the same one.",
|
|
188
|
+
"lease-not-held": "Nobody holds a live lease on the work item: it expired, was released or was never claimed.",
|
|
189
|
+
"lease-token-mismatch": "The live lease on the work item carries another fencing token than the one given.",
|
|
190
|
+
"lease-race": "Another writer changed the lease between this command's read and its write.",
|
|
191
|
+
"lease-push-rejected": "The remote refused the lease push: another clone claimed the item first, or the remote could not be reached.",
|
|
192
|
+
// A box whose isolation fails (check, #2727). Each is also a WSP finding.
|
|
193
|
+
"box-isolation-collision": "Two boxes on one host resolve to the same port, state path or cookie name, or two ports in one box share an offset.",
|
|
194
|
+
"box-isolation-literal": "A host's stateRoot or a box's state entry is a literal machine path instead of one derived from an environment reference and the box's name.",
|
|
159
195
|
// The lineage lock (check).
|
|
160
196
|
"lock-invalid": "The lineage lock can't be read.",
|
|
161
197
|
"manual-step-open": "A scope in the lineage lock has an open manual step.",
|
|
@@ -18,6 +18,7 @@ import { constraintCovers, isWorkspacePath, memberHolding, WORKSPACE_PATH_PATTER
|
|
|
18
18
|
import { pinFile, queryRecords, type RecordsDocument } from "./records-cli";
|
|
19
19
|
import { parseFrontMatter, RECORD_WARNING_CODES, SEAL_WARNING_CODES } from "./records";
|
|
20
20
|
import { WORK_WARNING_CODES } from "./work";
|
|
21
|
+
import { ANSWER_WARNING_CODES } from "./points";
|
|
21
22
|
import { workspaceGraph } from "./graph-cli";
|
|
22
23
|
import graphSchema from "./graph.schema.json";
|
|
23
24
|
import recordsSchema from "./records.schema.json";
|
|
@@ -170,8 +171,8 @@ describe("chant workspace records checks each pin", () => {
|
|
|
170
171
|
|
|
171
172
|
test("the schema lists exactly the warning codes", () => {
|
|
172
173
|
// A work kind's records carry the work warnings too (#2683), work-done-gap-open included since records walks a done item's region (#2686),
|
|
173
|
-
// and records adds record-unattested for an author seal under a signers file at base (#2688).
|
|
174
|
-
expect(recordsSchema.$defs.warning.properties.code.enum).toEqual([...RECORD_WARNING_CODES, ...WORK_WARNING_CODES, ...SEAL_WARNING_CODES]);
|
|
174
|
+
// an answer kind's the answer warnings (ws-058), and records adds record-unattested for an author seal under a signers file at base (#2688).
|
|
175
|
+
expect(recordsSchema.$defs.warning.properties.code.enum).toEqual([...RECORD_WARNING_CODES, ...WORK_WARNING_CODES, ...ANSWER_WARNING_CODES, ...SEAL_WARNING_CODES]);
|
|
175
176
|
});
|
|
176
177
|
|
|
177
178
|
test("records pin <path> prints the entry's path from the workspace root and the file's hash", () => {
|
|
@@ -81,7 +81,10 @@ export function checkPins(pins: { path: string; sha256: string }[], tree: Worksp
|
|
|
81
81
|
const state: PinState = actual === null ? "missing" : actual === pin.sha256 ? "pinned" : "drifted";
|
|
82
82
|
assets.push({ ...pin, actual, state });
|
|
83
83
|
if (state === "missing") {
|
|
84
|
-
|
|
84
|
+
// Worded the same for the working tree and a revision (#2745): a missing
|
|
85
|
+
// file is missing either way, and `tree.label` would otherwise make the
|
|
86
|
+
// same finding read differently depending on which one was read.
|
|
87
|
+
warnings.push({ code: "asset-missing", message: `evidence pins ${pin.path}, which does not exist` });
|
|
85
88
|
} else if (state === "drifted") {
|
|
86
89
|
warnings.push({
|
|
87
90
|
code: "asset-drift",
|
|
@@ -92,6 +92,13 @@ export type RecordView = RecordEntry & {
|
|
|
92
92
|
/** true when the record's seal verifies for its author against the signers at base; false when it fails, or is missing under an active policy; null when nothing here can say (#2688). */
|
|
93
93
|
attested?: boolean | null;
|
|
94
94
|
attestation?: VerdictAttestation;
|
|
95
|
+
/**
|
|
96
|
+
* For a work kind read in the working tree (#2732): the item's active work
|
|
97
|
+
* lease, or null when nobody holds one. Read from the local lease refs of
|
|
98
|
+
* the ledger owning the kind, without fetching. Absent under `--at`, since a
|
|
99
|
+
* lease is live state and not part of a revision.
|
|
100
|
+
*/
|
|
101
|
+
lease?: { holder: string; token: string; acquiredAt: string; expiresAt: string } | null;
|
|
95
102
|
};
|
|
96
103
|
|
|
97
104
|
/** The role in the trust policy whose holders' verdicts the quorum does not count (#2671). */
|
|
@@ -304,6 +311,11 @@ export async function queryRecords(query: RecordsQuery): Promise<RecordsDocument
|
|
|
304
311
|
...(seals ? authorSeal(loaded.kind, r, policy, seals.checkRecordSeal) : {}),
|
|
305
312
|
}));
|
|
306
313
|
if (loaded.kind.work && query.workGaps !== false && top) await raiseWorkGaps(loaded, records, { root, workspaceRoot, at: query.at });
|
|
314
|
+
if (loaded.kind.work && at === null && top) {
|
|
315
|
+
const { activeWorkLeases } = await import("../lifecycle/work-lease");
|
|
316
|
+
const leases = await activeWorkLeases(loaded.file);
|
|
317
|
+
for (const r of records) if (r.id !== null) r.lease = leases.get(r.id) ?? null;
|
|
318
|
+
}
|
|
307
319
|
return {
|
|
308
320
|
$schema: RECORDS_OUTPUT_SCHEMA_ID,
|
|
309
321
|
contract: RECORDS_CONTRACT_VERSION,
|
|
@@ -596,7 +608,8 @@ function formatRecords(records: RecordView[], summary: { total: number; valid: n
|
|
|
596
608
|
if (r.ready !== undefined) {
|
|
597
609
|
const blocked = (r.blockedBy ?? []).map((b) => `${b.id} (${b.state ?? "unknown"})`).join(", ");
|
|
598
610
|
const implemented = (r.implements ?? []).map((d) => `${d.id} (${d.state ?? "unknown"})`).join(", ");
|
|
599
|
-
const
|
|
611
|
+
const held = r.lease ? `held by ${r.lease.holder} until ${r.lease.expiresAt}` : "";
|
|
612
|
+
const status = [r.ready ? "ready" : blocked ? `blocked by ${blocked}` : "", implemented ? `implements ${implemented}` : "", held].filter(Boolean).join("; ");
|
|
600
613
|
if (status) lines.push(`${" ".repeat(idWidth + 2)}${status}`);
|
|
601
614
|
}
|
|
602
615
|
for (const reason of r.reasons) lines.push(`${" ".repeat(idWidth + 2)}${reason.code}: ${reason.message} (${r.path})`);
|
|
@@ -620,7 +633,7 @@ export async function runWorkspaceUnknown(ctx: CommandContext): Promise<number>
|
|
|
620
633
|
console.error(
|
|
621
634
|
formatError({
|
|
622
635
|
message: sub ? `Unknown workspace subcommand: ${sub}` : "chant workspace needs a subcommand",
|
|
623
|
-
hint: `Workspace subcommands: audit, build, check, graph, init, lineage, lint, ls, records, status, upgrade, verify. Run "chant --help" for their options.`,
|
|
636
|
+
hint: `Workspace subcommands: audit, build, check, graph, init, lineage, lint, ls, records, status, upgrade, verify, work. Run "chant --help" for their options.`,
|
|
624
637
|
}),
|
|
625
638
|
);
|
|
626
639
|
return 1;
|
|
@@ -13,6 +13,7 @@ import { afterAll, describe, expect, test } from "vitest";
|
|
|
13
13
|
import { queryRecords, RECORDS_CONTRACT_VERSION, RECORDS_OUTPUT_SCHEMA_ID, type RecordsDocument } from "./records-cli";
|
|
14
14
|
import { READ_ERROR_CODES, RECORD_REASON_CODES, RECORD_WARNING_CODES, recordTextDigest, REVIEW_REASON_CODES, SEAL_WARNING_CODES } from "./records";
|
|
15
15
|
import { WORK_WARNING_CODES } from "./work";
|
|
16
|
+
import { ANSWER_WARNING_CODES } from "./points";
|
|
16
17
|
import schema from "./records.schema.json";
|
|
17
18
|
import { PROVENANCE_LEVELS } from "./trust/attestor";
|
|
18
19
|
|
|
@@ -149,8 +150,8 @@ describe("records output schema", () => {
|
|
|
149
150
|
|
|
150
151
|
test("lists exactly the warning codes the code can return", () => {
|
|
151
152
|
// A work kind's records carry the work warnings too (#2683), work-done-gap-open included since records walks a done item's region (#2686),
|
|
152
|
-
// and records adds record-unattested for an author seal under a signers file at base (#2688).
|
|
153
|
-
expect(schema.$defs.warning.properties.code.enum).toEqual([...RECORD_WARNING_CODES, ...WORK_WARNING_CODES, ...SEAL_WARNING_CODES]);
|
|
153
|
+
// an answer kind's the answer warnings (ws-058), and records adds record-unattested for an author seal under a signers file at base (#2688).
|
|
154
|
+
expect(schema.$defs.warning.properties.code.enum).toEqual([...RECORD_WARNING_CODES, ...WORK_WARNING_CODES, ...ANSWER_WARNING_CODES, ...SEAL_WARNING_CODES]);
|
|
154
155
|
});
|
|
155
156
|
|
|
156
157
|
test("every failure validates with its code", async () => {
|
|
@@ -117,7 +117,8 @@
|
|
|
117
117
|
"asset-stale",
|
|
118
118
|
"record-supersedes-pending",
|
|
119
119
|
"record-no-evidence",
|
|
120
|
-
"review-undigested"
|
|
120
|
+
"review-undigested",
|
|
121
|
+
"source-transcript-drift"
|
|
121
122
|
]
|
|
122
123
|
},
|
|
123
124
|
"message": {
|
|
@@ -161,6 +162,8 @@
|
|
|
161
162
|
"record-id-unallocatable",
|
|
162
163
|
"record-path-unmatched",
|
|
163
164
|
"record-sign-failed",
|
|
165
|
+
"source-harvest-not-proposed",
|
|
166
|
+
"record-state-not-initial",
|
|
164
167
|
"record-unparseable",
|
|
165
168
|
"record-schema-invalid",
|
|
166
169
|
"record-id-duplicate",
|
|
@@ -67,6 +67,8 @@ export const NEW_ERROR_CODES = [
|
|
|
67
67
|
"record-id-unallocatable",
|
|
68
68
|
"record-path-unmatched",
|
|
69
69
|
"record-sign-failed",
|
|
70
|
+
"source-harvest-not-proposed",
|
|
71
|
+
"record-state-not-initial",
|
|
70
72
|
...RECORD_REASON_CODES,
|
|
71
73
|
] as const satisfies readonly ReasonCode[];
|
|
72
74
|
|
|
@@ -354,7 +356,7 @@ export async function readAll(o: Opened, source: RecordSource): Promise<RecordEn
|
|
|
354
356
|
const subjectKind = await loadRecordKind(resolve(dirname(o.loaded.file), o.loaded.kind.session.subjects.kind), o.root);
|
|
355
357
|
subjects = { records: (await readRecords(subjectKind, { root: o.root, source })).records, reviews: subjectKind.kind.reviews?.field ?? "reviews" };
|
|
356
358
|
}
|
|
357
|
-
return (await readRecords(o.loaded, { root: o.root, source, assets, ...(subjects ? { subjects } : {}) })).records;
|
|
359
|
+
return (await readRecords(o.loaded, { root: o.root, source, assets, workspaceRoot: o.workspaceRoot, ...(subjects ? { subjects } : {}) })).records;
|
|
358
360
|
}
|
|
359
361
|
|
|
360
362
|
/** `base` with the file at `path` holding `text`, added to its directory when new. */
|
|
@@ -441,6 +443,24 @@ function refuseRevisionFields(kind: LoadedRecordKind["kind"], fields: Record<str
|
|
|
441
443
|
}
|
|
442
444
|
}
|
|
443
445
|
|
|
446
|
+
/**
|
|
447
|
+
* Refuse a harvested record in any state but the kind's first (#2708): a
|
|
448
|
+
* record whose source block says `via: "harvest"` came out of a transcript
|
|
449
|
+
* after the fact, so it is a proposal, and a person decides it.
|
|
450
|
+
*/
|
|
451
|
+
function refuseHarvestedDecision(kind: LoadedRecordKind["kind"], fields: Record<string, unknown>): void {
|
|
452
|
+
if (!kind.source || !kind.states || kind.stateField === undefined) return;
|
|
453
|
+
const block = fields[kind.source.field];
|
|
454
|
+
if (block === null || typeof block !== "object" || Array.isArray(block) || (block as Record<string, unknown>).via !== "harvest") return;
|
|
455
|
+
const opens = kind.states[0];
|
|
456
|
+
const state = fields[kind.stateField];
|
|
457
|
+
if (state === opens) return;
|
|
458
|
+
throw new RecordWriteError(
|
|
459
|
+
"source-harvest-not-proposed",
|
|
460
|
+
`${kind.source.field}.via is harvest, and a harvested ${kind.name} opens ${opens}, not ${JSON.stringify(state ?? null)}: write it with ${kind.stateField} "${opens}", and let the person who decides it move it on with chant workspace records amend or review`,
|
|
461
|
+
);
|
|
462
|
+
}
|
|
463
|
+
|
|
444
464
|
/**
|
|
445
465
|
* `text`, the record `data` holds, with its author sealed (#2688): an ssh
|
|
446
466
|
* signature by the key `sign` names over the record id, the digest of
|
|
@@ -594,14 +614,71 @@ export interface NewRecordOptions {
|
|
|
594
614
|
sign?: string | true;
|
|
595
615
|
}
|
|
596
616
|
|
|
617
|
+
/**
|
|
618
|
+
* A write made through a channel other than the command line (#2707), such
|
|
619
|
+
* as `chant serve mcp`. The CLI never sets one.
|
|
620
|
+
*/
|
|
621
|
+
export interface WriteChannel {
|
|
622
|
+
/**
|
|
623
|
+
* Laid over the kind's source block (#2708) when the kind declares one:
|
|
624
|
+
* `via` and `client`. A new record gets the block, made when the fields
|
|
625
|
+
* give none; an amendment only when its fields set the block.
|
|
626
|
+
*/
|
|
627
|
+
source: Record<string, unknown>;
|
|
628
|
+
/** A new record opens in the kind's first state: filled in when the fields give no state, and refused with record-state-not-initial when they give another. */
|
|
629
|
+
opensInitial?: boolean;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
/** What a channel's write adds to the command line's: who the author is, and how it came. */
|
|
633
|
+
export interface ChannelOptions {
|
|
634
|
+
/** The record's author, written to the kind's `reviews.decider` field: the person or agent that decided. */
|
|
635
|
+
by?: string;
|
|
636
|
+
through?: WriteChannel;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
/** `fields` with the channel's author and source laid over them (#2707). `isNew` for `records new`. */
|
|
640
|
+
function applyChannel(o: Opened, fields: Record<string, unknown>, opts: ChannelOptions, isNew: boolean, flag: string): Record<string, unknown> {
|
|
641
|
+
const { kind } = o.loaded;
|
|
642
|
+
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
|
+
const through = opts.through;
|
|
653
|
+
if (!through) return out;
|
|
654
|
+
if (isNew && through.opensInitial && kind.states && kind.stateField !== undefined) {
|
|
655
|
+
const first = kind.states[0];
|
|
656
|
+
const state = out[kind.stateField];
|
|
657
|
+
if (state === undefined) out[kind.stateField] = first;
|
|
658
|
+
else if (state !== first) {
|
|
659
|
+
throw new RecordWriteError(
|
|
660
|
+
"record-state-not-initial",
|
|
661
|
+
`a new ${kind.name} opens ${first}, and the fields give ${kind.stateField} ${JSON.stringify(state)}: write it ${first}, and let the person who decides it move it on with a review or an amendment`,
|
|
662
|
+
);
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
if (kind.source && (isNew || kind.source.field in out)) {
|
|
666
|
+
const f = kind.source.field;
|
|
667
|
+
const block = out[f];
|
|
668
|
+
if (block === undefined || block === null) out[f] = { ...through.source };
|
|
669
|
+
else if (typeof block === "object" && !Array.isArray(block)) out[f] = { ...(block as Record<string, unknown>), ...through.source };
|
|
670
|
+
}
|
|
671
|
+
return out;
|
|
672
|
+
}
|
|
673
|
+
|
|
597
674
|
/** `records new`: write one new record from validated fields, sealed by its author with `sign`. */
|
|
598
|
-
export async function newRecord(opts: NewRecordOptions): Promise<NewDocument> {
|
|
675
|
+
export async function newRecord(opts: NewRecordOptions & ChannelOptions): Promise<NewDocument> {
|
|
599
676
|
try {
|
|
600
677
|
if (opts.prefix !== undefined && !/^[A-Za-z][A-Za-z0-9]*$/.test(opts.prefix)) {
|
|
601
678
|
throw new RecordWriteError("write-usage-invalid", `--prefix takes letters and digits, starting with a letter, not ${JSON.stringify(opts.prefix)}`);
|
|
602
679
|
}
|
|
603
|
-
const fields = parseFields(opts.fields, "--from");
|
|
604
680
|
const o = await open(opts.kind, opts.cwd);
|
|
681
|
+
const fields = applyChannel(o, parseFields(opts.fields, "--from"), opts, true, "--from");
|
|
605
682
|
const { kind, schema } = o.loaded;
|
|
606
683
|
refuseSealField(o, fields, "--from");
|
|
607
684
|
refuseRevisionFields(kind, fields, {}, "--from");
|
|
@@ -617,6 +694,7 @@ export async function newRecord(opts: NewRecordOptions): Promise<NewDocument> {
|
|
|
617
694
|
if (taken) throw new RecordWriteError("record-id-taken", `id ${given} is already used by ${taken.path}; ids are never reused, so leave ${idField} out to have the next one allocated`);
|
|
618
695
|
id = given;
|
|
619
696
|
}
|
|
697
|
+
refuseHarvestedDecision(kind, fields);
|
|
620
698
|
// A session records the commit it opened at (#2693): HEAD now, or null before the first commit.
|
|
621
699
|
const opened = kind.session?.openedRev ? { [kind.session.openedRev]: headCommit(o.root) } : {};
|
|
622
700
|
const full = { ...fields, ...opened, [idField]: id };
|
|
@@ -676,10 +754,10 @@ export interface AmendRecordOptions {
|
|
|
676
754
|
* changes only the reviews leaves the digest, and the seal, as they were.
|
|
677
755
|
* `--sign` with nothing to change seals the record as it is.
|
|
678
756
|
*/
|
|
679
|
-
export async function amendRecord(opts: AmendRecordOptions): Promise<AmendDocument> {
|
|
757
|
+
export async function amendRecord(opts: AmendRecordOptions & ChannelOptions): Promise<AmendDocument> {
|
|
680
758
|
try {
|
|
681
|
-
const given = parseFields(opts.fields, "--set");
|
|
682
759
|
const o = await open(opts.kind, opts.cwd);
|
|
760
|
+
const given = applyChannel(o, parseFields(opts.fields, "--set"), opts, false, "--set");
|
|
683
761
|
const { kind } = o.loaded;
|
|
684
762
|
refuseSealField(o, given, "--set");
|
|
685
763
|
const before = await readAll(o, o.source);
|
|
@@ -922,7 +1000,7 @@ function usage(schema: string, message: string): WriteFailure<"write-usage-inval
|
|
|
922
1000
|
* message the verb has always given; several are refused, since a write
|
|
923
1001
|
* never guesses which kind it means.
|
|
924
1002
|
*/
|
|
925
|
-
function declaredWriteKind(schema: string, cwd: string, missing: string): string | WriteFailure<"write-usage-invalid"> {
|
|
1003
|
+
export function declaredWriteKind(schema: string, cwd: string, missing: string): string | WriteFailure<"write-usage-invalid"> {
|
|
926
1004
|
let kinds: ReturnType<typeof declaredKindFiles>;
|
|
927
1005
|
try {
|
|
928
1006
|
kinds = declaredKindFiles(cwd);
|
|
@@ -978,7 +1056,7 @@ function readInput(schema: string, flag: string, value: string | undefined, cwd:
|
|
|
978
1056
|
* The session kind `records close` goes through when none is named (#2693):
|
|
979
1057
|
* the one session kind the declaration names.
|
|
980
1058
|
*/
|
|
981
|
-
async function declaredSessionKind(schema: string, cwd: string): Promise<string | WriteFailure<"write-usage-invalid">> {
|
|
1059
|
+
export async function declaredSessionKind(schema: string, cwd: string): Promise<string | WriteFailure<"write-usage-invalid">> {
|
|
982
1060
|
const kinds = (await findSessionKinds(cwd)).map((k) => k.file);
|
|
983
1061
|
if (kinds.length === 1) return kinds[0];
|
|
984
1062
|
if (kinds.length === 0) return usage(schema, "--kind <session kind file> is required: the declaration names no session kind");
|
|
@@ -177,6 +177,10 @@
|
|
|
177
177
|
"description": "Added in contract 1 by #2683, on every parsed record of a work kind. Each decision the record implements, with its state now; null for an id no decision has.",
|
|
178
178
|
"type": "array",
|
|
179
179
|
"items": { "$ref": "#/$defs/workLink" }
|
|
180
|
+
},
|
|
181
|
+
"lease": {
|
|
182
|
+
"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
|
+
"oneOf": [{ "type": "null" }, { "$ref": "#/$defs/workLease" }]
|
|
180
184
|
}
|
|
181
185
|
},
|
|
182
186
|
"allOf": [
|
|
@@ -187,6 +191,16 @@
|
|
|
187
191
|
}
|
|
188
192
|
]
|
|
189
193
|
},
|
|
194
|
+
"workLease": {
|
|
195
|
+
"type": "object",
|
|
196
|
+
"required": ["holder", "token", "acquiredAt", "expiresAt"],
|
|
197
|
+
"properties": {
|
|
198
|
+
"holder": { "type": "string", "description": "Who holds the lease, as --holder gave it." },
|
|
199
|
+
"token": { "type": "string", "description": "The fencing token: new on every claim, kept by every renew." },
|
|
200
|
+
"acquiredAt": { "type": "string", "description": "When the lease was claimed, ISO 8601." },
|
|
201
|
+
"expiresAt": { "type": "string", "description": "When the lease expires unless renewed, ISO 8601." }
|
|
202
|
+
}
|
|
203
|
+
},
|
|
190
204
|
"asset": {
|
|
191
205
|
"type": "object",
|
|
192
206
|
"required": ["path", "sha256", "actual", "state"],
|
|
@@ -209,6 +223,7 @@
|
|
|
209
223
|
"record-supersedes-pending",
|
|
210
224
|
"record-no-evidence",
|
|
211
225
|
"review-undigested",
|
|
226
|
+
"source-transcript-drift",
|
|
212
227
|
"work-needs-unknown",
|
|
213
228
|
"work-implements-unknown",
|
|
214
229
|
"work-needs-cycle",
|
|
@@ -216,6 +231,9 @@
|
|
|
216
231
|
"work-done-unpinned",
|
|
217
232
|
"work-closed-without-date",
|
|
218
233
|
"work-done-gap-open",
|
|
234
|
+
"answer-points-unreadable",
|
|
235
|
+
"answer-point-unknown",
|
|
236
|
+
"answer-point-changed",
|
|
219
237
|
"record-unattested"
|
|
220
238
|
]
|
|
221
239
|
},
|
package/src/workspace/records.ts
CHANGED
|
@@ -29,8 +29,10 @@ import type { ReasonCode } from "./reason-codes";
|
|
|
29
29
|
import { checkPins, pinEntries, type AssetPin } from "./record-assets";
|
|
30
30
|
import { joinSessions, type SessionCitation } from "./record-sessions";
|
|
31
31
|
import type { RecordSource } from "./record-source";
|
|
32
|
+
import { sourceBlock, sourceBlockProblems, transcriptDrift } from "./source-block";
|
|
32
33
|
import type { WorkspaceTree } from "./tree";
|
|
33
34
|
import type { DecisionWork, WorkLink, WorkWarningCode } from "./work";
|
|
35
|
+
import type { AnswerWarningCode } from "./points";
|
|
34
36
|
|
|
35
37
|
// ── Reason codes ─────────────────────────────────────────────────────────────
|
|
36
38
|
|
|
@@ -84,6 +86,12 @@ export const RECORD_WARNING_CODES = [
|
|
|
84
86
|
* amendment does not stop it counting (#2672).
|
|
85
87
|
*/
|
|
86
88
|
"review-undigested",
|
|
89
|
+
/**
|
|
90
|
+
* The kind's source block pins a transcript by hash, the file it names can
|
|
91
|
+
* be read here, and its bytes hash to something else: it is not the
|
|
92
|
+
* transcript the record means (#2708).
|
|
93
|
+
*/
|
|
94
|
+
"source-transcript-drift",
|
|
87
95
|
] as const satisfies readonly ReasonCode[];
|
|
88
96
|
export type RecordWarningCode = (typeof RECORD_WARNING_CODES)[number];
|
|
89
97
|
|
|
@@ -168,8 +176,8 @@ export interface VerdictAttestation {
|
|
|
168
176
|
}
|
|
169
177
|
|
|
170
178
|
export interface RecordWarning {
|
|
171
|
-
/** A work kind's records also carry the codes of `WORK_WARNING_CODES` (#2683), and `records` adds `SEAL_WARNING_CODES` (#2688). */
|
|
172
|
-
code: RecordWarningCode | WorkWarningCode | SealWarningCode;
|
|
179
|
+
/** A work kind's records also carry the codes of `WORK_WARNING_CODES` (#2683), an answer kind's those of `ANSWER_WARNING_CODES` (#2739), and `records` adds `SEAL_WARNING_CODES` (#2688). */
|
|
180
|
+
code: RecordWarningCode | WorkWarningCode | SealWarningCode | AnswerWarningCode;
|
|
173
181
|
message: string;
|
|
174
182
|
}
|
|
175
183
|
|
|
@@ -299,6 +307,15 @@ export const recordKindSchema = z
|
|
|
299
307
|
* `withdrawn_on`. Optional.
|
|
300
308
|
*/
|
|
301
309
|
reviews: z.object({ field: z.string().min(1), decider: z.string().min(1) }).strict().optional(),
|
|
310
|
+
/**
|
|
311
|
+
* The front-matter object that says where a record came from, opted in
|
|
312
|
+
* to the stored source block (#2708): `via`, `client`, `harness`,
|
|
313
|
+
* `model`, `session`, `turns` and `transcript`, beside the kind's own
|
|
314
|
+
* fields in the same object. With it, those fields are checked for their
|
|
315
|
+
* shapes, a write with `via: "harvest"` must open in the kind's first
|
|
316
|
+
* state, and `records` warns `source-transcript-drift`. Optional.
|
|
317
|
+
*/
|
|
318
|
+
source: z.object({ field: z.string().min(1) }).strict().optional(),
|
|
302
319
|
/**
|
|
303
320
|
* A review-session kind (#2673, #2650 C10): the front-matter list of the
|
|
304
321
|
* verdicts a session produced, the field that seals a closed session, and
|
|
@@ -344,6 +361,17 @@ export const recordKindSchema = z
|
|
|
344
361
|
})
|
|
345
362
|
.strict()
|
|
346
363
|
.optional(),
|
|
364
|
+
/**
|
|
365
|
+
* An answer kind (ws-058, #2739): the decision points file its records
|
|
366
|
+
* answer, relative to this kind file. The records are written by `chant
|
|
367
|
+
* workspace points ask` and `points answer` in the shape of
|
|
368
|
+
* `point-answer.schema.json`, with an `id`, the states `escalated`,
|
|
369
|
+
* `proposed` and `answered`, and `answered` closed. With it, `records`
|
|
370
|
+
* warns about an answer whose point is gone or changed, `check` validates
|
|
371
|
+
* the points file (WSP116), and `points` lists the open questions.
|
|
372
|
+
* Optional.
|
|
373
|
+
*/
|
|
374
|
+
answers: z.object({ points: z.string().min(1) }).strict().optional(),
|
|
347
375
|
})
|
|
348
376
|
.strict()
|
|
349
377
|
.refine((k) => (k.idField === undefined) !== (k.idFrom === undefined), {
|
|
@@ -377,7 +405,23 @@ export const recordKindSchema = z
|
|
|
377
405
|
.refine((k) => !k.work || (k.states !== undefined && k.states.includes(k.work.open) && k.states.includes(k.work.done)), {
|
|
378
406
|
message: "a work kind must have states, and its work open and done states must be listed in them",
|
|
379
407
|
path: ["work"],
|
|
380
|
-
})
|
|
408
|
+
})
|
|
409
|
+
.refine(
|
|
410
|
+
(k) =>
|
|
411
|
+
!k.answers ||
|
|
412
|
+
(k.format === "markdown-front-matter" &&
|
|
413
|
+
k.idField === "id" &&
|
|
414
|
+
k.stateField === "state" &&
|
|
415
|
+
ANSWER_STATES.every((s) => k.states?.includes(s)) &&
|
|
416
|
+
(k.closedStates ?? []).includes("answered")),
|
|
417
|
+
{
|
|
418
|
+
message: 'an answer kind is markdown-front-matter with idField "id", stateField "state", the states escalated, proposed and answered, and answered closed',
|
|
419
|
+
path: ["answers"],
|
|
420
|
+
},
|
|
421
|
+
);
|
|
422
|
+
|
|
423
|
+
/** The states of an answer kind's records (ws-058): open for people, proposed by a model, answered by a table, people, or a confirmed proposal. */
|
|
424
|
+
export const ANSWER_STATES = ["escalated", "proposed", "answered"] as const;
|
|
381
425
|
|
|
382
426
|
export type RecordKind = z.infer<typeof recordKindSchema>;
|
|
383
427
|
|
|
@@ -1076,6 +1120,8 @@ export async function readRecords(loaded: LoadedRecordKind, options: ReadRecords
|
|
|
1076
1120
|
const match = new RegExp(kind.location.match);
|
|
1077
1121
|
const validate = await compileSchema(loaded.schema, loaded.refs);
|
|
1078
1122
|
const workspaceRoot = options.workspaceRoot ?? ".";
|
|
1123
|
+
// Where a relative transcript path resolves (#2708): the workspace root in the working tree.
|
|
1124
|
+
const workspaceDir = workspaceRoot === "." ? options.root : resolve(options.root, ...workspaceRoot.split("/"));
|
|
1079
1125
|
|
|
1080
1126
|
const entries: RecordEntry[] = [];
|
|
1081
1127
|
const texts = new Map<string, string>();
|
|
@@ -1127,6 +1173,16 @@ export async function readRecords(loaded: LoadedRecordKind, options: ReadRecords
|
|
|
1127
1173
|
if (!result.ok) {
|
|
1128
1174
|
entry.reasons.push({ code: "record-schema-invalid", message: result.errors.join("; ") });
|
|
1129
1175
|
}
|
|
1176
|
+
if (kind.source) {
|
|
1177
|
+
// The stored source block (#2708): its proposal fields' shapes, and the transcript it pins.
|
|
1178
|
+
const block = sourceBlock(fm.value, kind.source.field);
|
|
1179
|
+
if (block) {
|
|
1180
|
+
const problems = sourceBlockProblems(block, kind.source.field);
|
|
1181
|
+
if (problems.length > 0) entry.reasons.push({ code: "record-schema-invalid", message: problems.join("; ") });
|
|
1182
|
+
const drift = transcriptDrift(block, kind.source.field, workspaceDir);
|
|
1183
|
+
if (drift) entry.warnings.push({ code: "source-transcript-drift", message: drift });
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1130
1186
|
if (kind.pins) {
|
|
1131
1187
|
const cited = fm.value[kind.pins.field];
|
|
1132
1188
|
if (Array.isArray(cited) && cited.length === 0) {
|
|
@@ -1222,6 +1278,8 @@ export async function readRecords(loaded: LoadedRecordKind, options: ReadRecords
|
|
|
1222
1278
|
joinSessions(kind, entries, texts, options.subjects ?? null);
|
|
1223
1279
|
// A work kind's links, ready and blocked (#2683), from every record before --current.
|
|
1224
1280
|
const work = kind.work ? await (await import("./work")).applyWork(loaded, entries, options) : undefined;
|
|
1281
|
+
// An answer kind's points (ws-058): an answer whose point is gone or changed is warned about.
|
|
1282
|
+
if (kind.answers) (await import("./points")).applyAnswers(loaded, entries, options);
|
|
1225
1283
|
|
|
1226
1284
|
for (const e of entries) e.valid = e.reasons.length === 0;
|
|
1227
1285
|
const records = options.current ? entries.filter((e) => e.supersededBy === null) : entries;
|