@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.
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/registry.d.ts +13 -1
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/lifecycle/gate-ledger.d.ts +13 -0
- package/dist/lifecycle/gate-ledger.d.ts.map +1 -1
- package/dist/workspace/__fixtures__/sessions.d.ts +23 -0
- package/dist/workspace/__fixtures__/sessions.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 +4 -0
- package/dist/workspace/checks.d.ts.map +1 -1
- package/dist/workspace/composites.d.ts +14 -1
- package/dist/workspace/composites.d.ts.map +1 -1
- package/dist/workspace/conformance/index.d.ts +211 -0
- package/dist/workspace/conformance/index.d.ts.map +1 -0
- package/dist/workspace/conformance/vitest.d.ts +11 -0
- package/dist/workspace/conformance/vitest.d.ts.map +1 -0
- package/dist/workspace/declaration.d.ts +28 -0
- package/dist/workspace/declaration.d.ts.map +1 -1
- package/dist/workspace/declaration.schema.json +40 -0
- package/dist/workspace/declared-kinds.d.ts +43 -0
- package/dist/workspace/declared-kinds.d.ts.map +1 -0
- package/dist/workspace/graph-cli.d.ts +11 -0
- package/dist/workspace/graph-cli.d.ts.map +1 -1
- package/dist/workspace/intent-cli.d.ts +2 -1
- package/dist/workspace/intent-cli.d.ts.map +1 -1
- package/dist/workspace/intent-joins.d.ts +45 -8
- package/dist/workspace/intent-joins.d.ts.map +1 -1
- package/dist/workspace/intent.d.ts +71 -7
- package/dist/workspace/intent.d.ts.map +1 -1
- package/dist/workspace/ls.d.ts +31 -1
- package/dist/workspace/ls.d.ts.map +1 -1
- package/dist/workspace/reason-codes.d.ts +47 -4
- package/dist/workspace/reason-codes.d.ts.map +1 -1
- package/dist/workspace/record-sessions.d.ts +51 -0
- package/dist/workspace/record-sessions.d.ts.map +1 -0
- package/dist/workspace/record-source.d.ts +2 -0
- package/dist/workspace/record-source.d.ts.map +1 -1
- package/dist/workspace/records-cli.d.ts +71 -4
- package/dist/workspace/records-cli.d.ts.map +1 -1
- package/dist/workspace/records-since.d.ts +90 -0
- package/dist/workspace/records-since.d.ts.map +1 -0
- package/dist/workspace/records-write.d.ts +171 -0
- package/dist/workspace/records-write.d.ts.map +1 -0
- package/dist/workspace/records.d.ts +244 -15
- package/dist/workspace/records.d.ts.map +1 -1
- package/dist/workspace/runtimes.d.ts +60 -0
- package/dist/workspace/runtimes.d.ts.map +1 -0
- package/dist/workspace/status-gates.d.ts +90 -0
- package/dist/workspace/status-gates.d.ts.map +1 -0
- package/dist/workspace/status.d.ts +17 -0
- package/dist/workspace/status.d.ts.map +1 -1
- package/dist/workspace/trust/seal.d.ts +85 -0
- package/dist/workspace/trust/seal.d.ts.map +1 -0
- package/dist/workspace/trust/ssh-commit.d.ts +7 -0
- package/dist/workspace/trust/ssh-commit.d.ts.map +1 -1
- package/dist/workspace/work.d.ts +56 -0
- package/dist/workspace/work.d.ts.map +1 -0
- package/package.json +19 -1
- package/src/cli/main.ts +55 -3
- package/src/cli/registry.ts +13 -1
- package/src/lifecycle/gate-ledger.ts +14 -0
- package/src/workspace/__fixtures__/sessions.ts +66 -0
- package/src/workspace/checks/records.ts +19 -0
- package/src/workspace/checks.test.ts +2 -0
- package/src/workspace/checks.ts +7 -1
- package/src/workspace/composites.schema.json +65 -3
- package/src/workspace/composites.test.ts +95 -5
- package/src/workspace/composites.ts +28 -7
- package/src/workspace/conformance/__fixture__/app/package.json +7 -0
- package/src/workspace/conformance/__fixture__/app/src/server.mjs +29 -0
- package/src/workspace/conformance/__fixture__/decisions/decision.kind.mjs +32 -0
- package/src/workspace/conformance/__fixture__/decisions/decision.schema.json +376 -0
- package/src/workspace/conformance/__fixture__/decisions/fix-001-how-the-app-is-deployed.md +40 -0
- package/src/workspace/conformance/__fixture__/delivery/chant.config.ts +7 -0
- package/src/workspace/conformance/__fixture__/delivery/lexicon/index.ts +26 -0
- package/src/workspace/conformance/__fixture__/delivery/package.json +7 -0
- package/src/workspace/conformance/__fixture__/delivery/src/app.component.ts +14 -0
- package/src/workspace/conformance/__fixture__/delivery/src/app.ts +4 -0
- package/src/workspace/conformance/conformance.test.ts +149 -0
- package/src/workspace/conformance/index.mjs +31 -0
- package/src/workspace/conformance/index.ts +453 -0
- package/src/workspace/conformance/vitest.ts +62 -0
- package/src/workspace/declaration.schema.json +40 -0
- package/src/workspace/declaration.ts +62 -0
- package/src/workspace/declared-kinds.test.ts +321 -0
- package/src/workspace/declared-kinds.ts +76 -0
- package/src/workspace/graph-cli.ts +8 -0
- package/src/workspace/intent-cli.ts +29 -6
- package/src/workspace/intent-gaps.test.ts +217 -0
- package/src/workspace/intent-joins.test.ts +60 -0
- package/src/workspace/intent-joins.ts +71 -19
- package/src/workspace/intent.schema.json +304 -7
- package/src/workspace/intent.test.ts +99 -0
- package/src/workspace/intent.ts +365 -46
- package/src/workspace/ls.schema.json +34 -0
- package/src/workspace/ls.ts +69 -4
- package/src/workspace/read-contract.test.ts +30 -9
- package/src/workspace/reason-codes.test.ts +16 -4
- package/src/workspace/reason-codes.ts +55 -4
- package/src/workspace/record-assets.test.ts +3 -1
- package/src/workspace/record-sessions.ts +105 -0
- package/src/workspace/record-source.ts +14 -5
- package/src/workspace/records-amend.schema.json +167 -0
- package/src/workspace/records-cli.ts +308 -19
- package/src/workspace/records-contract.test.ts +57 -2
- package/src/workspace/records-formats.test.ts +640 -0
- package/src/workspace/records-new.schema.json +158 -0
- package/src/workspace/records-quorum.test.ts +196 -0
- package/src/workspace/records-review.schema.json +227 -0
- package/src/workspace/records-sessions.test.ts +108 -0
- package/src/workspace/records-since.schema.json +193 -0
- package/src/workspace/records-since.test.ts +174 -0
- package/src/workspace/records-since.ts +259 -0
- package/src/workspace/records-write-contract.test.ts +125 -0
- package/src/workspace/records-write.test.ts +373 -0
- package/src/workspace/records-write.ts +765 -0
- package/src/workspace/records.schema.json +202 -9
- package/src/workspace/records.ts +700 -41
- package/src/workspace/runtimes.ts +107 -0
- package/src/workspace/status-contract.test.ts +163 -0
- package/src/workspace/status-gates.ts +215 -0
- package/src/workspace/status.schema.json +69 -3
- package/src/workspace/status.ts +35 -2
- package/src/workspace/trust/seal.test.ts +232 -0
- package/src/workspace/trust/seal.ts +195 -0
- package/src/workspace/trust/ssh-commit.ts +2 -2
- package/src/workspace/work.test.ts +390 -0
- 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,
|
|
12
|
-
*
|
|
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 {
|
|
24
|
-
import {
|
|
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 =
|
|
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
|
-
/**
|
|
50
|
-
|
|
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
|
-
|
|
150
|
-
|
|
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
|
|
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
|
-
|
|
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`,
|