@intentius/chant 0.85.0 → 0.87.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.
Files changed (129) hide show
  1. package/dist/cli/main.d.ts.map +1 -1
  2. package/dist/cli/registry.d.ts +13 -1
  3. package/dist/cli/registry.d.ts.map +1 -1
  4. package/dist/lifecycle/gate-ledger.d.ts +13 -0
  5. package/dist/lifecycle/gate-ledger.d.ts.map +1 -1
  6. package/dist/workspace/__fixtures__/sessions.d.ts +23 -0
  7. package/dist/workspace/__fixtures__/sessions.d.ts.map +1 -0
  8. package/dist/workspace/checks/records.d.ts +1 -0
  9. package/dist/workspace/checks/records.d.ts.map +1 -1
  10. package/dist/workspace/checks.d.ts +4 -0
  11. package/dist/workspace/checks.d.ts.map +1 -1
  12. package/dist/workspace/composites.d.ts +14 -1
  13. package/dist/workspace/composites.d.ts.map +1 -1
  14. package/dist/workspace/conformance/index.d.ts +211 -0
  15. package/dist/workspace/conformance/index.d.ts.map +1 -0
  16. package/dist/workspace/conformance/vitest.d.ts +11 -0
  17. package/dist/workspace/conformance/vitest.d.ts.map +1 -0
  18. package/dist/workspace/declaration.d.ts +28 -0
  19. package/dist/workspace/declaration.d.ts.map +1 -1
  20. package/dist/workspace/declaration.schema.json +40 -0
  21. package/dist/workspace/declared-kinds.d.ts +43 -0
  22. package/dist/workspace/declared-kinds.d.ts.map +1 -0
  23. package/dist/workspace/graph-cli.d.ts +11 -0
  24. package/dist/workspace/graph-cli.d.ts.map +1 -1
  25. package/dist/workspace/intent-cli.d.ts +2 -1
  26. package/dist/workspace/intent-cli.d.ts.map +1 -1
  27. package/dist/workspace/intent-joins.d.ts +45 -8
  28. package/dist/workspace/intent-joins.d.ts.map +1 -1
  29. package/dist/workspace/intent.d.ts +71 -7
  30. package/dist/workspace/intent.d.ts.map +1 -1
  31. package/dist/workspace/ls.d.ts +31 -1
  32. package/dist/workspace/ls.d.ts.map +1 -1
  33. package/dist/workspace/reason-codes.d.ts +47 -4
  34. package/dist/workspace/reason-codes.d.ts.map +1 -1
  35. package/dist/workspace/record-sessions.d.ts +51 -0
  36. package/dist/workspace/record-sessions.d.ts.map +1 -0
  37. package/dist/workspace/record-source.d.ts +2 -0
  38. package/dist/workspace/record-source.d.ts.map +1 -1
  39. package/dist/workspace/records-cli.d.ts +71 -4
  40. package/dist/workspace/records-cli.d.ts.map +1 -1
  41. package/dist/workspace/records-since.d.ts +90 -0
  42. package/dist/workspace/records-since.d.ts.map +1 -0
  43. package/dist/workspace/records-write.d.ts +171 -0
  44. package/dist/workspace/records-write.d.ts.map +1 -0
  45. package/dist/workspace/records.d.ts +244 -15
  46. package/dist/workspace/records.d.ts.map +1 -1
  47. package/dist/workspace/runtimes.d.ts +60 -0
  48. package/dist/workspace/runtimes.d.ts.map +1 -0
  49. package/dist/workspace/status-gates.d.ts +90 -0
  50. package/dist/workspace/status-gates.d.ts.map +1 -0
  51. package/dist/workspace/status.d.ts +17 -0
  52. package/dist/workspace/status.d.ts.map +1 -1
  53. package/dist/workspace/trust/seal.d.ts +85 -0
  54. package/dist/workspace/trust/seal.d.ts.map +1 -0
  55. package/dist/workspace/trust/ssh-commit.d.ts +7 -0
  56. package/dist/workspace/trust/ssh-commit.d.ts.map +1 -1
  57. package/dist/workspace/work.d.ts +56 -0
  58. package/dist/workspace/work.d.ts.map +1 -0
  59. package/package.json +19 -1
  60. package/src/cli/main.ts +55 -3
  61. package/src/cli/registry.ts +13 -1
  62. package/src/lifecycle/gate-ledger.ts +14 -0
  63. package/src/workspace/__fixtures__/sessions.ts +66 -0
  64. package/src/workspace/checks/records.ts +19 -0
  65. package/src/workspace/checks.test.ts +2 -0
  66. package/src/workspace/checks.ts +7 -1
  67. package/src/workspace/composites.schema.json +65 -3
  68. package/src/workspace/composites.test.ts +95 -5
  69. package/src/workspace/composites.ts +28 -7
  70. package/src/workspace/conformance/__fixture__/app/package.json +7 -0
  71. package/src/workspace/conformance/__fixture__/app/src/server.mjs +29 -0
  72. package/src/workspace/conformance/__fixture__/decisions/decision.kind.mjs +32 -0
  73. package/src/workspace/conformance/__fixture__/decisions/decision.schema.json +376 -0
  74. package/src/workspace/conformance/__fixture__/decisions/fix-001-how-the-app-is-deployed.md +40 -0
  75. package/src/workspace/conformance/__fixture__/delivery/chant.config.ts +7 -0
  76. package/src/workspace/conformance/__fixture__/delivery/lexicon/index.ts +26 -0
  77. package/src/workspace/conformance/__fixture__/delivery/package.json +7 -0
  78. package/src/workspace/conformance/__fixture__/delivery/src/app.component.ts +14 -0
  79. package/src/workspace/conformance/__fixture__/delivery/src/app.ts +4 -0
  80. package/src/workspace/conformance/conformance.test.ts +149 -0
  81. package/src/workspace/conformance/index.mjs +31 -0
  82. package/src/workspace/conformance/index.ts +453 -0
  83. package/src/workspace/conformance/vitest.ts +62 -0
  84. package/src/workspace/declaration.schema.json +40 -0
  85. package/src/workspace/declaration.ts +62 -0
  86. package/src/workspace/declared-kinds.test.ts +321 -0
  87. package/src/workspace/declared-kinds.ts +76 -0
  88. package/src/workspace/graph-cli.ts +8 -0
  89. package/src/workspace/intent-cli.ts +29 -6
  90. package/src/workspace/intent-gaps.test.ts +217 -0
  91. package/src/workspace/intent-joins.test.ts +60 -0
  92. package/src/workspace/intent-joins.ts +71 -19
  93. package/src/workspace/intent.schema.json +304 -7
  94. package/src/workspace/intent.test.ts +99 -0
  95. package/src/workspace/intent.ts +365 -46
  96. package/src/workspace/ls.schema.json +34 -0
  97. package/src/workspace/ls.ts +69 -4
  98. package/src/workspace/read-contract.test.ts +30 -9
  99. package/src/workspace/reason-codes.test.ts +16 -4
  100. package/src/workspace/reason-codes.ts +55 -4
  101. package/src/workspace/record-assets.test.ts +3 -1
  102. package/src/workspace/record-sessions.ts +105 -0
  103. package/src/workspace/record-source.ts +14 -5
  104. package/src/workspace/records-amend.schema.json +167 -0
  105. package/src/workspace/records-cli.ts +308 -19
  106. package/src/workspace/records-contract.test.ts +57 -2
  107. package/src/workspace/records-formats.test.ts +640 -0
  108. package/src/workspace/records-new.schema.json +158 -0
  109. package/src/workspace/records-quorum.test.ts +196 -0
  110. package/src/workspace/records-review.schema.json +227 -0
  111. package/src/workspace/records-sessions.test.ts +108 -0
  112. package/src/workspace/records-since.schema.json +193 -0
  113. package/src/workspace/records-since.test.ts +174 -0
  114. package/src/workspace/records-since.ts +259 -0
  115. package/src/workspace/records-write-contract.test.ts +125 -0
  116. package/src/workspace/records-write.test.ts +373 -0
  117. package/src/workspace/records-write.ts +765 -0
  118. package/src/workspace/records.schema.json +202 -9
  119. package/src/workspace/records.ts +700 -41
  120. package/src/workspace/runtimes.ts +107 -0
  121. package/src/workspace/status-contract.test.ts +163 -0
  122. package/src/workspace/status-gates.ts +215 -0
  123. package/src/workspace/status.schema.json +69 -3
  124. package/src/workspace/status.ts +35 -2
  125. package/src/workspace/trust/seal.test.ts +232 -0
  126. package/src/workspace/trust/seal.ts +195 -0
  127. package/src/workspace/trust/ssh-commit.ts +2 -2
  128. package/src/workspace/work.test.ts +390 -0
  129. package/src/workspace/work.ts +163 -0
@@ -0,0 +1,167 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://intentius.io/chant/schemas/workspace/records-amend/v1/records-amend.schema.json",
4
+ "title": "chant workspace records amend output",
5
+ "description": "What `chant workspace records amend <id> --kind <kind file> --set <file|->` prints (#2670): the record's path and id and the top-level fields the amendment changed, or the reason it wrote nothing. Version 1 of the write contract for records. The command writes one file or none and never commits. Readers ignore fields they do not know; a field is only ever added within a version. The error codes are a closed list, each in the one closed list of `reason-codes.ts`. Contract version 1 of this document is written by chant 0.86.0 and newer.",
6
+ "oneOf": [
7
+ {
8
+ "$ref": "#/$defs/result"
9
+ },
10
+ {
11
+ "$ref": "#/$defs/failure"
12
+ }
13
+ ],
14
+ "$defs": {
15
+ "result": {
16
+ "description": "The record was written, or would be with --dry-run. Exit code 0.",
17
+ "type": "object",
18
+ "required": [
19
+ "$schema",
20
+ "contract",
21
+ "kind",
22
+ "path",
23
+ "id",
24
+ "changed",
25
+ "dryRun",
26
+ "warnings"
27
+ ],
28
+ "properties": {
29
+ "$schema": {
30
+ "const": "https://intentius.io/chant/schemas/workspace/records-amend/v1/records-amend.schema.json"
31
+ },
32
+ "contract": {
33
+ "const": 1
34
+ },
35
+ "kind": {
36
+ "type": "object",
37
+ "required": [
38
+ "name",
39
+ "schema",
40
+ "file"
41
+ ],
42
+ "properties": {
43
+ "name": {
44
+ "type": "string",
45
+ "description": "The kind's name, such as \"decision\"."
46
+ },
47
+ "schema": {
48
+ "type": "string",
49
+ "description": "The `$id` of the schema the record was validated against."
50
+ },
51
+ "file": {
52
+ "type": "string",
53
+ "description": "The kind file, relative to the repository root, with / separators."
54
+ }
55
+ }
56
+ },
57
+ "path": {
58
+ "type": "string",
59
+ "description": "The record file, relative to the repository root (the working directory outside git), with / separators."
60
+ },
61
+ "id": {
62
+ "type": "string",
63
+ "description": "The record's id."
64
+ },
65
+ "changed": {
66
+ "type": "array",
67
+ "items": {
68
+ "type": "string"
69
+ },
70
+ "description": "The top-level fields whose value differs from the record as it was, in the record's order. Empty when the fields set change nothing, and then nothing is written."
71
+ },
72
+ "dryRun": {
73
+ "type": "boolean",
74
+ "description": "True when --dry-run was given and nothing was written."
75
+ },
76
+ "warnings": {
77
+ "type": "array",
78
+ "description": "The written record's warnings, as chant workspace records reports them. A warning never refuses a write.",
79
+ "items": {
80
+ "$ref": "#/$defs/warning"
81
+ }
82
+ },
83
+ "text": {
84
+ "type": "string",
85
+ "description": "With --dry-run only: the whole text of the file the command would write."
86
+ },
87
+ "error": false
88
+ }
89
+ },
90
+ "warning": {
91
+ "type": "object",
92
+ "required": [
93
+ "code",
94
+ "message"
95
+ ],
96
+ "properties": {
97
+ "code": {
98
+ "enum": [
99
+ "asset-drift",
100
+ "asset-missing",
101
+ "asset-stale",
102
+ "record-supersedes-pending",
103
+ "record-no-evidence",
104
+ "review-undigested"
105
+ ]
106
+ },
107
+ "message": {
108
+ "type": "string"
109
+ }
110
+ }
111
+ },
112
+ "failure": {
113
+ "description": "Nothing was written. Exit code 1.",
114
+ "type": "object",
115
+ "required": [
116
+ "$schema",
117
+ "contract",
118
+ "error"
119
+ ],
120
+ "properties": {
121
+ "$schema": {
122
+ "const": "https://intentius.io/chant/schemas/workspace/records-amend/v1/records-amend.schema.json"
123
+ },
124
+ "contract": {
125
+ "const": 1
126
+ },
127
+ "error": {
128
+ "type": "object",
129
+ "required": [
130
+ "code",
131
+ "message"
132
+ ],
133
+ "properties": {
134
+ "code": {
135
+ "enum": [
136
+ "kind-unreadable",
137
+ "kind-invalid",
138
+ "schema-unreadable",
139
+ "schema-id-mismatch",
140
+ "schema-invalid",
141
+ "location-missing",
142
+ "write-usage-invalid",
143
+ "write-input-invalid",
144
+ "record-not-found",
145
+ "amend-id-immutable",
146
+ "record-closed",
147
+ "amend-supersede-instead",
148
+ "record-unparseable",
149
+ "record-schema-invalid",
150
+ "record-id-duplicate",
151
+ "record-supersedes-unknown",
152
+ "record-supersedes-conflict",
153
+ "session-seal-mismatch",
154
+ "session-verdict-unknown-record"
155
+ ]
156
+ },
157
+ "message": {
158
+ "type": "string",
159
+ "description": "What was wrong, and what to do instead when there is something, such as writing a new record that supersedes this one."
160
+ }
161
+ }
162
+ },
163
+ "path": false
164
+ }
165
+ }
166
+ }
167
+ }
@@ -8,8 +8,13 @@
8
8
  * the exit code is 0 whenever the read itself worked. Only a kind, schema or
9
9
  * revision that cannot be read exits 1.
10
10
  *
11
- * It needs no `chant.workspace.json`. The kind is passed explicitly, so
12
- * nothing is inferred (#2525 rule 1).
11
+ * It needs no `chant.workspace.json`. The kind is passed explicitly, or,
12
+ * without `--kind`, it is every record kind the declaration names (#2680), so
13
+ * nothing is inferred (#2525 rule 1). Several kinds print one document per
14
+ * kind, in the declaration's order, inside one set.
15
+ *
16
+ * With `--since <rev>` it prints what changed between two revisions instead,
17
+ * through `records-since.ts` (#2673).
13
18
  */
14
19
 
15
20
  import { execFileSync } from "node:child_process";
@@ -20,8 +25,27 @@ import type { CommandContext } from "../cli/registry";
20
25
  import { findWorkspaceRoot } from "../project-root";
21
26
  import { fileDigest, isWorkspacePath } from "./record-assets";
22
27
  import { gitRevisionSource, gitRoot, resolveRevision, workingTreeSource } from "./record-source";
23
- import { loadRecordKind, readRecords, RecordReadError, type LoadedRecordKind, type ReadErrorCode, type ReadRecordsResult, type RecordEntry, type RecordHistory } from "./records";
24
- import { gitTree, workingTree } from "./tree";
28
+ import { declaredRecordKinds, readDeclaration, WorkspaceReadError, type RecordKindDeclaration } from "./declaration";
29
+ import { declaredKindFile } from "./declared-kinds";
30
+ import { locateWorkspace } from "./which-chant";
31
+ import {
32
+ computeQuorum,
33
+ DEFAULT_QUORUM,
34
+ loadRecordKind,
35
+ normalisePrincipal,
36
+ readRecords,
37
+ RecordReadError,
38
+ type LoadedRecordKind,
39
+ type Quorum,
40
+ type ReadErrorCode,
41
+ type ReadRecordsResult,
42
+ type RecordEntry,
43
+ type RecordFormat,
44
+ type RecordHistory,
45
+ type SealInput,
46
+ } from "./records";
47
+ import { gitTree, workingTree, type WorkspaceTree } from "./tree";
48
+ import type { DecisionWork } from "./work";
25
49
  import { activeAttestors, type ProvenanceLevel } from "./trust/attestor";
26
50
  import { policyAtBase, recordProvenance, resolveBase, type BaseSource, type RecordProvenance } from "./trust/provenance";
27
51
 
@@ -31,7 +55,8 @@ export const RECORDS_CONTRACT_VERSION = 1;
31
55
  /** `$id` of the JSON Schema for the `--json` output, shipped beside this file. */
32
56
  export const RECORDS_OUTPUT_SCHEMA_ID = "https://intentius.io/chant/schemas/workspace/records/v1/records.schema.json";
33
57
 
34
- const USAGE = "chant workspace records --kind <kind file> [--current] [--at <rev>] [--base <rev>] [--require attested] [--json] | chant workspace records pin <path>";
58
+ const USAGE =
59
+ "chant workspace records [--kind <kind file>] [--current] [--at <rev>] [--base <rev>] [--require attested] [--json] | chant workspace records [--kind <kind file>] --since <rev> [--at <rev>] [--json] | chant workspace records pin <path> | chant workspace records new|amend|review (#2670)";
35
60
 
36
61
  /** Exit code when the read worked and a record falls below `--require`. */
37
62
  export const EXIT_BELOW_REQUIRED = 2;
@@ -44,10 +69,22 @@ export interface RecordsQuery {
44
69
  base?: string;
45
70
  /** Where `kind` is resolved from and the repository is found. */
46
71
  cwd: string;
72
+ /**
73
+ * For a work kind: walk the region of each done item's `source` and raise
74
+ * `work-done-gap-open` when its finding still fires (#2686). On unless
75
+ * false; the intent graph passes false, since it raises the warning itself.
76
+ */
77
+ workGaps?: boolean;
47
78
  }
48
79
 
49
- /** A record as the output carries it: the entry plus its provenance (#2547). */
50
- export type RecordView = RecordEntry & { provenance: RecordProvenance };
80
+ /**
81
+ * A record as the output carries it: the entry plus its provenance (#2547),
82
+ * and its quorum when the kind has a reviews list (#2671).
83
+ */
84
+ export type RecordView = RecordEntry & { provenance: RecordProvenance; quorum?: Quorum | null };
85
+
86
+ /** The role in the trust policy whose holders' verdicts the quorum does not count (#2671). */
87
+ export const AGENT_ROLE = "agent";
51
88
 
52
89
  /** Where provenance was judged from (#2547). */
53
90
  export interface TrustView {
@@ -65,7 +102,7 @@ export type RecordsDocument =
65
102
  | {
66
103
  $schema: string;
67
104
  contract: number;
68
- kind: { name: string; schema: string; file: string };
105
+ kind: { name: string; schema: string; file: string; format: RecordFormat };
69
106
  at: string | null;
70
107
  /** The directory pinned paths resolve in, from the repository root: the workspace holding the kind file, or the repository root (#2549). */
71
108
  workspaceRoot: string;
@@ -73,9 +110,58 @@ export type RecordsDocument =
73
110
  trust: TrustView;
74
111
  records: RecordView[];
75
112
  summary: { total: number; valid: number; invalid: number; superseded: number };
113
+ /** For a work kind (#2683): each decision its decision kind reads, with the work records implementing it. */
114
+ decisions?: DecisionWork[];
76
115
  }
77
116
  | { $schema: string; contract: number; error: { code: ReadErrorCode; message: string } };
78
117
 
118
+ /** Where a document in a {@link RecordsSetDocument} comes from: the declaration's entry for its kind (#2680). */
119
+ export interface DeclaredKindView {
120
+ /** The member that declares the kind, or null for the workspace's own. */
121
+ member: string | null;
122
+ /** The kind file from the workspace root. */
123
+ path: string;
124
+ /** The name the declaration gives the kind, or null. */
125
+ name: string | null;
126
+ }
127
+
128
+ /**
129
+ * The `records` output without `--kind` when the declaration names record
130
+ * kinds (#2680): one {@link RecordsDocument} per kind, in the declaration's
131
+ * order, each with the entry that declares it.
132
+ */
133
+ export interface RecordsSetDocument {
134
+ $schema: string;
135
+ contract: number;
136
+ kinds: (RecordsDocument & { declared: DeclaredKindView })[];
137
+ }
138
+
139
+ /**
140
+ * The record kinds the declaration nearest above `cwd` names, in the tree
141
+ * `at` reads, with each kind file on disk. Empty when there is no declaration.
142
+ * Throws a {@link WorkspaceReadError} for one that can't be read.
143
+ */
144
+ export function declaredKindFiles(cwd: string, at?: string): { declared: RecordKindDeclaration; file: string }[] {
145
+ let located;
146
+ try {
147
+ located = locateWorkspace(cwd, at);
148
+ } catch (err) {
149
+ if (err instanceof WorkspaceReadError && err.code === "declaration-missing") return [];
150
+ throw err;
151
+ }
152
+ return declaredRecordKinds(readDeclaration(located.tree)).map((declared) => ({ declared, file: declaredKindFile(declared, located.rootOnDisk) }));
153
+ }
154
+
155
+ /** Read every declared kind in `kinds`, as {@link queryRecords} reads one. */
156
+ export async function queryDeclaredRecords(kinds: { declared: RecordKindDeclaration; file: string }[], query: Omit<RecordsQuery, "kind">): Promise<RecordsSetDocument> {
157
+ const out: RecordsSetDocument["kinds"] = [];
158
+ for (const k of kinds) {
159
+ const doc = await queryRecords({ ...query, kind: k.file });
160
+ out.push({ ...doc, declared: { member: k.declared.member, path: k.declared.path, name: k.declared.name } });
161
+ }
162
+ return { $schema: RECORDS_OUTPUT_SCHEMA_ID, contract: RECORDS_CONTRACT_VERSION, kinds: out };
163
+ }
164
+
79
165
  /** A records read, before provenance. */
80
166
  export interface RecordsRead {
81
167
  loaded: LoadedRecordKind;
@@ -86,15 +172,32 @@ export interface RecordsRead {
86
172
  at: string | null;
87
173
  /** Where pinned paths resolve, relative to `root` ("." for the root itself). */
88
174
  workspaceRoot: string;
175
+ /** The workspace root's tree, as read: the working tree, or the revision under `--at`. */
176
+ tree: WorkspaceTree;
89
177
  result: ReadRecordsResult;
90
178
  }
91
179
 
180
+ /**
181
+ * The quorum the workspace declares: the declaration at the workspace root
182
+ * of the tree read, or the default when there is none, or when it can't be
183
+ * read (#2671). Records need no declaration, so neither does this.
184
+ */
185
+ function declaredQuorum(tree: WorkspaceTree): { need: number; needFrom: "declaration" | "default" } {
186
+ try {
187
+ const q = readDeclaration(tree).quorum;
188
+ if (q !== null) return { need: q, needFrom: "declaration" };
189
+ } catch {
190
+ // No declaration, or one this read can't use: the default applies.
191
+ }
192
+ return { need: DEFAULT_QUORUM, needFrom: "default" };
193
+ }
194
+
92
195
  /**
93
196
  * Where a kind's pinned paths resolve: the workspace whose declaration sits
94
197
  * nearest above the kind file, when it is inside the repository, or else the
95
198
  * repository root. Relative to `root`, with / separators.
96
199
  */
97
- function pinRoot(kindFile: string, root: string): string {
200
+ export function pinRoot(kindFile: string, root: string): string {
98
201
  const found = findWorkspaceRoot(dirname(kindFile));
99
202
  if (!found) return ".";
100
203
  const rel = relative(root, realpathOr(found.dir)).split(sep).join("/");
@@ -146,14 +249,20 @@ export async function readRecordsFor(query: Omit<RecordsQuery, "base">): Promise
146
249
  assets = gitTree(top, at, workspaceRoot === "." ? "" : workspaceRoot);
147
250
  }
148
251
  const history = top ? gitHistory(top, at ?? "HEAD", workspaceRoot) : undefined;
149
- const result = await readRecords(loaded, { root, source, current: !!query.current, assets, ...(history ? { history } : {}) });
150
- return { loaded, root, top, at, workspaceRoot, result };
252
+ // A session kind's verdicts name records of another kind, read from the same tree (#2673).
253
+ let subjects: { records: RecordEntry[]; reviews: string } | undefined;
254
+ if (loaded.kind.session) {
255
+ const subjectKind = await loadRecordKind(resolve(dirname(loaded.file), loaded.kind.session.subjects.kind), cwd);
256
+ subjects = { records: (await readRecords(subjectKind, { root, source })).records, reviews: subjectKind.kind.reviews?.field ?? "reviews" };
257
+ }
258
+ const result = await readRecords(loaded, { root, source, current: !!query.current, assets, workspaceRoot, ...(history ? { history } : {}), ...(subjects ? { subjects } : {}) });
259
+ return { loaded, root, top, at, workspaceRoot, tree: assets, result };
151
260
  }
152
261
 
153
262
  /** Run the query and build the document `--json` prints. Never throws a {@link RecordReadError}. */
154
263
  export async function queryRecords(query: RecordsQuery): Promise<RecordsDocument> {
155
264
  try {
156
- const { loaded, root, top, at, workspaceRoot, result } = await readRecordsFor(query);
265
+ const { loaded, root, top, at, workspaceRoot, tree, result } = await readRecordsFor(query);
157
266
  // Provenance, judged by the policy at base and never by the tree read (#2547).
158
267
  const base = top ? resolveBase(top, query.base) : { commit: null, from: null };
159
268
  const policy = top ? policyAtBase(top, base) : policyAtBase(root, base);
@@ -164,6 +273,24 @@ export async function queryRecords(query: RecordsQuery): Promise<RecordsDocument
164
273
  paths: result.records.map((r) => r.path),
165
274
  attestors: policy.active ? await activeAttestors() : [],
166
275
  });
276
+ // The quorum: the need from the declaration in the tree read, agents,
277
+ // whether verdicts need a seal, and the keys a seal verifies against,
278
+ // all from the policy at base (#2671, #2687).
279
+ const checkVerdictSeal = loaded.kind.reviews ? (await import("./trust/seal")).checkVerdictSeal : undefined;
280
+ const quorumOptions = checkVerdictSeal
281
+ ? {
282
+ ...declaredQuorum(tree),
283
+ agents: new Set((policy.roles[AGENT_ROLE] ?? []).map(normalisePrincipal)),
284
+ attestation: policy.active,
285
+ verifySeal: (v: SealInput) => checkVerdictSeal(policy, v),
286
+ }
287
+ : undefined;
288
+ const records: RecordView[] = result.records.map((r) => ({
289
+ ...r,
290
+ provenance: provenance.get(r.path)!,
291
+ ...(quorumOptions ? { quorum: computeQuorum(loaded.kind, r, quorumOptions) } : {}),
292
+ }));
293
+ if (loaded.kind.work && query.workGaps !== false && top) await raiseWorkGaps(loaded, records, { root, workspaceRoot, at: query.at });
167
294
  return {
168
295
  $schema: RECORDS_OUTPUT_SCHEMA_ID,
169
296
  contract: RECORDS_CONTRACT_VERSION,
@@ -171,13 +298,15 @@ export async function queryRecords(query: RecordsQuery): Promise<RecordsDocument
171
298
  name: loaded.kind.name,
172
299
  schema: loaded.kind.schema.id,
173
300
  file: relative(root, loaded.file).split("\\").join("/"),
301
+ format: loaded.kind.format,
174
302
  },
175
303
  at,
176
304
  workspaceRoot,
177
305
  current: !!query.current,
178
306
  trust: { base: base.commit, baseFrom: base.from, active: policy.active, signersPath: policy.signersPath, problems: policy.problems },
179
- records: result.records.map((r) => ({ ...r, provenance: provenance.get(r.path)! })),
307
+ records,
180
308
  summary: result.summary,
309
+ ...(result.decisions ? { decisions: result.decisions } : {}),
181
310
  };
182
311
  } catch (err) {
183
312
  if (!(err instanceof RecordReadError)) throw err;
@@ -185,6 +314,56 @@ export async function queryRecords(query: RecordsQuery): Promise<RecordsDocument
185
314
  }
186
315
  }
187
316
 
317
+ /**
318
+ * `work-done-gap-open` on a records read (#2686): for each done work record
319
+ * whose `source` names a finding and a region, walk that region with the
320
+ * intent graph, at the same revision, and copy the warning the walk raises
321
+ * on the record. One walk per region, and only a region some done item names.
322
+ * The walk reads the record kinds the declaration names, and the work kind
323
+ * and its decision kind if it names neither. It needs git and a workspace
324
+ * declaration; when the walk can't be made, for one without them or a region
325
+ * that no longer exists, the record is left as it was.
326
+ */
327
+ async function raiseWorkGaps(loaded: LoadedRecordKind, records: RecordView[], opts: { root: string; workspaceRoot: string; at: string | undefined }): Promise<void> {
328
+ const work = loaded.kind.work!;
329
+ const byRegion = new Map<string, RecordView[]>();
330
+ for (const r of records) {
331
+ if (r.id === null || r.state !== work.done) continue;
332
+ const src = r.data?.source;
333
+ if (src === null || typeof src !== "object" || Array.isArray(src)) continue;
334
+ const { finding, region } = src as Record<string, unknown>;
335
+ if (typeof finding !== "string" || typeof region !== "string") continue;
336
+ byRegion.set(region, [...(byRegion.get(region) ?? []), r]);
337
+ }
338
+ if (byRegion.size === 0) return;
339
+ const cwd = opts.workspaceRoot === "." ? opts.root : join(opts.root, ...opts.workspaceRoot.split("/"));
340
+ let declared: string[];
341
+ try {
342
+ declared = declaredKindFiles(cwd, opts.at).map((k) => k.file);
343
+ } catch (err) {
344
+ if (err instanceof WorkspaceReadError) return;
345
+ throw err;
346
+ }
347
+ const kinds: string[] = [];
348
+ const seen = new Set<string>();
349
+ for (const file of [...declared, resolve(dirname(loaded.file), work.decisions), loaded.file]) {
350
+ const real = realpathOr(file);
351
+ if (seen.has(real)) continue;
352
+ seen.add(real);
353
+ kinds.push(file);
354
+ }
355
+ const { intentGraph } = await import("./intent");
356
+ for (const [region, items] of byRegion) {
357
+ const { doc } = await intentGraph({ cwd, region, at: opts.at, kinds });
358
+ if ("error" in doc) continue;
359
+ for (const r of items) {
360
+ const node = doc.nodes.find((n) => n.kind === "work" && n.id === `record:${loaded.kind.name}/${r.id}`);
361
+ const warning = node?.kind === "work" ? node.warnings.find((w) => w.code === "work-done-gap-open") : undefined;
362
+ if (warning && !r.warnings.some((w) => w.code === warning.code)) r.warnings.push(warning);
363
+ }
364
+ }
365
+ }
366
+
188
367
  /**
189
368
  * `chant workspace records pin <path>`: the `{path, sha256}` a decision's
190
369
  * evidence entry holds for a file, with the path from the workspace root
@@ -204,6 +383,9 @@ export function pinFile(file: string, cwd: string): { path: string; sha256: stri
204
383
 
205
384
  export async function runWorkspaceRecords(ctx: CommandContext): Promise<number> {
206
385
  const { args } = ctx;
386
+ if (args.extraPositional === "new" || args.extraPositional === "amend" || args.extraPositional === "review") {
387
+ return (await import("./records-write")).runRecordsWrite(ctx);
388
+ }
207
389
  if (args.extraPositional === "pin") {
208
390
  if (!args.extraPositional2) {
209
391
  console.error(formatError({ message: "pin needs the path of a file", hint: USAGE }));
@@ -218,17 +400,27 @@ export async function runWorkspaceRecords(ctx: CommandContext): Promise<number>
218
400
  return 0;
219
401
  }
220
402
  if (args.extraPositional) {
221
- console.error(formatError({ message: `chant workspace records takes no argument but pin (got ${args.extraPositional})`, hint: USAGE }));
222
- return 1;
223
- }
224
- if (!args.kind) {
225
- console.error(formatError({ message: "--kind <kind file> is required", hint: USAGE }));
403
+ console.error(formatError({ message: `chant workspace records takes no argument but pin, new, amend or review (got ${args.extraPositional})`, hint: USAGE }));
226
404
  return 1;
227
405
  }
406
+ if (!args.kind) return runDeclaredRecords(args);
228
407
  if (args.require !== undefined && args.require !== "attested") {
229
408
  console.error(formatError({ message: `--require takes one level, attested, not ${JSON.stringify(args.require)}`, hint: USAGE }));
230
409
  return 1;
231
410
  }
411
+ if (args.since !== undefined) {
412
+ if (args.current || args.require !== undefined || args.base !== undefined) {
413
+ console.error(formatError({ message: "--since compares two revisions and takes no --current, --require or --base", hint: USAGE }));
414
+ return 1;
415
+ }
416
+ // Loaded here, so a plain records read never loads it (#2673).
417
+ const { formatSince, queryRecordsSince } = await import("./records-since");
418
+ const since = await queryRecordsSince({ kind: args.kind, since: args.since, at: args.at, cwd: process.cwd() });
419
+ if (args.json) console.log(JSON.stringify(since, null, 2));
420
+ else if ("error" in since) console.error(formatError({ message: `${since.error.code}: ${since.error.message}`, hint: USAGE }));
421
+ else console.log(formatSince(since));
422
+ return "error" in since ? 1 : 0;
423
+ }
232
424
  const doc = await queryRecords({ kind: args.kind, current: args.current, at: args.at, base: args.base, cwd: process.cwd() });
233
425
  if (args.json) {
234
426
  console.log(JSON.stringify(doc, null, 2));
@@ -256,7 +448,92 @@ export async function runWorkspaceRecords(ctx: CommandContext): Promise<number>
256
448
  return 0;
257
449
  }
258
450
 
259
- function realpathOr(dir: string): string {
451
+ /**
452
+ * `records` without `--kind` (#2680): every record kind the declaration
453
+ * names, or, when it names none or there is no declaration, the error it has
454
+ * always been. A kind whose read fails is listed with its error, the others
455
+ * are still read, and the exit code is 1.
456
+ */
457
+ async function runDeclaredRecords(args: CommandContext["args"]): Promise<number> {
458
+ if (args.require !== undefined && args.require !== "attested") {
459
+ console.error(formatError({ message: `--require takes one level, attested, not ${JSON.stringify(args.require)}`, hint: USAGE }));
460
+ return 1;
461
+ }
462
+ let declared: { declared: RecordKindDeclaration; file: string }[];
463
+ try {
464
+ declared = declaredKindFiles(process.cwd(), args.at);
465
+ } catch (err) {
466
+ if (!(err instanceof WorkspaceReadError)) throw err;
467
+ console.error(formatError({ message: `${err.code}: ${err.describe()}; without --kind, the declaration names the record kinds`, hint: USAGE }));
468
+ return 1;
469
+ }
470
+ if (declared.length === 0) {
471
+ console.error(formatError({ message: "--kind <kind file> is required", hint: USAGE }));
472
+ return 1;
473
+ }
474
+ if (args.since !== undefined) return runDeclaredSince(declared, args);
475
+ const set = await queryDeclaredRecords(declared, { current: args.current, at: args.at, base: args.base, cwd: process.cwd() });
476
+ if (args.json) console.log(JSON.stringify(set, null, 2));
477
+ for (const doc of args.json ? [] : set.kinds) {
478
+ if ("error" in doc) {
479
+ console.error(formatError({ message: `${doc.declared.path}: ${doc.error.code}: ${doc.error.message}`, hint: USAGE }));
480
+ continue;
481
+ }
482
+ console.log(`${doc.declared.name ?? doc.kind.name} (${doc.declared.path})`);
483
+ console.log(formatRecords(doc.records, doc.summary, doc.at));
484
+ }
485
+ if (set.kinds.some((d) => "error" in d)) return 1;
486
+ if (args.require) {
487
+ const results = set.kinds.filter((d): d is Extract<typeof d, { records: unknown }> => !("error" in d));
488
+ const all = results.flatMap((d) => d.records);
489
+ const below = belowRequired(all, args.require);
490
+ if (below.length > 0) {
491
+ const inactive = results.find((d) => !d.trust.active);
492
+ console.error(
493
+ formatError({
494
+ message: `${below.length} of ${all.length} records are not ${args.require}: ${below
495
+ .slice(0, 5)
496
+ .map((r) => `${r.path} (${r.provenance.level})`)
497
+ .join(", ")}${below.length > 5 ? ", ..." : ""}`,
498
+ hint: inactive ? `there is no signers file (${inactive.trust.signersPath}) at base` : "run with --json to see each record's reason",
499
+ }),
500
+ );
501
+ return EXIT_BELOW_REQUIRED;
502
+ }
503
+ }
504
+ return 0;
505
+ }
506
+
507
+ /**
508
+ * `records --since` without `--kind` (#2680): what changed in every declared
509
+ * kind, one `records-since` document per kind inside one set, in the
510
+ * declaration's order. A kind whose read fails is listed with its error, and
511
+ * the exit code is 1.
512
+ */
513
+ async function runDeclaredSince(declared: { declared: RecordKindDeclaration; file: string }[], args: CommandContext["args"]): Promise<number> {
514
+ if (args.current || args.require !== undefined || args.base !== undefined) {
515
+ console.error(formatError({ message: "--since compares two revisions and takes no --current, --require or --base", hint: USAGE }));
516
+ return 1;
517
+ }
518
+ const { formatSince, queryRecordsSince, RECORDS_SINCE_OUTPUT_SCHEMA_ID } = await import("./records-since");
519
+ const kinds = [];
520
+ for (const k of declared) {
521
+ const doc = await queryRecordsSince({ kind: k.file, since: args.since!, at: args.at, cwd: process.cwd() });
522
+ kinds.push({ ...doc, declared: { member: k.declared.member, path: k.declared.path, name: k.declared.name } });
523
+ }
524
+ if (args.json) console.log(JSON.stringify({ $schema: RECORDS_SINCE_OUTPUT_SCHEMA_ID, contract: RECORDS_CONTRACT_VERSION, kinds }, null, 2));
525
+ for (const doc of args.json ? [] : kinds) {
526
+ if ("error" in doc) {
527
+ console.error(formatError({ message: `${doc.declared.path}: ${doc.error.code}: ${doc.error.message}`, hint: USAGE }));
528
+ continue;
529
+ }
530
+ console.log(`${doc.declared.name ?? doc.kind.name} (${doc.declared.path})`);
531
+ console.log(formatSince(doc));
532
+ }
533
+ return kinds.some((d) => "error" in d) ? 1 : 0;
534
+ }
535
+
536
+ export function realpathOr(dir: string): string {
260
537
  try {
261
538
  return realpathSync(dir);
262
539
  } catch {
@@ -279,8 +556,20 @@ function formatRecords(records: RecordView[], summary: { total: number; valid: n
279
556
  const superseded = r.supersededBy ? ` superseded by ${r.supersededBy}` : "";
280
557
  const attested = r.provenance.level === "attested" ? ` attested by ${r.provenance.principal}` : "";
281
558
  lines.push(`${(r.id ?? "-").padEnd(idWidth)} ${(r.state ?? "-").padEnd(stateWidth)} ${title}${superseded}${attested}${flag}`);
559
+ if (r.ready !== undefined) {
560
+ const blocked = (r.blockedBy ?? []).map((b) => `${b.id} (${b.state ?? "unknown"})`).join(", ");
561
+ const implemented = (r.implements ?? []).map((d) => `${d.id} (${d.state ?? "unknown"})`).join(", ");
562
+ const status = [r.ready ? "ready" : blocked ? `blocked by ${blocked}` : "", implemented ? `implements ${implemented}` : ""].filter(Boolean).join("; ");
563
+ if (status) lines.push(`${" ".repeat(idWidth + 2)}${status}`);
564
+ }
282
565
  for (const reason of r.reasons) lines.push(`${" ".repeat(idWidth + 2)}${reason.code}: ${reason.message} (${r.path})`);
283
566
  for (const warning of r.warnings) lines.push(`${" ".repeat(idWidth + 2)}warning ${warning.code}: ${warning.message} (${r.path})`);
567
+ const q = r.quorum;
568
+ if (q && q.counted.length + q.notCounted.length > 0) {
569
+ const verdict = q.metWithObjections ? "met with objections" : q.met ? "met" : "not met";
570
+ const concerns = q.openConcerns.length > 0 ? `, ${q.openConcerns.length} open ${q.openConcerns.length === 1 ? "concern" : "concerns"}` : "";
571
+ lines.push(`${" ".repeat(idWidth + 2)}quorum ${q.agreed} of ${q.need} agreed, ${verdict}; ${q.notCounted.length} not counted${concerns}`);
572
+ }
284
573
  }
285
574
  lines.push(
286
575
  `${summary.total} records${at ? ` at ${at.slice(0, 8)}` : ""}: ${summary.valid} valid, ${summary.invalid} invalid, ${summary.superseded} superseded`,