@intentius/chant 0.84.0 → 0.86.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/mcp/resource-handlers.d.ts +2 -1
- package/dist/cli/mcp/resource-handlers.d.ts.map +1 -1
- package/dist/cli/mcp/server.d.ts +1 -0
- package/dist/cli/mcp/server.d.ts.map +1 -1
- package/dist/cli/mcp/tools/composites.d.ts +44 -0
- package/dist/cli/mcp/tools/composites.d.ts.map +1 -0
- package/dist/cli/mcp/tools/search.d.ts.map +1 -1
- package/dist/cli/registry.d.ts +13 -1
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/components/cli-support.d.ts +4 -0
- package/dist/components/cli-support.d.ts.map +1 -1
- package/dist/composite.d.ts +6 -0
- package/dist/composite.d.ts.map +1 -1
- package/dist/lexicon.d.ts +44 -0
- package/dist/lexicon.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__/contract-repo.d.ts +7 -0
- package/dist/workspace/__fixtures__/contract-repo.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 +152 -0
- package/dist/workspace/composites.d.ts.map +1 -0
- 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 +24 -2
- package/dist/workspace/graph-cli.d.ts.map +1 -1
- package/dist/workspace/intent-cli.d.ts +6 -1
- package/dist/workspace/intent-cli.d.ts.map +1 -1
- package/dist/workspace/intent-joins.d.ts +74 -9
- package/dist/workspace/intent-joins.d.ts.map +1 -1
- package/dist/workspace/intent.d.ts +90 -6
- 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/member-commands.d.ts +7 -2
- package/dist/workspace/member-commands.d.ts.map +1 -1
- package/dist/workspace/reason-codes.d.ts +52 -2
- 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 +65 -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 +164 -0
- package/dist/workspace/records-write.d.ts.map +1 -0
- package/dist/workspace/records.d.ts +202 -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/work.d.ts +56 -0
- package/dist/workspace/work.d.ts.map +1 -0
- package/package.json +19 -1
- package/src/cli/handlers/graph.ts +4 -0
- package/src/cli/main.test.ts +9 -0
- package/src/cli/main.ts +56 -3
- package/src/cli/mcp/resource-handlers.ts +17 -0
- package/src/cli/mcp/server.test.ts +140 -4
- package/src/cli/mcp/server.ts +5 -1
- package/src/cli/mcp/tools/composites.ts +98 -0
- package/src/cli/mcp/tools/search.ts +47 -5
- package/src/cli/registry.ts +13 -1
- package/src/components/cli-support.test.ts +16 -0
- package/src/components/cli-support.ts +8 -2
- package/src/composite.ts +9 -0
- package/src/lexicon.ts +47 -0
- package/src/lifecycle/gate-ledger.ts +14 -0
- package/src/workspace/__fixtures__/contract-repo.ts +17 -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 +533 -0
- package/src/workspace/composites.test.ts +334 -0
- package/src/workspace/composites.ts +316 -0
- 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 +364 -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 +40 -4
- package/src/workspace/intent-cli.ts +54 -7
- package/src/workspace/intent-joins.test.ts +60 -0
- package/src/workspace/intent-joins.ts +117 -20
- package/src/workspace/intent.schema.json +357 -19
- package/src/workspace/intent.test.ts +235 -20
- package/src/workspace/intent.ts +396 -51
- package/src/workspace/ls.schema.json +34 -0
- package/src/workspace/ls.ts +69 -4
- package/src/workspace/member-commands.ts +11 -5
- package/src/workspace/read-contract.test.ts +52 -3
- package/src/workspace/reason-codes.test.ts +48 -4
- package/src/workspace/reason-codes.ts +67 -2
- 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 +246 -19
- package/src/workspace/records-contract.test.ts +77 -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 +202 -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 +736 -0
- package/src/workspace/records.schema.json +187 -9
- package/src/workspace/records.test.ts +93 -0
- package/src/workspace/records.ts +683 -49
- 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/work.test.ts +388 -0
- package/src/workspace/work.ts +163 -0
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://intentius.io/chant/schemas/workspace/records-since/v1/records-since.schema.json",
|
|
4
|
+
"title": "chant workspace records --since output",
|
|
5
|
+
"description": "What `chant workspace records --kind <kind file> --since <rev> --json` prints (#2673, #2650 C11): what changed in a kind's records between the commit --since names and the tree --at names, or the working tree without it. Records are matched by id; a record whose id could not be read is not compared. Between a review session's open and close commits, the changes are what the session did. Readers ignore fields they do not know; a field is only ever added within a version. The change kinds and the error codes are closed lists. Contract version 1 is written by chant 0.81.0 and newer, and this document was added to it by #2673; a chant without --since on records refuses the flag. Every code is in the one closed list of `reason-codes.ts`.",
|
|
6
|
+
"oneOf": [{ "$ref": "#/$defs/result" }, { "$ref": "#/$defs/failure" }, { "$ref": "#/$defs/set" }],
|
|
7
|
+
"$defs": {
|
|
8
|
+
"set": {
|
|
9
|
+
"description": "Added in contract 1 by #2680. What records --since prints without --kind when the workspace declaration names record kinds: one result or failure per declared kind, in the declaration's order (the workspace's own kinds, then each member's in member order). Exit code 1 when any kind's read failed; the others are still compared and listed.",
|
|
10
|
+
"type": "object",
|
|
11
|
+
"required": ["$schema", "contract", "kinds"],
|
|
12
|
+
"properties": {
|
|
13
|
+
"$schema": { "const": "https://intentius.io/chant/schemas/workspace/records-since/v1/records-since.schema.json" },
|
|
14
|
+
"contract": { "const": 1 },
|
|
15
|
+
"kinds": {
|
|
16
|
+
"type": "array",
|
|
17
|
+
"items": {
|
|
18
|
+
"allOf": [
|
|
19
|
+
{ "oneOf": [{ "$ref": "#/$defs/result" }, { "$ref": "#/$defs/failure" }] },
|
|
20
|
+
{
|
|
21
|
+
"type": "object",
|
|
22
|
+
"required": ["declared"],
|
|
23
|
+
"properties": {
|
|
24
|
+
"declared": {
|
|
25
|
+
"description": "The declaration's entry for the kind, as in the records set.",
|
|
26
|
+
"type": "object",
|
|
27
|
+
"required": ["member", "path", "name"],
|
|
28
|
+
"properties": {
|
|
29
|
+
"member": { "type": ["string", "null"] },
|
|
30
|
+
"path": { "type": "string" },
|
|
31
|
+
"name": { "type": ["string", "null"] }
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
]
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
"kind": false,
|
|
40
|
+
"changes": false,
|
|
41
|
+
"error": false
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
"result": {
|
|
45
|
+
"description": "Both revisions were read. Exit code 0, whatever changed.",
|
|
46
|
+
"type": "object",
|
|
47
|
+
"required": ["$schema", "contract", "kind", "since", "at", "changes", "summary"],
|
|
48
|
+
"properties": {
|
|
49
|
+
"$schema": { "const": "https://intentius.io/chant/schemas/workspace/records-since/v1/records-since.schema.json" },
|
|
50
|
+
"contract": { "const": 1 },
|
|
51
|
+
"kind": {
|
|
52
|
+
"type": "object",
|
|
53
|
+
"required": ["name", "schema", "file"],
|
|
54
|
+
"properties": {
|
|
55
|
+
"name": { "type": "string", "description": "The kind's name, such as \"decision\" or \"session\"." },
|
|
56
|
+
"schema": { "type": "string", "description": "The `$id` of the kind's record schema." },
|
|
57
|
+
"file": { "type": "string", "description": "The kind file, relative to the repository root, with / separators. It is read from the working tree for both revisions." }
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
"since": { "type": "string", "pattern": "^[0-9a-f]{40,64}$", "description": "The full commit id --since names." },
|
|
61
|
+
"at": { "type": ["string", "null"], "pattern": "^[0-9a-f]{40,64}$", "description": "The full commit id --at names, or null for the working tree." },
|
|
62
|
+
"error": false,
|
|
63
|
+
"changes": {
|
|
64
|
+
"description": "In the order new, removed, state, verdict, supersession, pin, and by id within each.",
|
|
65
|
+
"type": "array",
|
|
66
|
+
"items": { "$ref": "#/$defs/change" }
|
|
67
|
+
},
|
|
68
|
+
"summary": {
|
|
69
|
+
"description": "How many changes of each kind.",
|
|
70
|
+
"type": "object",
|
|
71
|
+
"required": ["new", "removed", "state", "verdict", "supersession", "pin"],
|
|
72
|
+
"properties": {
|
|
73
|
+
"new": { "type": "integer", "minimum": 0 },
|
|
74
|
+
"removed": { "type": "integer", "minimum": 0 },
|
|
75
|
+
"state": { "type": "integer", "minimum": 0 },
|
|
76
|
+
"verdict": { "type": "integer", "minimum": 0 },
|
|
77
|
+
"supersession": { "type": "integer", "minimum": 0 },
|
|
78
|
+
"pin": { "type": "integer", "minimum": 0 }
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
"change": {
|
|
84
|
+
"oneOf": [
|
|
85
|
+
{ "$ref": "#/$defs/new" },
|
|
86
|
+
{ "$ref": "#/$defs/removed" },
|
|
87
|
+
{ "$ref": "#/$defs/state" },
|
|
88
|
+
{ "$ref": "#/$defs/verdict" },
|
|
89
|
+
{ "$ref": "#/$defs/supersession" },
|
|
90
|
+
{ "$ref": "#/$defs/pin" }
|
|
91
|
+
]
|
|
92
|
+
},
|
|
93
|
+
"new": {
|
|
94
|
+
"description": "A record with an id no record had at --since. A directory that did not exist at --since held no records, so every record in it is new.",
|
|
95
|
+
"type": "object",
|
|
96
|
+
"required": ["change", "id", "path", "state"],
|
|
97
|
+
"properties": {
|
|
98
|
+
"change": { "const": "new" },
|
|
99
|
+
"id": { "type": "string" },
|
|
100
|
+
"path": { "type": "string", "description": "From the repository root, with / separators." },
|
|
101
|
+
"state": { "type": ["string", "null"] }
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
"removed": {
|
|
105
|
+
"description": "A record whose id no record has any more.",
|
|
106
|
+
"type": "object",
|
|
107
|
+
"required": ["change", "id", "path", "state"],
|
|
108
|
+
"properties": {
|
|
109
|
+
"change": { "const": "removed" },
|
|
110
|
+
"id": { "type": "string" },
|
|
111
|
+
"path": { "type": "string", "description": "Where it was at --since." },
|
|
112
|
+
"state": { "type": ["string", "null"], "description": "Its state at --since." }
|
|
113
|
+
}
|
|
114
|
+
},
|
|
115
|
+
"state": {
|
|
116
|
+
"description": "A state transition, such as decided to ratified, or a session open to closed.",
|
|
117
|
+
"type": "object",
|
|
118
|
+
"required": ["change", "id", "from", "to"],
|
|
119
|
+
"properties": {
|
|
120
|
+
"change": { "const": "state" },
|
|
121
|
+
"id": { "type": "string" },
|
|
122
|
+
"from": { "type": ["string", "null"] },
|
|
123
|
+
"to": { "type": ["string", "null"] }
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
"verdict": {
|
|
127
|
+
"description": "A verdict the record did not have at --since, including every verdict on a new record. On a session it is an entry of the session's verdict list, with the record it judged in record; on any other record, an entry of its reviews list, with the session it was given in when it names one. Two entries are the same verdict when they name the same principal, verdict, date, session and record, so a dissent that later gains addressed_by is not a new one.",
|
|
128
|
+
"type": "object",
|
|
129
|
+
"required": ["change", "id", "principal", "verdict", "index"],
|
|
130
|
+
"properties": {
|
|
131
|
+
"change": { "const": "verdict" },
|
|
132
|
+
"id": { "type": "string", "description": "The record holding the verdict." },
|
|
133
|
+
"principal": { "type": ["string", "null"], "description": "Who gave it, as written: a review's reviewer, or a session verdict's principal." },
|
|
134
|
+
"verdict": { "type": ["string", "null"], "description": "agree, dissent or abstain, as written." },
|
|
135
|
+
"index": { "type": "integer", "minimum": 0, "description": "Its position in the list, from 0." },
|
|
136
|
+
"record": { "type": "string", "description": "On a session's verdict, the record it judged." },
|
|
137
|
+
"session": { "type": "string", "description": "On a review entry, the session it names." }
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
"supersession": {
|
|
141
|
+
"description": "A supersession that took effect since --since, derived by the same rule as records' supersededBy, so a pending link that is now approved counts.",
|
|
142
|
+
"type": "object",
|
|
143
|
+
"required": ["change", "id", "supersedes"],
|
|
144
|
+
"properties": {
|
|
145
|
+
"change": { "const": "supersession" },
|
|
146
|
+
"id": { "type": "string", "description": "The superseding record." },
|
|
147
|
+
"supersedes": { "type": "string", "description": "The record it now supersedes." }
|
|
148
|
+
}
|
|
149
|
+
},
|
|
150
|
+
"pin": {
|
|
151
|
+
"description": "A pinned file whose pinned sha256 changed on a record that exists at both revisions: from is null for a pin added, and to is null for a pin removed.",
|
|
152
|
+
"type": "object",
|
|
153
|
+
"required": ["change", "id", "path", "from", "to"],
|
|
154
|
+
"properties": {
|
|
155
|
+
"change": { "const": "pin" },
|
|
156
|
+
"id": { "type": "string" },
|
|
157
|
+
"path": { "type": "string", "description": "From the workspace root, as the pin writes it." },
|
|
158
|
+
"from": { "type": ["string", "null"], "pattern": "^[0-9a-f]{64}$" },
|
|
159
|
+
"to": { "type": ["string", "null"], "pattern": "^[0-9a-f]{64}$" }
|
|
160
|
+
}
|
|
161
|
+
},
|
|
162
|
+
"failure": {
|
|
163
|
+
"description": "A revision or the kind could not be read. Exit code 1.",
|
|
164
|
+
"type": "object",
|
|
165
|
+
"required": ["$schema", "contract", "error"],
|
|
166
|
+
"properties": {
|
|
167
|
+
"$schema": { "const": "https://intentius.io/chant/schemas/workspace/records-since/v1/records-since.schema.json" },
|
|
168
|
+
"contract": { "const": 1 },
|
|
169
|
+
"error": {
|
|
170
|
+
"type": "object",
|
|
171
|
+
"required": ["code", "message"],
|
|
172
|
+
"properties": {
|
|
173
|
+
"code": {
|
|
174
|
+
"enum": [
|
|
175
|
+
"kind-unreadable",
|
|
176
|
+
"kind-invalid",
|
|
177
|
+
"schema-unreadable",
|
|
178
|
+
"schema-id-mismatch",
|
|
179
|
+
"schema-invalid",
|
|
180
|
+
"location-missing",
|
|
181
|
+
"not-a-git-repository",
|
|
182
|
+
"revision-unknown",
|
|
183
|
+
"since-rev-unknown"
|
|
184
|
+
]
|
|
185
|
+
},
|
|
186
|
+
"message": { "type": "string" }
|
|
187
|
+
}
|
|
188
|
+
},
|
|
189
|
+
"changes": false
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `chant workspace records --since <rev>` (#2673, #2650 C11), the contract
|
|
3
|
+
* and the comparison. The fixture is a repository with commits for a review
|
|
4
|
+
* session's open and close: between them the session closes with two
|
|
5
|
+
* verdicts, ref-001 gains the two reviews that name it and is ratified,
|
|
6
|
+
* a new decision supersedes ref-002, and ref-002's pin moves. Every document
|
|
7
|
+
* validates against records-since.schema.json.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { spawnSync } from "node:child_process";
|
|
11
|
+
import { cpSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
import { pathToFileURL } from "node:url";
|
|
14
|
+
import { afterAll, describe, expect, test } from "vitest";
|
|
15
|
+
import { cleanScratch, commitAll, contract, git, REPO, scratchDir, validSchema } from "./__fixtures__/contract-repo";
|
|
16
|
+
import { DECISIONS_KIND, reviewed, sessionText, SESSIONS_KIND, sessionsRepo } from "./__fixtures__/sessions";
|
|
17
|
+
import { queryRecordsSince, RECORDS_SINCE_ERROR_CODES, RECORDS_SINCE_OUTPUT_SCHEMA_ID, SINCE_CHANGE_KINDS, type RecordsSinceDocument } from "./records-since";
|
|
18
|
+
import schema from "./records-since.schema.json";
|
|
19
|
+
|
|
20
|
+
afterAll(cleanScratch);
|
|
21
|
+
|
|
22
|
+
const { expectValid } = contract(schema);
|
|
23
|
+
|
|
24
|
+
async function since(q: { kind: string; since: string; at?: string; cwd: string }): Promise<RecordsSinceDocument> {
|
|
25
|
+
const doc = await queryRecordsSince(q);
|
|
26
|
+
expectValid(doc);
|
|
27
|
+
return doc;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function ok(doc: RecordsSinceDocument): Extract<RecordsSinceDocument, { changes: unknown }> {
|
|
31
|
+
if ("error" in doc) throw new Error(`${doc.error.code}: ${doc.error.message}`);
|
|
32
|
+
return doc;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** The session fixture: returns the repository and its commits. */
|
|
36
|
+
function sessionHistory(): { root: string; before: string; open: string; close: string } {
|
|
37
|
+
const root = sessionsRepo();
|
|
38
|
+
const before = git(root, "rev-parse", "HEAD");
|
|
39
|
+
writeFileSync(join(root, "design", "sessions", "S-0002-walk.md"), sessionText({ id: "S-0002", state: "open", agenda: ["ref-001", "ref-002"] }));
|
|
40
|
+
const open = commitAll(root, "open S-0002");
|
|
41
|
+
|
|
42
|
+
reviewed(root, ["alice", "bob"], "S-0002", "ratified");
|
|
43
|
+
writeFileSync(
|
|
44
|
+
join(root, "design", "sessions", "S-0002-walk.md"),
|
|
45
|
+
sessionText({
|
|
46
|
+
id: "S-0002",
|
|
47
|
+
state: "closed",
|
|
48
|
+
agenda: ["ref-001", "ref-002"],
|
|
49
|
+
verdicts: [
|
|
50
|
+
{ record: "ref-001", principal: "alice", verdict: "agree" },
|
|
51
|
+
{ record: "ref-001", principal: "bob", verdict: "agree" },
|
|
52
|
+
],
|
|
53
|
+
}),
|
|
54
|
+
);
|
|
55
|
+
const ref002 = join(root, "decisions", "ref-002-where-the-screen-design-lives.md");
|
|
56
|
+
const text = readFileSync(ref002, "utf-8");
|
|
57
|
+
writeFileSync(ref002, text.replace(/sha256: "[0-9a-f]{64}"/, `sha256: "${"a".repeat(64)}"`));
|
|
58
|
+
writeFileSync(
|
|
59
|
+
join(root, "decisions", "ref-003-a-successor.md"),
|
|
60
|
+
text.replace(/^id: .*$/m, 'id: "ref-003"').replace(/^supersedes: \[\]$/m, 'supersedes:\n - decision: "ref-002"'),
|
|
61
|
+
);
|
|
62
|
+
const close = commitAll(root, "close S-0002");
|
|
63
|
+
return { root, before, open, close };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
describe("records-since output schema", () => {
|
|
67
|
+
test("is a valid draft 2020-12 document with the published $id", () => {
|
|
68
|
+
expect(validSchema(schema)).toBe(true);
|
|
69
|
+
expect(schema.$id).toBe(RECORDS_SINCE_OUTPUT_SCHEMA_ID);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("lists exactly the error codes and change kinds the code can return", () => {
|
|
73
|
+
expect(schema.$defs.failure.properties.error.properties.code.enum).toEqual([...RECORDS_SINCE_ERROR_CODES]);
|
|
74
|
+
expect(schema.$defs.change.oneOf.map((r) => (schema.$defs as unknown as Record<string, { properties: { change: { const: string } } }>)[r.$ref.replace("#/$defs/", "")].properties.change.const)).toEqual([...SINCE_CHANGE_KINDS]);
|
|
75
|
+
expect(Object.keys(schema.$defs.result.properties.summary.properties)).toEqual([...SINCE_CHANGE_KINDS]);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe("records --since (#2673)", () => {
|
|
80
|
+
const h = sessionHistory();
|
|
81
|
+
|
|
82
|
+
test("between a session's open and close commits, the decisions show every verdict, state change, supersession and pin", async () => {
|
|
83
|
+
const doc = ok(await since({ kind: DECISIONS_KIND, since: h.open, at: h.close, cwd: h.root }));
|
|
84
|
+
expect(doc.since).toBe(h.open);
|
|
85
|
+
expect(doc.at).toBe(h.close);
|
|
86
|
+
expect(doc.kind).toEqual({ name: "decision", schema: "urn:intentius:chant:decision:1", file: "decisions/decision.kind.mjs" });
|
|
87
|
+
expect(doc.changes).toEqual([
|
|
88
|
+
{ change: "new", id: "ref-003", path: "decisions/ref-003-a-successor.md", state: "decided" },
|
|
89
|
+
{ change: "state", id: "ref-001", from: "decided", to: "ratified" },
|
|
90
|
+
{ change: "verdict", id: "ref-001", principal: "alice", verdict: "agree", index: 0, session: "S-0002" },
|
|
91
|
+
{ change: "verdict", id: "ref-001", principal: "bob", verdict: "agree", index: 1, session: "S-0002" },
|
|
92
|
+
{ change: "supersession", id: "ref-003", supersedes: "ref-002" },
|
|
93
|
+
{ change: "pin", id: "ref-002", path: "design/screens/home.json", from: expect.stringMatching(/^[0-9a-f]{64}$/), to: "a".repeat(64) },
|
|
94
|
+
]);
|
|
95
|
+
expect(doc.summary).toEqual({ new: 1, removed: 0, state: 1, verdict: 2, supersession: 1, pin: 1 });
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("the session kind shows the session closing and the verdicts it produced", async () => {
|
|
99
|
+
const doc = ok(await since({ kind: SESSIONS_KIND, since: h.open, at: h.close, cwd: h.root }));
|
|
100
|
+
expect(doc.changes).toEqual([
|
|
101
|
+
{ change: "state", id: "S-0002", from: "open", to: "closed" },
|
|
102
|
+
{ change: "verdict", id: "S-0002", principal: "alice", verdict: "agree", index: 0, record: "ref-001" },
|
|
103
|
+
{ change: "verdict", id: "S-0002", principal: "bob", verdict: "agree", index: 1, record: "ref-001" },
|
|
104
|
+
]);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("without --at it compares with the working tree, and a record deleted there is removed", async () => {
|
|
108
|
+
rmSync(join(h.root, "decisions", "ref-003-a-successor.md"));
|
|
109
|
+
try {
|
|
110
|
+
const doc = ok(await since({ kind: DECISIONS_KIND, since: h.close, cwd: h.root }));
|
|
111
|
+
expect(doc.at).toBeNull();
|
|
112
|
+
expect(doc.changes).toEqual([{ change: "removed", id: "ref-003", path: "decisions/ref-003-a-successor.md", state: "decided" }]);
|
|
113
|
+
} finally {
|
|
114
|
+
git(h.root, "checkout", "--", "decisions");
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test("a directory that did not exist at --since held no records, so every record in it is new", async () => {
|
|
119
|
+
const root = scratchDir("chant-since-empty-");
|
|
120
|
+
git(root, "init", "-q");
|
|
121
|
+
writeFileSync(join(root, "README.md"), "empty\n");
|
|
122
|
+
const first = commitAll(root, "empty");
|
|
123
|
+
const copy = sessionsRepo();
|
|
124
|
+
for (const d of ["decisions", "design"]) cpSync(join(copy, d), join(root, d), { recursive: true });
|
|
125
|
+
const doc = ok(await since({ kind: SESSIONS_KIND, since: first, cwd: root }));
|
|
126
|
+
expect(doc.changes).toEqual([{ change: "new", id: "S-0001", path: "design/sessions/S-0001-first-walk-of-the-reference-decisions.md", state: "closed" }]);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test("nothing changed is an empty list", async () => {
|
|
130
|
+
const doc = ok(await since({ kind: DECISIONS_KIND, since: h.close, at: "HEAD", cwd: h.root }));
|
|
131
|
+
expect(doc.changes).toEqual([]);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
test("a --since that names no commit is since-rev-unknown, and a bad --at is revision-unknown", async () => {
|
|
135
|
+
const docs = [
|
|
136
|
+
await since({ kind: DECISIONS_KIND, since: "no-such-rev", cwd: h.root }),
|
|
137
|
+
await since({ kind: DECISIONS_KIND, since: "--all", cwd: h.root }),
|
|
138
|
+
await since({ kind: DECISIONS_KIND, since: h.open, at: "no-such-rev", cwd: h.root }),
|
|
139
|
+
await since({ kind: "missing.kind.mjs", since: h.open, cwd: h.root }),
|
|
140
|
+
];
|
|
141
|
+
expect(docs.map((d) => ("error" in d ? d.error.code : "ok"))).toEqual(["since-rev-unknown", "since-rev-unknown", "revision-unknown", "kind-unreadable"]);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test("outside a git repository it is not-a-git-repository", async () => {
|
|
145
|
+
const root = scratchDir("chant-since-nogit-");
|
|
146
|
+
const doc = await since({ kind: DECISIONS_KIND, since: "HEAD", cwd: root });
|
|
147
|
+
expect("error" in doc && doc.error.code).toBe("not-a-git-repository");
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test(
|
|
151
|
+
"through the CLI: --json prints the document, the text form a line per change, and a bad --since exits 1",
|
|
152
|
+
() => {
|
|
153
|
+
const run = (...args: string[]) =>
|
|
154
|
+
spawnSync(process.execPath, ["--import", pathToFileURL(join(REPO, "node_modules/tsx/dist/loader.mjs")).href, join(REPO, "packages/core/src/cli/main.ts"), "workspace", "records", ...args], {
|
|
155
|
+
cwd: h.root,
|
|
156
|
+
encoding: "utf-8",
|
|
157
|
+
env: { ...process.env, NO_COLOR: "1" },
|
|
158
|
+
timeout: 60_000,
|
|
159
|
+
});
|
|
160
|
+
const json = run("--kind", SESSIONS_KIND, "--since", h.open, "--at", h.close, "--json");
|
|
161
|
+
expect(json.status, json.stderr).toBe(0);
|
|
162
|
+
expectValid(JSON.parse(json.stdout));
|
|
163
|
+
const text = run("--kind", SESSIONS_KIND, "--since", h.open, "--at", h.close);
|
|
164
|
+
expect(text.stdout).toContain("S-0002 state open to closed");
|
|
165
|
+
expect(text.stdout).toContain("S-0002 verdict agree by alice on ref-001");
|
|
166
|
+
const bad = run("--kind", SESSIONS_KIND, "--since", "nope", "--json");
|
|
167
|
+
expect(bad.status).toBe(1);
|
|
168
|
+
expect(JSON.parse(bad.stdout).error.code).toBe("since-rev-unknown");
|
|
169
|
+
const mixed = run("--kind", SESSIONS_KIND, "--since", h.open, "--current");
|
|
170
|
+
expect(mixed.status).toBe(1);
|
|
171
|
+
},
|
|
172
|
+
120_000,
|
|
173
|
+
);
|
|
174
|
+
});
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `chant workspace records --kind <kind file> --since <rev> [--at <rev>]
|
|
3
|
+
* [--json]` (#2673, #2650 C11): what changed in a kind's records between two
|
|
4
|
+
* revisions. It reads the records at `<rev>` and at `--at` (the working tree
|
|
5
|
+
* without it), through the same reader as `records`, and lists new and
|
|
6
|
+
* removed records, state transitions, new verdicts, new supersessions and
|
|
7
|
+
* changed pins. Between a review session's open and close commits, that is
|
|
8
|
+
* what the session did.
|
|
9
|
+
*
|
|
10
|
+
* Records are matched by id. A record whose id could not be read is left
|
|
11
|
+
* out of the comparison, since nothing says which record it was before.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { realpathSync } from "node:fs";
|
|
15
|
+
import { relative } from "node:path";
|
|
16
|
+
import { pinEntries } from "./record-assets";
|
|
17
|
+
import { gitRoot, resolveRevision } from "./record-source";
|
|
18
|
+
import { READ_ERROR_CODES, RecordReadError, type LoadedRecordKind, type ReadErrorCode, type RecordEntry, type RecordKind } from "./records";
|
|
19
|
+
import { readRecordsFor, RECORDS_CONTRACT_VERSION } from "./records-cli";
|
|
20
|
+
import type { ReasonCode } from "./reason-codes";
|
|
21
|
+
|
|
22
|
+
/** `$id` of the JSON Schema for `records --since --json`, shipped beside this file. */
|
|
23
|
+
export const RECORDS_SINCE_OUTPUT_SCHEMA_ID = "https://intentius.io/chant/schemas/workspace/records-since/v1/records-since.schema.json";
|
|
24
|
+
|
|
25
|
+
/** Why a `--since` read failed: any records read error, or a `--since` that names no commit. Closed. */
|
|
26
|
+
export const RECORDS_SINCE_ERROR_CODES = [
|
|
27
|
+
...READ_ERROR_CODES,
|
|
28
|
+
/** `--since` names no commit. */
|
|
29
|
+
"since-rev-unknown",
|
|
30
|
+
] as const satisfies readonly ReasonCode[];
|
|
31
|
+
export type RecordsSinceErrorCode = (typeof RECORDS_SINCE_ERROR_CODES)[number];
|
|
32
|
+
|
|
33
|
+
/** The kinds of change, in the order the output lists them. */
|
|
34
|
+
export const SINCE_CHANGE_KINDS = ["new", "removed", "state", "verdict", "supersession", "pin"] as const;
|
|
35
|
+
export type SinceChangeKind = (typeof SINCE_CHANGE_KINDS)[number];
|
|
36
|
+
|
|
37
|
+
/** The review list on a record that is not a session, when its kind declares none (#2670, #2671). */
|
|
38
|
+
const REVIEWS_FIELD = "reviews";
|
|
39
|
+
|
|
40
|
+
export type SinceChange =
|
|
41
|
+
| { change: "new"; id: string; path: string; state: string | null }
|
|
42
|
+
| { change: "removed"; id: string; path: string; state: string | null }
|
|
43
|
+
| { change: "state"; id: string; from: string | null; to: string | null }
|
|
44
|
+
| { change: "verdict"; id: string; principal: string | null; verdict: string | null; index: number; record?: string; session?: string }
|
|
45
|
+
| { change: "supersession"; id: string; supersedes: string }
|
|
46
|
+
| { change: "pin"; id: string; path: string; from: string | null; to: string | null };
|
|
47
|
+
|
|
48
|
+
export type RecordsSinceDocument =
|
|
49
|
+
| {
|
|
50
|
+
$schema: string;
|
|
51
|
+
contract: number;
|
|
52
|
+
kind: { name: string; schema: string; file: string };
|
|
53
|
+
since: string;
|
|
54
|
+
at: string | null;
|
|
55
|
+
changes: SinceChange[];
|
|
56
|
+
summary: Record<SinceChangeKind, number>;
|
|
57
|
+
}
|
|
58
|
+
| { $schema: string; contract: number; error: { code: RecordsSinceErrorCode; message: string } };
|
|
59
|
+
|
|
60
|
+
export interface RecordsSinceQuery {
|
|
61
|
+
kind: string;
|
|
62
|
+
since: string;
|
|
63
|
+
at?: string;
|
|
64
|
+
cwd: string;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
class SinceError extends Error {
|
|
68
|
+
constructor(
|
|
69
|
+
readonly code: RecordsSinceErrorCode,
|
|
70
|
+
message: string,
|
|
71
|
+
) {
|
|
72
|
+
super(message);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Run the comparison and build the document `--json` prints. Never throws a read error. */
|
|
77
|
+
export async function queryRecordsSince(query: RecordsSinceQuery): Promise<RecordsSinceDocument> {
|
|
78
|
+
const failure = (code: RecordsSinceErrorCode, message: string): RecordsSinceDocument => ({
|
|
79
|
+
$schema: RECORDS_SINCE_OUTPUT_SCHEMA_ID,
|
|
80
|
+
contract: RECORDS_CONTRACT_VERSION,
|
|
81
|
+
error: { code, message },
|
|
82
|
+
});
|
|
83
|
+
try {
|
|
84
|
+
const since = resolveSince(query.cwd, query.since);
|
|
85
|
+
const after = await readRecordsFor({ kind: query.kind, at: query.at, cwd: query.cwd });
|
|
86
|
+
let before: RecordEntry[];
|
|
87
|
+
try {
|
|
88
|
+
before = (await readRecordsFor({ kind: query.kind, at: since, cwd: query.cwd })).result.records;
|
|
89
|
+
} catch (err) {
|
|
90
|
+
// A directory that did not exist yet held no records.
|
|
91
|
+
if (!(err instanceof RecordReadError) || err.code !== "location-missing") throw err;
|
|
92
|
+
before = [];
|
|
93
|
+
}
|
|
94
|
+
const changes = compareRecords(after.loaded.kind, before, after.result.records);
|
|
95
|
+
const summary = Object.fromEntries(SINCE_CHANGE_KINDS.map((k) => [k, changes.filter((c) => c.change === k).length])) as Record<SinceChangeKind, number>;
|
|
96
|
+
return {
|
|
97
|
+
$schema: RECORDS_SINCE_OUTPUT_SCHEMA_ID,
|
|
98
|
+
contract: RECORDS_CONTRACT_VERSION,
|
|
99
|
+
kind: kindView(after.loaded, after.root),
|
|
100
|
+
since,
|
|
101
|
+
at: after.at,
|
|
102
|
+
changes,
|
|
103
|
+
summary,
|
|
104
|
+
};
|
|
105
|
+
} catch (err) {
|
|
106
|
+
if (err instanceof SinceError) return failure(err.code, err.message);
|
|
107
|
+
if (err instanceof RecordReadError) return failure(err.code satisfies ReadErrorCode, err.message);
|
|
108
|
+
throw err;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function kindView(loaded: LoadedRecordKind, root: string): { name: string; schema: string; file: string } {
|
|
113
|
+
return { name: loaded.kind.name, schema: loaded.kind.schema.id, file: relative(root, loaded.file).split("\\").join("/") };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** The full commit id `rev` names, from the repository holding `cwd`. */
|
|
117
|
+
function resolveSince(cwd: string, rev: string): string {
|
|
118
|
+
let dir = cwd;
|
|
119
|
+
try {
|
|
120
|
+
dir = realpathSync(cwd);
|
|
121
|
+
} catch {
|
|
122
|
+
// Reported by the read that follows, if at all.
|
|
123
|
+
}
|
|
124
|
+
const top = gitRoot(dir);
|
|
125
|
+
if (!top) throw new RecordReadError("not-a-git-repository", "--since reads git objects, and this directory is not in a git repository");
|
|
126
|
+
try {
|
|
127
|
+
return resolveRevision(top, rev);
|
|
128
|
+
} catch (err) {
|
|
129
|
+
if (err instanceof RecordReadError && err.code === "revision-unknown") {
|
|
130
|
+
throw new SinceError("since-rev-unknown", `--since ${rev} names no commit in this repository`);
|
|
131
|
+
}
|
|
132
|
+
throw err;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** The first record with each id, in path order; records with no id are left out. */
|
|
137
|
+
function byId(records: RecordEntry[]): Map<string, RecordEntry> {
|
|
138
|
+
const out = new Map<string, RecordEntry>();
|
|
139
|
+
for (const r of records) if (r.id !== null && !out.has(r.id)) out.set(r.id, r);
|
|
140
|
+
return out;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
interface Verdict {
|
|
144
|
+
key: string;
|
|
145
|
+
index: number;
|
|
146
|
+
principal: string | null;
|
|
147
|
+
verdict: string | null;
|
|
148
|
+
record?: string;
|
|
149
|
+
session?: string;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* A record's verdicts: a session's own verdict list, or the review list on
|
|
154
|
+
* any other record. Two entries are the same verdict when they name the same
|
|
155
|
+
* principal, verdict, date, session and record, so a dissent that later gains
|
|
156
|
+
* `addressed_by` is not a new verdict.
|
|
157
|
+
*/
|
|
158
|
+
function verdicts(kind: RecordKind, r: RecordEntry): Verdict[] {
|
|
159
|
+
const field = kind.session ? kind.session.verdicts : (kind.reviews?.field ?? REVIEWS_FIELD);
|
|
160
|
+
const list = r.data?.[field];
|
|
161
|
+
if (!Array.isArray(list)) return [];
|
|
162
|
+
const str = (v: unknown): string | undefined => (typeof v === "string" ? v : undefined);
|
|
163
|
+
const out: Verdict[] = [];
|
|
164
|
+
list.forEach((e, index) => {
|
|
165
|
+
if (e === null || typeof e !== "object" || Array.isArray(e)) return;
|
|
166
|
+
const o = e as Record<string, unknown>;
|
|
167
|
+
const principal = str(kind.session ? o.principal : o.reviewer) ?? null;
|
|
168
|
+
const verdict = str(o.verdict) ?? null;
|
|
169
|
+
const record = str(o.record);
|
|
170
|
+
const session = str(o.session);
|
|
171
|
+
out.push({
|
|
172
|
+
key: JSON.stringify([principal, verdict, str(o.on) ?? null, session ?? null, record ?? null]),
|
|
173
|
+
index,
|
|
174
|
+
principal,
|
|
175
|
+
verdict,
|
|
176
|
+
...(record !== undefined ? { record } : {}),
|
|
177
|
+
...(session !== undefined ? { session } : {}),
|
|
178
|
+
});
|
|
179
|
+
});
|
|
180
|
+
return out;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/** What changed from `before` to `after`, in the order of {@link SINCE_CHANGE_KINDS} and then by id. */
|
|
184
|
+
export function compareRecords(kind: RecordKind, before: RecordEntry[], after: RecordEntry[]): SinceChange[] {
|
|
185
|
+
const old = byId(before);
|
|
186
|
+
const now = byId(after);
|
|
187
|
+
const ids = [...new Set([...old.keys(), ...now.keys()])].sort();
|
|
188
|
+
const out: Record<SinceChangeKind, SinceChange[]> = { new: [], removed: [], state: [], verdict: [], supersession: [], pin: [] };
|
|
189
|
+
for (const id of ids) {
|
|
190
|
+
const a = old.get(id);
|
|
191
|
+
const b = now.get(id);
|
|
192
|
+
if (!a && b) out.new.push({ change: "new", id, path: b.path, state: b.state });
|
|
193
|
+
if (a && !b) out.removed.push({ change: "removed", id, path: a.path, state: a.state });
|
|
194
|
+
if (!b) continue;
|
|
195
|
+
if (a && a.state !== b.state) out.state.push({ change: "state", id, from: a.state, to: b.state });
|
|
196
|
+
|
|
197
|
+
// Verdicts in b beyond the ones a already had, compared as multisets.
|
|
198
|
+
const seen = new Map<string, number>();
|
|
199
|
+
for (const v of a ? verdicts(kind, a) : []) seen.set(v.key, (seen.get(v.key) ?? 0) + 1);
|
|
200
|
+
for (const v of verdicts(kind, b)) {
|
|
201
|
+
const left = seen.get(v.key) ?? 0;
|
|
202
|
+
if (left > 0) {
|
|
203
|
+
seen.set(v.key, left - 1);
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
out.verdict.push({
|
|
207
|
+
change: "verdict",
|
|
208
|
+
id,
|
|
209
|
+
principal: v.principal,
|
|
210
|
+
verdict: v.verdict,
|
|
211
|
+
index: v.index,
|
|
212
|
+
...(v.record !== undefined ? { record: v.record } : {}),
|
|
213
|
+
...(v.session !== undefined ? { session: v.session } : {}),
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// Supersession as derived, so a link that only now takes effect counts.
|
|
218
|
+
if (b.supersededBy !== null && (!a || a.supersededBy !== b.supersededBy)) {
|
|
219
|
+
out.supersession.push({ change: "supersession", id: b.supersededBy, supersedes: id });
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (a && kind.pins) {
|
|
223
|
+
const from = new Map(pinEntries(a.data, kind.pins.field).map((p) => [p.path, p.sha256]));
|
|
224
|
+
const to = new Map(pinEntries(b.data, kind.pins.field).map((p) => [p.path, p.sha256]));
|
|
225
|
+
for (const path of [...new Set([...from.keys(), ...to.keys()])].sort()) {
|
|
226
|
+
const x = from.get(path) ?? null;
|
|
227
|
+
const y = to.get(path) ?? null;
|
|
228
|
+
if (x !== y) out.pin.push({ change: "pin", id, path, from: x, to: y });
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
out.supersession.sort((x, y) => (x.id < y.id ? -1 : x.id > y.id ? 1 : 0));
|
|
233
|
+
return SINCE_CHANGE_KINDS.flatMap((k) => out[k]);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** The text form: one line per change and a summary. */
|
|
237
|
+
export function formatSince(doc: Extract<RecordsSinceDocument, { changes: SinceChange[] }>): string {
|
|
238
|
+
const lines = doc.changes.map((c) => {
|
|
239
|
+
switch (c.change) {
|
|
240
|
+
case "new":
|
|
241
|
+
return `${c.id} new, ${c.state ?? "no state"} (${c.path})`;
|
|
242
|
+
case "removed":
|
|
243
|
+
return `${c.id} removed, was ${c.state ?? "no state"} (${c.path})`;
|
|
244
|
+
case "state":
|
|
245
|
+
return `${c.id} state ${c.from ?? "none"} to ${c.to ?? "none"}`;
|
|
246
|
+
case "verdict":
|
|
247
|
+
return `${c.id} verdict ${c.verdict ?? "-"} by ${c.principal ?? "-"}${c.record ? ` on ${c.record}` : ""}${c.session ? ` in ${c.session}` : ""}`;
|
|
248
|
+
case "supersession":
|
|
249
|
+
return `${c.id} supersedes ${c.supersedes}`;
|
|
250
|
+
case "pin":
|
|
251
|
+
return `${c.id} pin ${c.path} ${c.from ? c.from.slice(0, 12) : "none"} to ${c.to ? c.to.slice(0, 12) : "none"}`;
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
const s = doc.summary;
|
|
255
|
+
lines.push(
|
|
256
|
+
`${doc.changes.length} changes since ${doc.since.slice(0, 8)}${doc.at ? ` to ${doc.at.slice(0, 8)}` : " to the working tree"}: ${s.new} new, ${s.removed} removed, ${s.state} state, ${s.verdict} verdicts, ${s.supersession} supersessions, ${s.pin} pins`,
|
|
257
|
+
);
|
|
258
|
+
return lines.join("\n");
|
|
259
|
+
}
|