@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,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Review sessions as a record kind (#2673, #2650 C10), read through the
|
|
3
|
+
* reference workspace's session kind: a closed session's seal is checked on
|
|
4
|
+
* read, each verdict names a decision that exists, and each session lists the
|
|
5
|
+
* decision reviews that cite it.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { execFileSync } from "node:child_process";
|
|
9
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
import { afterAll, describe, expect, test } from "vitest";
|
|
12
|
+
import { cleanScratch, commitAll, contract, REPO } from "./__fixtures__/contract-repo";
|
|
13
|
+
import { reviewed, sessionText, SESSIONS_KIND as SESSIONS, sessionsRepo as fixture } from "./__fixtures__/sessions";
|
|
14
|
+
import { sessionSeal } from "./record-sessions";
|
|
15
|
+
import { queryRecords, type RecordsDocument } from "./records-cli";
|
|
16
|
+
import recordsSchema from "./records.schema.json";
|
|
17
|
+
|
|
18
|
+
afterAll(cleanScratch);
|
|
19
|
+
|
|
20
|
+
const { expectValid } = contract(recordsSchema);
|
|
21
|
+
|
|
22
|
+
async function read(root: string, at?: string): Promise<Extract<RecordsDocument, { records: unknown }>> {
|
|
23
|
+
const doc = await queryRecords({ kind: SESSIONS, cwd: root, ...(at ? { at } : {}) });
|
|
24
|
+
expectValid(doc);
|
|
25
|
+
if ("error" in doc) throw new Error(`${doc.error.code}: ${doc.error.message}`);
|
|
26
|
+
return doc;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe("the session kind (#2673)", () => {
|
|
30
|
+
test("the reference workspace's session is closed, sealed and valid, and cited by nothing", async () => {
|
|
31
|
+
const doc = await read(join(REPO, "reference-workspace"));
|
|
32
|
+
expect(doc.kind.name).toBe("session");
|
|
33
|
+
expect(doc.records.map((r) => [r.id, r.state, r.valid])).toEqual([["S-0001", "closed", true]]);
|
|
34
|
+
expect(doc.records[0].citedBy).toEqual([]);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("the seal is the awk recipe the docs give", () => {
|
|
38
|
+
const file = join(REPO, "reference-workspace", "design", "sessions", "S-0001-first-walk-of-the-reference-decisions.md");
|
|
39
|
+
const awk = execFileSync("sh", ["-c", `awk 'NR==1&&/^---$/{f=1;print;next} f&&/^---$/{f=0} f&&/^closed_digest:/{next} {print}' "$1" | shasum -a 256`, "sh", file], { encoding: "utf-8" });
|
|
40
|
+
const text = readFileSync(file, "utf-8");
|
|
41
|
+
expect(awk.split(" ")[0]).toBe(sessionSeal(text, "closed_digest"));
|
|
42
|
+
expect(text).toContain(`closed_digest: "${sessionSeal(text, "closed_digest")}"`);
|
|
43
|
+
// CRLF line endings hash as LF.
|
|
44
|
+
expect(sessionSeal(text.replace(/\n/g, "\r\n"), "closed_digest")).toBe(sessionSeal(text, "closed_digest"));
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("a closed session edited after it closed is session-seal-mismatch", async () => {
|
|
48
|
+
const root = fixture();
|
|
49
|
+
const file = join(root, "design", "sessions", "S-0001-first-walk-of-the-reference-decisions.md");
|
|
50
|
+
writeFileSync(file, readFileSync(file, "utf-8").replace("First walk", "Second walk"));
|
|
51
|
+
const s1 = (await read(root)).records[0];
|
|
52
|
+
expect(s1.valid).toBe(false);
|
|
53
|
+
expect(s1.reasons.map((r) => r.code)).toEqual(["session-seal-mismatch"]);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("an open session with a seal, or a closed one without, is schema-invalid", async () => {
|
|
57
|
+
const root = fixture();
|
|
58
|
+
const dir = join(root, "design", "sessions");
|
|
59
|
+
writeFileSync(join(dir, "S-0002-open.md"), sessionText({ id: "S-0002", state: "open" }).replace("verdicts: []", 'verdicts: []\nclosed_digest: "' + "0".repeat(64) + '"'));
|
|
60
|
+
writeFileSync(join(dir, "S-0003-closed.md"), sessionText({ id: "S-0003", state: "closed" }).replace(/^closed_digest: .*\n/m, ""));
|
|
61
|
+
const doc = await read(root);
|
|
62
|
+
expect(doc.records.filter((r) => r.id !== "S-0001").map((r) => [r.id, r.reasons.map((x) => x.code)])).toEqual([
|
|
63
|
+
["S-0002", ["record-schema-invalid"]],
|
|
64
|
+
["S-0003", ["record-schema-invalid"]],
|
|
65
|
+
]);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("a verdict naming a record no decision has is session-verdict-unknown-record", async () => {
|
|
69
|
+
const root = fixture();
|
|
70
|
+
writeFileSync(
|
|
71
|
+
join(root, "design", "sessions", "S-0002-walk.md"),
|
|
72
|
+
sessionText({ id: "S-0002", state: "closed", verdicts: [{ record: "ref-001", principal: "alice", verdict: "agree" }, { record: "ref-999", principal: "alice", verdict: "agree" }] }),
|
|
73
|
+
);
|
|
74
|
+
const s2 = (await read(root)).records.find((r) => r.id === "S-0002")!;
|
|
75
|
+
expect(s2.valid).toBe(false);
|
|
76
|
+
expect(s2.reasons).toEqual([{ code: "session-verdict-unknown-record", message: expect.stringContaining("verdicts[1] names ref-999") }]);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("each session lists the decision reviews that name it, and --at reads both kinds at the revision", async () => {
|
|
80
|
+
const root = fixture();
|
|
81
|
+
writeFileSync(join(root, "design", "sessions", "S-0002-walk.md"), sessionText({ id: "S-0002", state: "open" }));
|
|
82
|
+
const open = commitAll(root, "open S-0002");
|
|
83
|
+
reviewed(root, ["alice", "bob"], "S-0002");
|
|
84
|
+
writeFileSync(
|
|
85
|
+
join(root, "design", "sessions", "S-0002-walk.md"),
|
|
86
|
+
sessionText({ id: "S-0002", state: "closed", verdicts: [{ record: "ref-001", principal: "alice", verdict: "agree" }, { record: "ref-001", principal: "bob", verdict: "agree" }] }),
|
|
87
|
+
);
|
|
88
|
+
const doc = await read(root);
|
|
89
|
+
expect(doc.summary.invalid).toBe(0);
|
|
90
|
+
const s2 = doc.records.find((r) => r.id === "S-0002")!;
|
|
91
|
+
expect(s2.citedBy).toEqual([
|
|
92
|
+
{ id: "ref-001", path: "decisions/ref-001-how-the-app-is-deployed.md", index: 0, reviewer: "alice", verdict: "agree" },
|
|
93
|
+
{ id: "ref-001", path: "decisions/ref-001-how-the-app-is-deployed.md", index: 1, reviewer: "bob", verdict: "agree" },
|
|
94
|
+
]);
|
|
95
|
+
expect(doc.records.find((r) => r.id === "S-0001")!.citedBy).toEqual([]);
|
|
96
|
+
const then = await read(root, open);
|
|
97
|
+
expect(then.records.find((r) => r.id === "S-0002")).toMatchObject({ state: "open", valid: true, citedBy: [] });
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("the decision kind accepts a review entry naming its session", async () => {
|
|
101
|
+
const root = fixture();
|
|
102
|
+
reviewed(root, ["alice"], "S-0001");
|
|
103
|
+
const doc = await queryRecords({ kind: "decisions/decision.kind.mjs", cwd: root });
|
|
104
|
+
if ("error" in doc) throw new Error(doc.error.message);
|
|
105
|
+
expect(doc.records.find((r) => r.id === "ref-001")).toMatchObject({ valid: true });
|
|
106
|
+
expect((await read(root)).records[0].citedBy).toEqual([expect.objectContaining({ id: "ref-001", reviewer: "alice" })]);
|
|
107
|
+
});
|
|
108
|
+
});
|
|
@@ -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
|
+
});
|