@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
package/src/workspace/ls.ts
CHANGED
|
@@ -8,6 +8,11 @@
|
|
|
8
8
|
*
|
|
9
9
|
* The `--json` output is part of the read contract, versioned like
|
|
10
10
|
* `workspace records` and described by `ls.schema.json` beside this file.
|
|
11
|
+
*
|
|
12
|
+
* The record kinds the declaration names are listed too, the workspace's own
|
|
13
|
+
* and each member's, with each kind file's own name when it loads (#2680).
|
|
14
|
+
* Loading one imports it, so {@link listWorkspace} lists them unloaded and
|
|
15
|
+
* {@link listWorkspaceWithKinds}, which the command runs, loads them.
|
|
11
16
|
*/
|
|
12
17
|
|
|
13
18
|
import { existsSync } from "node:fs";
|
|
@@ -28,6 +33,7 @@ import type { ReasonCode } from "./reason-codes";
|
|
|
28
33
|
import { loadKindRegistry, probeKind, type KindRegistry } from "./kinds";
|
|
29
34
|
import type { WorkspaceTree } from "./tree";
|
|
30
35
|
import { handToRootChant, locateWorkspace } from "./which-chant";
|
|
36
|
+
import type { DeclaredKindReasonCode } from "./declared-kinds";
|
|
31
37
|
|
|
32
38
|
/** The version of the `ls` output this chant writes. */
|
|
33
39
|
export const LS_CONTRACT_VERSION = 1;
|
|
@@ -58,6 +64,18 @@ export const GROUP_REASON_CODES = [
|
|
|
58
64
|
] as const satisfies readonly ReasonCode[];
|
|
59
65
|
export type GroupReasonCode = (typeof GROUP_REASON_CODES)[number];
|
|
60
66
|
|
|
67
|
+
/** A record kind the declaration names (#2680). */
|
|
68
|
+
export interface LsRecordKind {
|
|
69
|
+
/** The name the declaration gives it, else the kind file's own, else null when the file wasn't loaded. */
|
|
70
|
+
name: string | null;
|
|
71
|
+
/** The kind file from the workspace root. */
|
|
72
|
+
path: string;
|
|
73
|
+
/** The kind file's `recordKind.name`, or null when it wasn't loaded. */
|
|
74
|
+
kind: string | null;
|
|
75
|
+
/** Why the kind file can't be loaded, or null. */
|
|
76
|
+
reason: { code: DeclaredKindReasonCode; message: string } | null;
|
|
77
|
+
}
|
|
78
|
+
|
|
61
79
|
export interface LsMember {
|
|
62
80
|
name: string;
|
|
63
81
|
dir: string;
|
|
@@ -67,6 +85,8 @@ export interface LsMember {
|
|
|
67
85
|
because: string | null;
|
|
68
86
|
readable: boolean;
|
|
69
87
|
reason: { code: MemberReasonCode; message: string } | null;
|
|
88
|
+
/** The record kinds the member declares, in file order (#2680). */
|
|
89
|
+
records: LsRecordKind[];
|
|
70
90
|
}
|
|
71
91
|
|
|
72
92
|
export interface LsGroup {
|
|
@@ -92,6 +112,8 @@ export type LsDocument =
|
|
|
92
112
|
schema: number;
|
|
93
113
|
minReader: string | null;
|
|
94
114
|
pins: Declaration["pins"];
|
|
115
|
+
/** The workspace's own record kinds, from the top-level `records` (#2680). */
|
|
116
|
+
records: LsRecordKind[];
|
|
95
117
|
};
|
|
96
118
|
members: LsMember[];
|
|
97
119
|
groups: LsGroup[];
|
|
@@ -126,8 +148,36 @@ export function memberReason(member: Member, tree: WorkspaceTree, kinds: KindReg
|
|
|
126
148
|
return null;
|
|
127
149
|
}
|
|
128
150
|
|
|
129
|
-
/**
|
|
151
|
+
/**
|
|
152
|
+
* Find the workspace, read it and build the document `--json` prints, with
|
|
153
|
+
* the declared record kinds unloaded: `kind` null and no reason. Never throws
|
|
154
|
+
* a {@link WorkspaceReadError}.
|
|
155
|
+
*/
|
|
130
156
|
export function listWorkspace(query: LsQuery): LsDocument {
|
|
157
|
+
return readListing(query).doc;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** {@link listWorkspace}, with each declared record kind loaded for its name, or the reason it can't be (#2680). */
|
|
161
|
+
export async function listWorkspaceWithKinds(query: LsQuery): Promise<LsDocument> {
|
|
162
|
+
const { doc, declaration, tree, rootOnDisk } = readListing(query);
|
|
163
|
+
if (!declaration || "error" in doc) return doc;
|
|
164
|
+
const { loadDeclaredKinds } = await import("./declared-kinds");
|
|
165
|
+
const loaded = await loadDeclaredKinds(declaration, tree!, rootOnDisk!);
|
|
166
|
+
const byPath = new Map(loaded.map((k) => [k.declared.path, k]));
|
|
167
|
+
const fill = (r: LsRecordKind): LsRecordKind => {
|
|
168
|
+
const k = byPath.get(r.path)!;
|
|
169
|
+
return { ...r, name: r.name ?? k.kind, kind: k.kind, reason: k.reason };
|
|
170
|
+
};
|
|
171
|
+
return {
|
|
172
|
+
...doc,
|
|
173
|
+
workspace: { ...doc.workspace, records: doc.workspace.records.map(fill) },
|
|
174
|
+
members: doc.members.map((m) => ({ ...m, records: m.records.map(fill) })),
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const unloaded = (records: Declaration["records"]): LsRecordKind[] => records.map((r) => ({ name: r.name, path: r.path, kind: null, reason: null }));
|
|
179
|
+
|
|
180
|
+
function readListing(query: LsQuery): { doc: LsDocument; declaration?: Declaration; tree?: WorkspaceTree; rootOnDisk?: string } {
|
|
131
181
|
const chant = readerVersion();
|
|
132
182
|
const head = { $schema: LS_OUTPUT_SCHEMA_ID, contract: LS_CONTRACT_VERSION, chant };
|
|
133
183
|
try {
|
|
@@ -149,6 +199,7 @@ export function listWorkspace(query: LsQuery): LsDocument {
|
|
|
149
199
|
because: m.because,
|
|
150
200
|
readable: reason === null,
|
|
151
201
|
reason,
|
|
202
|
+
records: unloaded(m.records),
|
|
152
203
|
};
|
|
153
204
|
});
|
|
154
205
|
const lsGroups: LsGroup[] = groups.map((g) => ({
|
|
@@ -162,7 +213,7 @@ export function listWorkspace(query: LsQuery): LsDocument {
|
|
|
162
213
|
? { code: "no-matches", message: `no directory ${g.group.globs.join(" or ")} matches${tree.label} holds a chant project` }
|
|
163
214
|
: null,
|
|
164
215
|
}));
|
|
165
|
-
|
|
216
|
+
const doc: LsDocument = {
|
|
166
217
|
...head,
|
|
167
218
|
at,
|
|
168
219
|
workspace: {
|
|
@@ -172,6 +223,7 @@ export function listWorkspace(query: LsQuery): LsDocument {
|
|
|
172
223
|
schema: declaration.schema,
|
|
173
224
|
minReader: declaration.minReader,
|
|
174
225
|
pins: declaration.pins,
|
|
226
|
+
records: unloaded(declaration.records),
|
|
175
227
|
},
|
|
176
228
|
members,
|
|
177
229
|
groups: lsGroups,
|
|
@@ -182,9 +234,10 @@ export function listWorkspace(query: LsQuery): LsDocument {
|
|
|
182
234
|
matches: lsGroups.reduce((n, g) => n + g.matches.length, 0),
|
|
183
235
|
},
|
|
184
236
|
};
|
|
237
|
+
return { doc, declaration, tree, rootOnDisk };
|
|
185
238
|
} catch (err) {
|
|
186
239
|
if (!(err instanceof WorkspaceReadError)) throw err;
|
|
187
|
-
return { ...head, error: { code: err.code, message: err.message, location: err.location ?? null } };
|
|
240
|
+
return { doc: { ...head, error: { code: err.code, message: err.message, location: err.location ?? null } } };
|
|
188
241
|
}
|
|
189
242
|
}
|
|
190
243
|
|
|
@@ -198,7 +251,7 @@ export async function runWorkspaceLs(ctx: CommandContext): Promise<number> {
|
|
|
198
251
|
// The root's chant reads the declaration (ws-021).
|
|
199
252
|
const handed = await handToRootChant(cwd, args.at);
|
|
200
253
|
if (handed !== undefined) return handed;
|
|
201
|
-
const doc =
|
|
254
|
+
const doc = await listWorkspaceWithKinds({ cwd, at: args.at });
|
|
202
255
|
if (args.json) {
|
|
203
256
|
console.log(JSON.stringify(doc, null, 2));
|
|
204
257
|
} else if ("error" in doc) {
|
|
@@ -233,6 +286,18 @@ function formatLs(doc: Extract<LsDocument, { members: unknown }>): string {
|
|
|
233
286
|
if (m.reason) lines.push(` ${m.reason.code}: ${m.reason.message}`);
|
|
234
287
|
});
|
|
235
288
|
}
|
|
289
|
+
const kinds = [...w.records.map((r) => ({ owner: "(workspace)", r })), ...doc.members.flatMap((m) => m.records.map((r) => ({ owner: m.name, r })))];
|
|
290
|
+
if (kinds.length > 0) {
|
|
291
|
+
lines.push("");
|
|
292
|
+
const rows = [["RECORDS", "KIND", "MEMBER", "FILE"]];
|
|
293
|
+
for (const { owner, r } of kinds) rows.push([r.name ?? "-", r.kind ?? "-", owner, r.path]);
|
|
294
|
+
const rendered = table(rows);
|
|
295
|
+
lines.push(rendered[0]);
|
|
296
|
+
kinds.forEach(({ r }, i) => {
|
|
297
|
+
lines.push(rendered[i + 1]);
|
|
298
|
+
if (r.reason) lines.push(` ${r.reason.code}: ${r.reason.message}`);
|
|
299
|
+
});
|
|
300
|
+
}
|
|
236
301
|
if (doc.groups.length > 0) {
|
|
237
302
|
lines.push("");
|
|
238
303
|
const rows = [["GROUP", "KIND", "GLOB", "PROJECTS"]];
|
|
@@ -6,9 +6,9 @@
|
|
|
6
6
|
* `chant` member (`delivery`), three `other` members and decision records.
|
|
7
7
|
* Each read-contract command reads it here, and its output must validate
|
|
8
8
|
* against the command's schema: `ls`, `graph` (running delivery's real
|
|
9
|
-
* `chant graph`), `graph --composites`, `check`, `status
|
|
10
|
-
* the working tree and, for `ls`, `graph` and `check`,
|
|
11
|
-
* `--at`.
|
|
9
|
+
* `chant graph`), `graph --composites`, `check`, `status`, `records` and
|
|
10
|
+
* `records --since`, in the working tree and, for `ls`, `graph` and `check`,
|
|
11
|
+
* at `HEAD` through `--at`.
|
|
12
12
|
*
|
|
13
13
|
* The schemas themselves are checked here too: each is a draft 2020-12
|
|
14
14
|
* document under `https://intentius.io/chant/schemas/workspace/<command>/v1/`,
|
|
@@ -34,13 +34,15 @@ import { readerBin, type Toolchain } from "./member-commands";
|
|
|
34
34
|
import { READ_CONTRACT_FLOOR, READ_CONTRACT_VERSION } from "./reason-codes";
|
|
35
35
|
import { queryRecords } from "./records-cli";
|
|
36
36
|
import recordsSchema from "./records.schema.json";
|
|
37
|
+
import { queryRecordsSince } from "./records-since";
|
|
38
|
+
import recordsSinceSchema from "./records-since.schema.json";
|
|
37
39
|
import { workspaceStatus } from "./status";
|
|
38
40
|
import statusSchema from "./status.schema.json";
|
|
39
41
|
|
|
40
42
|
const FIXTURE = join(REPO, "reference-workspace");
|
|
41
43
|
const TIMEOUT = 240_000;
|
|
42
44
|
|
|
43
|
-
const SCHEMAS = { ls: lsSchema, graph: graphSchema, check: checkSchema, status: statusSchema, records: recordsSchema, intent: intentSchema, composites: compositesSchema };
|
|
45
|
+
const SCHEMAS = { ls: lsSchema, graph: graphSchema, check: checkSchema, status: statusSchema, records: recordsSchema, "records-since": recordsSinceSchema, intent: intentSchema, composites: compositesSchema };
|
|
44
46
|
|
|
45
47
|
/** This checkout's chant, started the way the CLI starts it, for members with no toolchain of their own. */
|
|
46
48
|
const reader: Toolchain = {
|
|
@@ -135,7 +137,7 @@ describe("every schema against the reference workspace (#2543)", () => {
|
|
|
135
137
|
});
|
|
136
138
|
|
|
137
139
|
test(
|
|
138
|
-
"graph --composites runs delivery's own component graph, and
|
|
140
|
+
"graph --composites runs delivery's own component graph, and lists its app with the component that deploys it (#2662)",
|
|
139
141
|
async () => {
|
|
140
142
|
const { expectValid } = contract(compositesSchema);
|
|
141
143
|
for (const at of [undefined, "HEAD"]) {
|
|
@@ -150,10 +152,15 @@ describe("every schema against the reference workspace (#2543)", () => {
|
|
|
150
152
|
["design-client", "skipped"],
|
|
151
153
|
["design", "skipped"],
|
|
152
154
|
]);
|
|
153
|
-
// delivery
|
|
154
|
-
expect(doc.composites).
|
|
155
|
-
|
|
156
|
-
|
|
155
|
+
// delivery declares the app as a DockerWebService, and its app component names that kind.
|
|
156
|
+
expect(doc.composites.map((c) => [c.id, c.kinds, c.components.map((m) => [m.component, m.by, m.via])])).toEqual([
|
|
157
|
+
["delivery/app", ["DockerWebService"], [["delivery/app", "composites", "member"]]],
|
|
158
|
+
]);
|
|
159
|
+
expect(doc.components.map((c) => [c.id, c.archetype])).toEqual([["delivery/app", "service"]]);
|
|
160
|
+
// #2674: the reference config's lexicons host no component runs, so local is the only runtime, read at HEAD too.
|
|
161
|
+
expect(doc.components[0].runtimes).toEqual([{ name: "local", lexicon: null, default: true, command: "chant run --components app" }]);
|
|
162
|
+
expect(doc.members.find((m) => m.name === "delivery")!.runtimeReasons).toEqual([]);
|
|
163
|
+
expect(doc.reasons).toEqual([]);
|
|
157
164
|
}
|
|
158
165
|
},
|
|
159
166
|
TIMEOUT,
|
|
@@ -168,5 +175,19 @@ describe("every schema against the reference workspace (#2543)", () => {
|
|
|
168
175
|
expect(doc.summary.invalid).toBe(0);
|
|
169
176
|
expect(doc.records.map((r) => r.id)).toContain("ref-001");
|
|
170
177
|
}
|
|
178
|
+
const sessions = await queryRecords({ kind: "design/sessions/session.kind.mjs", cwd: FIXTURE });
|
|
179
|
+
expectValid(sessions);
|
|
180
|
+
if ("error" in sessions) throw new Error(sessions.error.message);
|
|
181
|
+
expect(sessions.summary.invalid).toBe(0);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
test("records --since HEAD, for decisions and sessions (#2673)", async () => {
|
|
185
|
+
const { expectValid } = contract(recordsSinceSchema);
|
|
186
|
+
for (const kind of ["decisions/decision.kind.mjs", "design/sessions/session.kind.mjs"]) {
|
|
187
|
+
const doc = await queryRecordsSince({ kind, since: "HEAD", cwd: FIXTURE });
|
|
188
|
+
expectValid(doc);
|
|
189
|
+
if ("error" in doc) throw new Error(doc.error.message);
|
|
190
|
+
expect(doc.since).toBe(head);
|
|
191
|
+
}
|
|
171
192
|
});
|
|
172
193
|
});
|
|
@@ -10,7 +10,7 @@ import { join } from "node:path";
|
|
|
10
10
|
import { describe, expect, test } from "vitest";
|
|
11
11
|
import { REPO } from "./__fixtures__/contract-repo";
|
|
12
12
|
import { MEMBER_RUN_REASON_CODES } from "./compose-graph";
|
|
13
|
-
import { COMPOSITES_ERROR_CODES, COMPOSITES_REASON_CODES } from "./composites";
|
|
13
|
+
import { COMPOSITES_ERROR_CODES, COMPOSITES_REASON_CODES, COMPOSITES_RUNTIME_REASON_CODES } from "./composites";
|
|
14
14
|
import { WORKSPACE_ERROR_CODES } from "./declaration";
|
|
15
15
|
import { GRAPH_ERROR_CODES } from "./graph-cli";
|
|
16
16
|
import { INTENT_ERROR_CODES, INTENT_FINDING_CODES, INTENT_REASON_CODES } from "./intent";
|
|
@@ -19,8 +19,11 @@ import { GROUP_REASON_CODES, MEMBER_REASON_CODES } from "./ls";
|
|
|
19
19
|
import intentSchema from "./intent.schema.json";
|
|
20
20
|
import { isPluginCode, isReasonCode, REASON_CODES, REASONS } from "./reason-codes";
|
|
21
21
|
import { contract } from "./__fixtures__/contract-repo";
|
|
22
|
-
import { READ_ERROR_CODES, RECORD_REASON_CODES, RECORD_WARNING_CODES } from "./records";
|
|
23
|
-
import {
|
|
22
|
+
import { READ_ERROR_CODES, RECORD_REASON_CODES, RECORD_WARNING_CODES, REVIEW_REASON_CODES, SEAL_REASON_CODES } from "./records";
|
|
23
|
+
import { AMEND_ERROR_CODES, NEW_ERROR_CODES, REVIEW_ERROR_CODES } from "./records-write";
|
|
24
|
+
import { RECORDS_SINCE_ERROR_CODES } from "./records-since";
|
|
25
|
+
import { STATUS_ERROR_CODES, STATUS_GATE_REASON_CODES, STATUS_REASON_CODES } from "./status";
|
|
26
|
+
import { WORK_WARNING_CODES } from "./work";
|
|
24
27
|
|
|
25
28
|
const HERE = import.meta.dirname;
|
|
26
29
|
|
|
@@ -34,14 +37,23 @@ const PER_COMMAND: Record<string, readonly string[]> = {
|
|
|
34
37
|
CHECK_ERROR_CODES,
|
|
35
38
|
STATUS_REASON_CODES,
|
|
36
39
|
STATUS_ERROR_CODES,
|
|
40
|
+
STATUS_GATE_REASON_CODES,
|
|
37
41
|
RECORD_REASON_CODES,
|
|
38
42
|
RECORD_WARNING_CODES,
|
|
43
|
+
REVIEW_REASON_CODES,
|
|
44
|
+
SEAL_REASON_CODES,
|
|
45
|
+
WORK_WARNING_CODES,
|
|
39
46
|
READ_ERROR_CODES,
|
|
47
|
+
NEW_ERROR_CODES,
|
|
48
|
+
AMEND_ERROR_CODES,
|
|
49
|
+
REVIEW_ERROR_CODES,
|
|
50
|
+
RECORDS_SINCE_ERROR_CODES,
|
|
40
51
|
INTENT_ERROR_CODES,
|
|
41
52
|
INTENT_FINDING_CODES,
|
|
42
53
|
INTENT_REASON_CODES,
|
|
43
54
|
COMPOSITES_ERROR_CODES,
|
|
44
55
|
COMPOSITES_REASON_CODES,
|
|
56
|
+
COMPOSITES_RUNTIME_REASON_CODES,
|
|
45
57
|
};
|
|
46
58
|
|
|
47
59
|
/** Every string in an `enum` under a property named `code`, anywhere in a schema. */
|
|
@@ -103,7 +115,7 @@ describe("the closed list of reason codes", () => {
|
|
|
103
115
|
});
|
|
104
116
|
|
|
105
117
|
test("no source file emits a code outside the list", () => {
|
|
106
|
-
const emitted = /(?:\bcode:\s*|(?:WorkspaceReadError|RecordReadError|StatusError|IntentError)\(\s*)"([a-z0-9-]+)"/g;
|
|
118
|
+
const emitted = /(?:\bcode:\s*|(?:WorkspaceReadError|RecordReadError|RecordWriteError|StatusError|IntentError)\(\s*)"([a-z0-9-]+)"/g;
|
|
107
119
|
let seen = 0;
|
|
108
120
|
for (const file of sourceFiles(HERE)) {
|
|
109
121
|
const text = readFileSync(file, "utf-8");
|
|
@@ -53,18 +53,45 @@ export const REASONS = {
|
|
|
53
53
|
// A ledger status can't fully read.
|
|
54
54
|
"ledger-unreadable": "Reading the ledger failed, so nothing from it is listed.",
|
|
55
55
|
"ledger-malformed": "Some lines of the ledger aren't release records; the rest are listed.",
|
|
56
|
+
// A member's gates status can't list (status, #2674).
|
|
57
|
+
"gates-no-ledger": "The checkout has no chant/lifecycle branch, so there is no gate ledger to read.",
|
|
58
|
+
"gates-no-gate-ledger": "The branch has no gate ledger for the member: no run of it has reached a gate.",
|
|
59
|
+
"gates-ledger-unreadable": "Reading the member's gate ledger failed, so no gate is listed.",
|
|
56
60
|
// A record that isn't valid (records).
|
|
57
|
-
"record-unparseable": "No front matter, a YAML error
|
|
58
|
-
"record-schema-invalid": "The front matter does not match the kind's schema.",
|
|
61
|
+
"record-unparseable": "No front matter, a YAML error or a value outside the JSON subset of YAML, or for a JSON kind a file that is not one object or repeats a member name.",
|
|
62
|
+
"record-schema-invalid": "The record's front matter, or its JSON object, does not match the kind's schema.",
|
|
59
63
|
"record-id-duplicate": "Another record earlier in path order has the same id.",
|
|
60
64
|
"record-supersedes-unknown": "A supersedes link names an id no record has.",
|
|
61
65
|
"record-supersedes-conflict": "A second closed record supersedes a record another one already superseded.",
|
|
66
|
+
// A review session that isn't valid (records, #2673).
|
|
67
|
+
"session-seal-mismatch": "A closed session's seal is not the digest of its text: the session changed after it closed.",
|
|
68
|
+
"session-verdict-unknown-record": "A session's verdict names a record that none of the session kind's subject records has.",
|
|
62
69
|
// A record that is valid but warned about (records, #2549).
|
|
63
70
|
"asset-drift": "A file the record pins by hash has changed: its bytes no longer hash to the pinned sha256.",
|
|
64
71
|
"asset-missing": "A file the record pins by hash does not exist in the tree read.",
|
|
65
72
|
"asset-stale": "A file the record pins is unchanged at the hash a record it supersedes pinned: the decision changed and the artifact did not follow.",
|
|
66
73
|
"record-supersedes-pending": "A supersedes link from a record whose state is weaker than the record it names, so the link has no effect yet.",
|
|
67
74
|
"record-no-evidence": "The record's evidence list is empty: it cites nothing and pins no file. Information for a reviewer, never an error.",
|
|
75
|
+
"review-undigested": "A verdict names no digest of the text it judged. It still counts, and an amendment does not stop it counting.",
|
|
76
|
+
// A work record that is valid but warned about (records and graph --intent, #2683).
|
|
77
|
+
"work-needs-unknown": "A work record's needs list names a work id no record has, so the item stays blocked.",
|
|
78
|
+
"work-implements-unknown": "A work record's implements list names a decision id no decision has.",
|
|
79
|
+
"work-needs-cycle": "A work record needs itself through its needs links, so it can never be ready.",
|
|
80
|
+
"work-implements-undecided": "A work record implements a decision whose state is not approved, such as proposed.",
|
|
81
|
+
"work-done-unpinned": "A work record is done and its evidence list is empty: nothing shows the work was done.",
|
|
82
|
+
"work-closed-without-date": "A work record is done or dropped and has no closing date.",
|
|
83
|
+
"work-done-gap-open": "A work record is done, and the finding it came from still fires on its region. graph --intent raises it, and records does by walking that region.",
|
|
84
|
+
// A verdict the quorum does not count (records, #2671, #2672).
|
|
85
|
+
"review-decider": "The verdict is the decider's own, and the quorum counts verdicts besides the decider's.",
|
|
86
|
+
"review-agent": "The reviewer holds the agent role in the trust policy at base.",
|
|
87
|
+
"review-duplicate": "A later verdict by the same principal replaces this one. Names are compared after NFKC, trimming and lower-casing.",
|
|
88
|
+
"review-older-digest": "The verdict names a digest other than the record's text now: the record changed after the verdict.",
|
|
89
|
+
"review-unattested": "An attestation policy is active at base, and the verdict carries no seal that verifies for its reviewer.",
|
|
90
|
+
// A verdict's seal that is not attested (records, #2687). Every verdict reports one of these in its attestation, unless its seal verified.
|
|
91
|
+
"seal-missing": "The verdict carries no seal.",
|
|
92
|
+
"seal-signer-unlisted": "The reviewer has no key in the signers file at base, so the seal can't count.",
|
|
93
|
+
"seal-signature-invalid": "The seal is malformed, names a signer other than the reviewer, or its signature does not verify over the verdict.",
|
|
94
|
+
"seal-unverifiable": "Nothing here can say whose seal it is: there is no signers file at base, or ssh-keygen is not installed.",
|
|
68
95
|
// A records read that fails (records).
|
|
69
96
|
"kind-unreadable": "The record kind file is missing or could not be imported.",
|
|
70
97
|
"kind-invalid": "The record kind file exports no recordKind, or its shape is wrong.",
|
|
@@ -72,6 +99,21 @@ export const REASONS = {
|
|
|
72
99
|
"schema-id-mismatch": "The schema's $id differs from the id the record kind names.",
|
|
73
100
|
"schema-invalid": "The record schema itself does not compile.",
|
|
74
101
|
"location-missing": "The records directory does not exist, in the tree or at the revision.",
|
|
102
|
+
// A records write that is refused (records new, amend and review, #2670). Nothing is written.
|
|
103
|
+
"write-usage-invalid": "The command line lacks a value the write needs, or gives one it does not take.",
|
|
104
|
+
"write-input-invalid": "The fields given with --from or --set can't be read, are not JSON, or are not a JSON object.",
|
|
105
|
+
"record-not-found": "No record of the kind has the id given.",
|
|
106
|
+
"record-id-taken": "The id given for a new record is already used, by a record or a file name.",
|
|
107
|
+
"record-id-unallocatable": "No id was given and none can be allocated: the records share no single prefix and --prefix names none.",
|
|
108
|
+
"record-path-unmatched": "The file name made from the record's id and title does not match the kind's location.",
|
|
109
|
+
"record-closed": "The record is in a closed state, so nothing in it changes; a new record supersedes it instead.",
|
|
110
|
+
"amend-id-immutable": "An amendment changes the record's id, and ids are never renumbered.",
|
|
111
|
+
"amend-supersede-instead": "The record is approved, and the amendment changes a field the approval rule does not let change in place; a new record supersedes it instead.",
|
|
112
|
+
"review-unsupported": "The kind's schema has no reviews field, so its records take no review.",
|
|
113
|
+
"review-note-required": "A dissent was given with no note: a dissent needs a reason.",
|
|
114
|
+
"review-sign-failed": "--sign was given and no seal could be made: the key can't be read or used, git names no ssh signing key, or ssh-keygen is not installed.",
|
|
115
|
+
// records --since that fails (#2673).
|
|
116
|
+
"since-rev-unknown": "--since names no commit.",
|
|
75
117
|
// The intent graph (graph --intent, #2651): a read that fails.
|
|
76
118
|
"intent-region-invalid": "The region's path, or its line range, does not exist in the tree read.",
|
|
77
119
|
// The intent graph: part of the walk that can't be read. The document is still printed.
|
|
@@ -82,18 +124,26 @@ export const REASONS = {
|
|
|
82
124
|
"intent-commit-bare": "A commit names no unit, no pull request and no decision covering the region at its time.",
|
|
83
125
|
"intent-pin-drifted": "A decision's pinned artifact no longer hashes to the pin.",
|
|
84
126
|
"intent-pin-missing": "A decision's pinned artifact does not exist in the tree read.",
|
|
127
|
+
"intent-pin-stale": "A current decision pins an artifact at the hash a record it supersedes pinned, and the artifact has not changed since: the decision moved on and the artifact did not.",
|
|
85
128
|
"intent-artifact-unpinned": "An artifact decisions in the graph pinned, which no current decision pins.",
|
|
86
129
|
"intent-decision-superseded-live": "Every decision constraining the region is superseded.",
|
|
87
130
|
"intent-decision-provisional": "The current decisions constraining the region are all in states their kind does not close, such as decided.",
|
|
131
|
+
"intent-decision-contested": "A current decision constraining the region has an open concern: a dissent neither addressed nor withdrawn.",
|
|
88
132
|
"intent-constraint-coarse": "The region is constrained only through its member, not by path.",
|
|
89
133
|
"intent-constraint-lost": "A decision's path constraint names a path that does not exist in the tree read.",
|
|
90
134
|
"intent-evidence-unpinned": "A decision's evidence has no hash: a URL, or a path with no sha256.",
|
|
91
135
|
"intent-trailer-unverified": "A commit carries a trailer a plugin says claims authorship, and the commit is not attested.",
|
|
92
136
|
"intent-region-unconstrained": "No decision constrains the region at any granularity.",
|
|
137
|
+
"intent-decision-unimplemented": "A decided decision constrains the region, no work item that is not dropped implements it, and no commit falls in its window.",
|
|
138
|
+
"intent-work-blocked": "A work item constraining the region has commits in its window while a work item it needs is not done.",
|
|
139
|
+
"intent-work-open-decided-code": "Commits in the region are a decision's own work while the work item implementing that decision is still open.",
|
|
93
140
|
// Composite instances joined to components (graph --composites, #2662): why the list is empty or has no component.
|
|
94
141
|
"composites-no-chant-member": "No member of kind chant was read, so nothing declares a composite instance or a component.",
|
|
95
142
|
"composites-none-declared": "The members read declare no composite instance.",
|
|
96
143
|
"composites-no-component": "The members read declare no component, so no composite instance has one.",
|
|
144
|
+
// The runtimes a member's components can deploy on (graph --composites, #2674).
|
|
145
|
+
"runtimes-config-unreadable": "The member's chant.config.ts could not be read, so only the built-in local runtime is listed.",
|
|
146
|
+
"runtimes-lexicon-unreadable": "A lexicon the member's config lists could not be loaded, so it is not listed as a runtime.",
|
|
97
147
|
// The lineage lock (check).
|
|
98
148
|
"lock-invalid": "The lineage lock can't be read.",
|
|
99
149
|
"manual-step-open": "A scope in the lineage lock has an open manual step.",
|
|
@@ -111,14 +161,15 @@ export function isReasonCode(value: unknown): value is ReasonCode {
|
|
|
111
161
|
/**
|
|
112
162
|
* A finding code a plugin contributes to the intent graph through its
|
|
113
163
|
* `commitJoins` (#2656): `plugin:<name>:<code>`, where `<name>` is the kind's
|
|
114
|
-
* name
|
|
164
|
+
* name, or the kind file's `commitJoinsName` when it has one (#2663), and
|
|
165
|
+
* `<code>` is lower case words joined by dashes. These are outside the
|
|
115
166
|
* closed list: the plugin owns its namespace, and core only carries them.
|
|
116
167
|
*/
|
|
117
168
|
export type PluginCode = `plugin:${string}:${string}`;
|
|
118
169
|
|
|
119
170
|
export const PLUGIN_CODE = /^plugin:([^:\s]+):([a-z0-9]+(?:-[a-z0-9]+)*)$/;
|
|
120
171
|
|
|
121
|
-
/** Whether `value` is a plugin code, and, given `name
|
|
172
|
+
/** Whether `value` is a plugin code, and, given `name` (a kind's name or its `commitJoinsName`), one in that namespace. */
|
|
122
173
|
export function isPluginCode(value: unknown, name?: string): value is PluginCode {
|
|
123
174
|
if (typeof value !== "string") return false;
|
|
124
175
|
const m = value.match(PLUGIN_CODE);
|
|
@@ -17,6 +17,7 @@ import { runChecks } from "./lineage-check";
|
|
|
17
17
|
import { constraintCovers, isWorkspacePath, memberHolding, WORKSPACE_PATH_PATTERN } from "./record-assets";
|
|
18
18
|
import { pinFile, queryRecords, type RecordsDocument } from "./records-cli";
|
|
19
19
|
import { parseFrontMatter, RECORD_WARNING_CODES } from "./records";
|
|
20
|
+
import { WORK_WARNING_CODES } from "./work";
|
|
20
21
|
import { workspaceGraph } from "./graph-cli";
|
|
21
22
|
import graphSchema from "./graph.schema.json";
|
|
22
23
|
import recordsSchema from "./records.schema.json";
|
|
@@ -168,7 +169,8 @@ describe("chant workspace records checks each pin", () => {
|
|
|
168
169
|
});
|
|
169
170
|
|
|
170
171
|
test("the schema lists exactly the warning codes", () => {
|
|
171
|
-
|
|
172
|
+
// A work kind's records carry the work warnings too (#2683), work-done-gap-open included since records walks a done item's region (#2686).
|
|
173
|
+
expect(recordsSchema.$defs.warning.properties.code.enum).toEqual([...RECORD_WARNING_CODES, ...WORK_WARNING_CODES]);
|
|
172
174
|
});
|
|
173
175
|
|
|
174
176
|
test("records pin <path> prints the entry's path from the workspace root and the file's hash", () => {
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Review sessions (#2673, #2650 C10): a record kind whose records are the
|
|
3
|
+
* sessions a group reviewed records in. A session kind names, in its
|
|
4
|
+
* `session` declaration, the list of verdicts a session produced, the field
|
|
5
|
+
* that seals it once closed, and the kind file of the records its verdicts
|
|
6
|
+
* name (the subjects, such as decisions).
|
|
7
|
+
*
|
|
8
|
+
* On read, a closed session's seal has to match its text
|
|
9
|
+
* (`session-seal-mismatch`), and each verdict has to name a subject record
|
|
10
|
+
* that exists (`session-verdict-unknown-record`). Each session also lists the
|
|
11
|
+
* subjects' review entries that name it in their `session` field, as
|
|
12
|
+
* `citedBy`. This module only reads; nothing here writes a seal into a file.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { recordTextDigest, type RecordEntry, type RecordFormat, type RecordKind } from "./records";
|
|
16
|
+
|
|
17
|
+
/** A subject record's review entry that names a session in its `session` field. */
|
|
18
|
+
export interface SessionCitation {
|
|
19
|
+
/** The subject record's id. */
|
|
20
|
+
id: string;
|
|
21
|
+
/** The subject record's path, from the repository root. */
|
|
22
|
+
path: string;
|
|
23
|
+
/** The entry's position in the subject's reviews list, from 0. */
|
|
24
|
+
index: number;
|
|
25
|
+
/** The reviewer as the entry writes it, or null when it names none. */
|
|
26
|
+
reviewer: string | null;
|
|
27
|
+
/** The verdict as the entry writes it, or null when it names none. */
|
|
28
|
+
verdict: string | null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The seal of a session record: {@link recordTextDigest} with the seal field
|
|
33
|
+
* in place of the reviews list. That is the lowercase hex SHA-256 of the
|
|
34
|
+
* file's text with LF line endings and without the front-matter line that
|
|
35
|
+
* holds the seal, a quoted string on one line. Every other byte, the `---`
|
|
36
|
+
* lines and the body included, is kept.
|
|
37
|
+
*
|
|
38
|
+
* `awk 'NR==1&&/^---$/{f=1;print;next} f&&/^---$/{f=0} f&&/^closed_digest:/{next} {print}' S-0001.md | shasum -a 256`
|
|
39
|
+
* gives the same value for a file with LF line endings that ends in one.
|
|
40
|
+
* For a session kind of format json (ws-053), the seal member is removed by
|
|
41
|
+
* the JSON rule of {@link recordTextDigest} instead.
|
|
42
|
+
*/
|
|
43
|
+
export function sessionSeal(text: string, field: string, format: RecordFormat = "markdown-front-matter"): string {
|
|
44
|
+
return recordTextDigest(text, field, format);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Check each session in `entries` against its seal and the subject records,
|
|
49
|
+
* and fill in `citedBy`. `texts` holds each entry's file text by path.
|
|
50
|
+
* `subjects` holds the subject records and the name of their reviews list;
|
|
51
|
+
* it is null when they were not read, and then no verdict is checked.
|
|
52
|
+
*/
|
|
53
|
+
export function joinSessions(kind: RecordKind, entries: RecordEntry[], texts: Map<string, string>, subjects: { records: RecordEntry[]; reviews: string } | null): void {
|
|
54
|
+
const decl = kind.session;
|
|
55
|
+
if (!decl) return;
|
|
56
|
+
const closed = new Set(kind.closedStates);
|
|
57
|
+
const subjectIds = new Set(subjects?.records.map((s) => s.id).filter((id): id is string => id !== null) ?? []);
|
|
58
|
+
const citations = new Map<string, SessionCitation[]>();
|
|
59
|
+
for (const s of subjects?.records ?? []) {
|
|
60
|
+
if (s.id === null) continue;
|
|
61
|
+
const reviews = s.data?.[subjects!.reviews];
|
|
62
|
+
if (!Array.isArray(reviews)) continue;
|
|
63
|
+
reviews.forEach((r, index) => {
|
|
64
|
+
if (r === null || typeof r !== "object") return;
|
|
65
|
+
const entry = r as Record<string, unknown>;
|
|
66
|
+
if (typeof entry.session !== "string") return;
|
|
67
|
+
const list = citations.get(entry.session) ?? [];
|
|
68
|
+
list.push({
|
|
69
|
+
id: s.id!,
|
|
70
|
+
path: s.path,
|
|
71
|
+
index,
|
|
72
|
+
reviewer: typeof entry.reviewer === "string" ? entry.reviewer : null,
|
|
73
|
+
verdict: typeof entry.verdict === "string" ? entry.verdict : null,
|
|
74
|
+
});
|
|
75
|
+
citations.set(entry.session, list);
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
for (const e of entries) {
|
|
79
|
+
e.citedBy = e.id === null ? [] : (citations.get(e.id) ?? []);
|
|
80
|
+
if (e.data === null) continue;
|
|
81
|
+
if (e.state !== null && closed.has(e.state)) {
|
|
82
|
+
const seal = e.data[decl.seal];
|
|
83
|
+
const text = texts.get(e.path);
|
|
84
|
+
if (typeof seal === "string" && text !== undefined) {
|
|
85
|
+
const actual = sessionSeal(text, decl.seal, kind.format);
|
|
86
|
+
if (actual !== seal) {
|
|
87
|
+
e.reasons.push({
|
|
88
|
+
code: "session-seal-mismatch",
|
|
89
|
+
message: `${decl.seal} is ${seal}, but the text hashes to ${actual}: the session changed after it closed`,
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
if (subjects === null) continue;
|
|
95
|
+
const verdicts = e.data[decl.verdicts];
|
|
96
|
+
if (!Array.isArray(verdicts)) continue;
|
|
97
|
+
verdicts.forEach((v, i) => {
|
|
98
|
+
if (v === null || typeof v !== "object") return;
|
|
99
|
+
const record = (v as Record<string, unknown>).record;
|
|
100
|
+
if (typeof record === "string" && !subjectIds.has(record)) {
|
|
101
|
+
e.reasons.push({ code: "session-verdict-unknown-record", message: `${decl.verdicts}[${i}] names ${record}, which no ${decl.subjects.kind} record has` });
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -16,6 +16,8 @@ export interface RecordSource {
|
|
|
16
16
|
list(dir: string): string[] | undefined;
|
|
17
17
|
/** The text of a file listed by `list`. */
|
|
18
18
|
read(path: string): string;
|
|
19
|
+
/** The bytes of a file listed by `list`, for a content-addressed id (ws-053). */
|
|
20
|
+
bytes(path: string): Uint8Array;
|
|
19
21
|
}
|
|
20
22
|
|
|
21
23
|
/** Records in the working tree under `root`. */
|
|
@@ -34,6 +36,9 @@ export function workingTreeSource(root: string): RecordSource {
|
|
|
34
36
|
read(path) {
|
|
35
37
|
return readFileSync(join(root, path), "utf-8");
|
|
36
38
|
},
|
|
39
|
+
bytes(path) {
|
|
40
|
+
return readFileSync(join(root, path));
|
|
41
|
+
},
|
|
37
42
|
};
|
|
38
43
|
}
|
|
39
44
|
|
|
@@ -68,7 +73,12 @@ export function resolveRevision(root: string, rev: string): string {
|
|
|
68
73
|
|
|
69
74
|
/** Records as they were at `commit`, read from the object store of the repository at `root`. */
|
|
70
75
|
export function gitRevisionSource(root: string, commit: string): RecordSource {
|
|
71
|
-
const blobs = new Map<string,
|
|
76
|
+
const blobs = new Map<string, Buffer>();
|
|
77
|
+
const blob = (path: string): Buffer => {
|
|
78
|
+
const bytes = blobs.get(path);
|
|
79
|
+
if (bytes === undefined) throw new Error(`${path} was not listed at ${commit}`);
|
|
80
|
+
return bytes;
|
|
81
|
+
};
|
|
72
82
|
return {
|
|
73
83
|
label: ` at ${commit.slice(0, 8)}`,
|
|
74
84
|
list(dir) {
|
|
@@ -102,16 +112,15 @@ export function gitRevisionSource(root: string, commit: string): RecordSource {
|
|
|
102
112
|
const nl = out.indexOf(0x0a, at);
|
|
103
113
|
const size = Number(out.subarray(at, nl).toString("utf-8").split(" ")[2]);
|
|
104
114
|
const start = nl + 1;
|
|
105
|
-
blobs.set(dir === "." ? e.name : `${dir}/${e.name}`, out.subarray(start, start + size)
|
|
115
|
+
blobs.set(dir === "." ? e.name : `${dir}/${e.name}`, out.subarray(start, start + size));
|
|
106
116
|
at = start + size + 1;
|
|
107
117
|
}
|
|
108
118
|
}
|
|
109
119
|
return entries.map((e) => e.name);
|
|
110
120
|
},
|
|
111
121
|
read(path) {
|
|
112
|
-
|
|
113
|
-
if (text === undefined) throw new Error(`${path} was not listed at ${commit}`);
|
|
114
|
-
return text;
|
|
122
|
+
return blob(path).toString("utf-8");
|
|
115
123
|
},
|
|
124
|
+
bytes: blob,
|
|
116
125
|
};
|
|
117
126
|
}
|