@intentius/chant 0.79.0 → 0.80.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/audit/core.d.ts.map +1 -1
- package/dist/audit/discover.d.ts +38 -0
- package/dist/audit/discover.d.ts.map +1 -1
- package/dist/audit/terraform-state.d.ts +14 -0
- package/dist/audit/terraform-state.d.ts.map +1 -1
- package/dist/cli/command-group.d.ts +6 -0
- package/dist/cli/command-group.d.ts.map +1 -1
- package/dist/cli/commands/audit.d.ts +27 -2
- package/dist/cli/commands/audit.d.ts.map +1 -1
- package/dist/cli/commands/check-lexicon-examples.d.ts.map +1 -1
- package/dist/cli/commands/doctor.d.ts.map +1 -1
- package/dist/cli/commands/import-agents.d.ts.map +1 -1
- package/dist/cli/commands/init.d.ts +6 -0
- package/dist/cli/commands/init.d.ts.map +1 -1
- package/dist/cli/commands/lint.d.ts.map +1 -1
- package/dist/cli/commands/onboard.d.ts.map +1 -1
- package/dist/cli/commands/update.d.ts.map +1 -1
- package/dist/cli/conflict-check.d.ts +6 -0
- package/dist/cli/conflict-check.d.ts.map +1 -1
- package/dist/cli/handlers/dev.d.ts.map +1 -1
- package/dist/cli/handlers/lifecycle.d.ts.map +1 -1
- package/dist/cli/handlers/misc.d.ts.map +1 -1
- package/dist/cli/handlers/operator.d.ts.map +1 -1
- package/dist/cli/handlers/promote.d.ts +17 -0
- package/dist/cli/handlers/promote.d.ts.map +1 -0
- package/dist/cli/handlers/run-generate.d.ts +39 -0
- package/dist/cli/handlers/run-generate.d.ts.map +1 -0
- package/dist/cli/handlers/run.d.ts.map +1 -1
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/mcp/op-tools.d.ts.map +1 -1
- package/dist/cli/plugins.d.ts +9 -0
- package/dist/cli/plugins.d.ts.map +1 -1
- package/dist/cli/registry.d.ts +14 -1
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/components/capability-plugin-loader.d.ts.map +1 -1
- package/dist/components/cli-support.d.ts.map +1 -1
- package/dist/components/discover.d.ts.map +1 -1
- package/dist/components/driver.d.ts.map +1 -1
- package/dist/components/fan-out-support.d.ts.map +1 -1
- package/dist/components/pilots/alb-ecs.pilot.d.ts +7 -1
- package/dist/components/pilots/alb-ecs.pilot.d.ts.map +1 -1
- package/dist/components/presets/ecs-fargate.d.ts +10 -4
- package/dist/components/presets/ecs-fargate.d.ts.map +1 -1
- package/dist/components/promote.d.ts +211 -0
- package/dist/components/promote.d.ts.map +1 -0
- package/dist/components/unbound-gate-approval.d.ts +24 -0
- package/dist/components/unbound-gate-approval.d.ts.map +1 -0
- package/dist/config.d.ts +82 -5
- package/dist/config.d.ts.map +1 -1
- package/dist/discovery/convergence.d.ts +97 -0
- package/dist/discovery/convergence.d.ts.map +1 -0
- package/dist/discovery/files.d.ts +41 -2
- package/dist/discovery/files.d.ts.map +1 -1
- package/dist/discovery/fold-import.d.ts +15 -0
- package/dist/discovery/fold-import.d.ts.map +1 -1
- package/dist/graph-ir.d.ts +13 -0
- package/dist/graph-ir.d.ts.map +1 -1
- package/dist/lexicon-module.d.ts +65 -0
- package/dist/lexicon-module.d.ts.map +1 -0
- package/dist/lexicon.d.ts +24 -0
- package/dist/lexicon.d.ts.map +1 -1
- package/dist/lifecycle/build-ledger-store.d.ts.map +1 -1
- package/dist/lifecycle/legacy-digest.d.ts +26 -0
- package/dist/lifecycle/legacy-digest.d.ts.map +1 -0
- package/dist/lifecycle/release-ledger.d.ts +25 -0
- package/dist/lifecycle/release-ledger.d.ts.map +1 -1
- package/dist/lint/rules/comp/comp003-mutating-no-rollback.d.ts +2 -3
- package/dist/lint/rules/comp/comp003-mutating-no-rollback.d.ts.map +1 -1
- package/dist/op/activity-contract-registry.d.ts.map +1 -1
- package/dist/op/activity-registry.d.ts.map +1 -1
- package/dist/op/discover.d.ts.map +1 -1
- package/dist/op/gate-approval.d.ts.map +1 -1
- package/dist/op/generate-pipeline.d.ts.map +1 -1
- package/dist/op/runtimes/local.d.ts.map +1 -1
- package/dist/workspace/record-source.d.ts +22 -0
- package/dist/workspace/record-source.d.ts.map +1 -0
- package/dist/workspace/records-cli.d.ts +57 -0
- package/dist/workspace/records-cli.d.ts.map +1 -0
- package/dist/workspace/records.d.ts +118 -0
- package/dist/workspace/records.d.ts.map +1 -0
- package/package.json +3 -3
- package/src/audit/core.ts +13 -2
- package/src/audit/discover.ts +90 -12
- package/src/audit/terraform-state.test.ts +86 -1
- package/src/audit/terraform-state.ts +26 -0
- package/src/cli/command-group.ts +7 -0
- package/src/cli/commands/audit-walk-warnings.test.ts +156 -0
- package/src/cli/commands/audit.ts +93 -19
- package/src/cli/commands/check-lexicon-examples.ts +2 -1
- package/src/cli/commands/doctor.ts +3 -0
- package/src/cli/commands/import-agents.ts +11 -4
- package/src/cli/commands/init.ts +57 -25
- package/src/cli/commands/lint.test.ts +58 -1
- package/src/cli/commands/lint.ts +48 -18
- package/src/cli/commands/onboard.ts +15 -0
- package/src/cli/commands/update.ts +5 -1
- package/src/cli/conflict-check.test.ts +18 -1
- package/src/cli/conflict-check.ts +13 -0
- package/src/cli/discovery-skip.test.ts +168 -0
- package/src/cli/handlers/build.test.ts +11 -0
- package/src/cli/handlers/build.ts +1 -1
- package/src/cli/handlers/dev.ts +3 -0
- package/src/cli/handlers/graph.test.ts +20 -0
- package/src/cli/handlers/graph.ts +4 -2
- package/src/cli/handlers/lifecycle.ts +2 -1
- package/src/cli/handlers/misc.ts +16 -0
- package/src/cli/handlers/operator.ts +2 -1
- package/src/cli/handlers/promote.test.ts +261 -0
- package/src/cli/handlers/promote.ts +325 -0
- package/src/cli/handlers/run-generate.test.ts +160 -0
- package/src/cli/handlers/run-generate.ts +133 -0
- package/src/cli/handlers/run.ts +13 -1
- package/src/cli/main.test.ts +33 -1
- package/src/cli/main.ts +69 -16
- package/src/cli/mcp/op-tools.ts +2 -1
- package/src/cli/path-lexicon-messages.test.ts +152 -0
- package/src/cli/plugins.ts +44 -6
- package/src/cli/registry.ts +14 -1
- package/src/components/__fixtures__/alb-ecs-service.json +0 -8
- package/src/components/capability-plugin-loader.ts +3 -1
- package/src/components/cli-support.ts +3 -2
- package/src/components/discover.ts +26 -2
- package/src/components/driver.test.ts +12 -11
- package/src/components/driver.ts +2 -0
- package/src/components/fan-out-support.ts +2 -1
- package/src/components/pilots/README.md +1 -1
- package/src/components/pilots/alb-ecs.pilot.ts +7 -7
- package/src/components/presets/ecs-fargate.ts +10 -5
- package/src/components/promote.test.ts +371 -0
- package/src/components/promote.ts +537 -0
- package/src/components/unbound-gate-approval.test.ts +82 -0
- package/src/components/unbound-gate-approval.ts +51 -0
- package/src/config.test.ts +40 -0
- package/src/config.ts +128 -11
- package/src/discovery/convergence.test.ts +231 -0
- package/src/discovery/convergence.ts +399 -0
- package/src/discovery/files.test.ts +127 -1
- package/src/discovery/files.ts +97 -9
- package/src/discovery/fold-import.ts +83 -9
- package/src/discovery/sandbox/path-lexicon-parity.test.ts +162 -0
- package/src/graph-ir.ts +14 -0
- package/src/lexicon-module.test.ts +203 -0
- package/src/lexicon-module.ts +117 -0
- package/src/lexicon.ts +26 -0
- package/src/lifecycle/build-ledger-store.ts +11 -1
- package/src/lifecycle/legacy-digest.test.ts +114 -0
- package/src/lifecycle/legacy-digest.ts +62 -0
- package/src/lifecycle/release-ledger.ts +34 -0
- package/src/lint/rules/comp/comp003-mutating-no-rollback.ts +2 -3
- package/src/op/activity-contract-registry.ts +18 -4
- package/src/op/activity-registry.ts +26 -0
- package/src/op/discover-convergence.test.ts +70 -0
- package/src/op/discover.ts +4 -0
- package/src/op/gate-approval.ts +3 -1
- package/src/op/generate-pipeline.ts +2 -1
- package/src/op/runtimes/local.ts +2 -1
- package/src/workspace/record-source.ts +117 -0
- package/src/workspace/records-cli.ts +126 -0
- package/src/workspace/records-contract.test.ts +88 -0
- package/src/workspace/records.schema.json +120 -0
- package/src/workspace/records.test.ts +243 -0
- package/src/workspace/records.ts +383 -0
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"$id": "https://intentius.io/chant/schemas/workspace/records/v1/records.schema.json",
|
|
4
|
+
"title": "chant workspace records output",
|
|
5
|
+
"description": "What `chant workspace records --json` prints, version 1 of the read contract for records (#2536). This is the first-test slice (#2546): records are unsealed and there is no workspace declaration yet. Readers ignore fields they do not know; a field is only ever added within a version. The reason and error codes are closed lists: a new code is a new contract version.",
|
|
6
|
+
"oneOf": [{ "$ref": "#/$defs/result" }, { "$ref": "#/$defs/failure" }],
|
|
7
|
+
"$defs": {
|
|
8
|
+
"result": {
|
|
9
|
+
"description": "The read worked. Exit code 0, even when some records are invalid.",
|
|
10
|
+
"type": "object",
|
|
11
|
+
"required": ["$schema", "contract", "kind", "at", "current", "records", "summary"],
|
|
12
|
+
"properties": {
|
|
13
|
+
"$schema": { "const": "https://intentius.io/chant/schemas/workspace/records/v1/records.schema.json" },
|
|
14
|
+
"contract": { "const": 1 },
|
|
15
|
+
"kind": {
|
|
16
|
+
"type": "object",
|
|
17
|
+
"required": ["name", "schema", "file"],
|
|
18
|
+
"properties": {
|
|
19
|
+
"name": { "type": "string", "description": "The kind's name, such as \"decision\"." },
|
|
20
|
+
"schema": { "type": "string", "description": "The `$id` of the schema each record was validated against." },
|
|
21
|
+
"file": { "type": "string", "description": "The kind file, relative to the repository root, with / separators." }
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"at": {
|
|
25
|
+
"description": "The full commit id the records were read at (--at), or null for the working tree.",
|
|
26
|
+
"type": ["string", "null"],
|
|
27
|
+
"pattern": "^[0-9a-f]{40,64}$"
|
|
28
|
+
},
|
|
29
|
+
"error": false,
|
|
30
|
+
"current": { "type": "boolean", "description": "Whether superseded records were left out (--current)." },
|
|
31
|
+
"records": {
|
|
32
|
+
"description": "One entry per file the kind locates, sorted by path.",
|
|
33
|
+
"type": "array",
|
|
34
|
+
"items": { "$ref": "#/$defs/record" }
|
|
35
|
+
},
|
|
36
|
+
"summary": {
|
|
37
|
+
"type": "object",
|
|
38
|
+
"required": ["total", "valid", "invalid", "superseded"],
|
|
39
|
+
"properties": {
|
|
40
|
+
"total": { "type": "integer", "minimum": 0, "description": "Entries in records." },
|
|
41
|
+
"valid": { "type": "integer", "minimum": 0 },
|
|
42
|
+
"invalid": { "type": "integer", "minimum": 0 },
|
|
43
|
+
"superseded": { "type": "integer", "minimum": 0, "description": "Records a closed record supersedes, counted before --current leaves them out." }
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
"record": {
|
|
49
|
+
"type": "object",
|
|
50
|
+
"required": ["id", "path", "state", "valid", "reasons", "supersededBy", "data"],
|
|
51
|
+
"properties": {
|
|
52
|
+
"id": { "type": ["string", "null"], "description": "Null when the front matter could not be parsed or has no string id." },
|
|
53
|
+
"path": { "type": "string", "description": "From the repository root, with / separators." },
|
|
54
|
+
"state": { "type": ["string", "null"], "description": "The state as the record writes it. Supersession is reported in supersededBy, not here." },
|
|
55
|
+
"valid": { "type": "boolean", "description": "True exactly when reasons is empty." },
|
|
56
|
+
"reasons": { "type": "array", "items": { "$ref": "#/$defs/reason" } },
|
|
57
|
+
"supersededBy": {
|
|
58
|
+
"type": ["string", "null"],
|
|
59
|
+
"description": "The id of the closed record whose supersedes link replaces this one. Derived from that record's link, never from this record's own fields."
|
|
60
|
+
},
|
|
61
|
+
"data": {
|
|
62
|
+
"type": ["object", "null"],
|
|
63
|
+
"description": "The front matter as JSON, following the kind's schema when valid is true. Null when it could not be parsed."
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
"allOf": [
|
|
67
|
+
{
|
|
68
|
+
"if": { "properties": { "valid": { "const": true } } },
|
|
69
|
+
"then": { "properties": { "reasons": { "type": "array", "maxItems": 0 }, "data": { "type": "object" }, "id": { "type": "string" } } },
|
|
70
|
+
"else": { "properties": { "reasons": { "type": "array", "minItems": 1 } } }
|
|
71
|
+
}
|
|
72
|
+
]
|
|
73
|
+
},
|
|
74
|
+
"reason": {
|
|
75
|
+
"type": "object",
|
|
76
|
+
"required": ["code", "message"],
|
|
77
|
+
"properties": {
|
|
78
|
+
"code": {
|
|
79
|
+
"enum": [
|
|
80
|
+
"record-unparseable",
|
|
81
|
+
"record-schema-invalid",
|
|
82
|
+
"record-id-duplicate",
|
|
83
|
+
"record-supersedes-unknown",
|
|
84
|
+
"record-supersedes-conflict"
|
|
85
|
+
]
|
|
86
|
+
},
|
|
87
|
+
"message": { "type": "string" }
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
"failure": {
|
|
91
|
+
"description": "The read failed and returned no records. Exit code 1.",
|
|
92
|
+
"type": "object",
|
|
93
|
+
"required": ["$schema", "contract", "error"],
|
|
94
|
+
"properties": {
|
|
95
|
+
"$schema": { "const": "https://intentius.io/chant/schemas/workspace/records/v1/records.schema.json" },
|
|
96
|
+
"contract": { "const": 1 },
|
|
97
|
+
"error": {
|
|
98
|
+
"type": "object",
|
|
99
|
+
"required": ["code", "message"],
|
|
100
|
+
"properties": {
|
|
101
|
+
"code": {
|
|
102
|
+
"enum": [
|
|
103
|
+
"kind-unreadable",
|
|
104
|
+
"kind-invalid",
|
|
105
|
+
"schema-unreadable",
|
|
106
|
+
"schema-id-mismatch",
|
|
107
|
+
"schema-invalid",
|
|
108
|
+
"location-missing",
|
|
109
|
+
"not-a-git-repository",
|
|
110
|
+
"revision-unknown"
|
|
111
|
+
]
|
|
112
|
+
},
|
|
113
|
+
"message": { "type": "string" }
|
|
114
|
+
}
|
|
115
|
+
},
|
|
116
|
+
"records": false
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { cpSync, mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, test } from "vitest";
|
|
6
|
+
import { lexiconModulePath } from "../lexicon-module";
|
|
7
|
+
import { gitRevisionSource, gitRoot, resolveRevision, workingTreeSource } from "./record-source";
|
|
8
|
+
import { loadRecordKind, parseFrontMatter, readRecords, RecordReadError, type LoadedRecordKind } from "./records";
|
|
9
|
+
|
|
10
|
+
const REPO = join(import.meta.dirname, "..", "..", "..", "..");
|
|
11
|
+
const DECISIONS = join(REPO, "docs", "design", "decisions");
|
|
12
|
+
const SAMPLE = readFileSync(join(DECISIONS, "ws-003-seal-scope.md"), "utf-8");
|
|
13
|
+
|
|
14
|
+
/** A copy of a real decision with its id, state and supersedes links replaced. */
|
|
15
|
+
function decision(id: string, state = "decided", supersedes: string[] = []): string {
|
|
16
|
+
const links = supersedes.length === 0 ? "supersedes: []" : `supersedes:\n${supersedes.map((d) => ` - decision: "${d}"`).join("\n")}`;
|
|
17
|
+
return SAMPLE.replace(/^id: .*$/m, `id: "${id}"`)
|
|
18
|
+
.replace(/^state: .*$/m, `state: "${state}"`)
|
|
19
|
+
.replace(/^supersedes:(?: \[\])?\n(?: .*\n)*/m, `${links}\n`);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let dir: string;
|
|
23
|
+
beforeEach(() => {
|
|
24
|
+
dir = realpathSync(mkdtempSync(join(tmpdir(), "chant-records-")));
|
|
25
|
+
mkdirSync(join(dir, "decisions"));
|
|
26
|
+
cpSync(join(DECISIONS, "decision.kind.mjs"), join(dir, "decisions", "decision.kind.mjs"));
|
|
27
|
+
cpSync(join(DECISIONS, "decision.schema.json"), join(dir, "decisions", "decision.schema.json"));
|
|
28
|
+
});
|
|
29
|
+
afterEach(() => rmSync(dir, { recursive: true, force: true }));
|
|
30
|
+
|
|
31
|
+
function write(name: string, text: string): void {
|
|
32
|
+
writeFileSync(join(dir, "decisions", name), text);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function read(options: { current?: boolean } = {}) {
|
|
36
|
+
const loaded = await loadRecordKind(join(dir, "decisions", "decision.kind.mjs"));
|
|
37
|
+
return readRecords(loaded, { root: dir, source: workingTreeSource(dir), ...options });
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const codes = (r: { reasons: Array<{ code: string }> }) => r.reasons.map((x) => x.code);
|
|
41
|
+
|
|
42
|
+
describe("parseFrontMatter", () => {
|
|
43
|
+
test("reads the JSON subset of YAML, whatever the line endings", () => {
|
|
44
|
+
const fm = parseFrontMatter('---\r\nid: "a-001"\r\nn: 2\r\nok: true\r\nlist: []\r\n---\r\n# A\r\n');
|
|
45
|
+
expect(fm).toEqual({ ok: true, value: { id: "a-001", n: 2, ok: true, list: [] } });
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("an unquoted date stays a string", () => {
|
|
49
|
+
expect(parseFrontMatter("---\non: 2026-09-23\n---\n")).toEqual({ ok: true, value: { on: "2026-09-23" } });
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test.each([
|
|
53
|
+
["no front matter", "# Just a heading\n", /no front matter/],
|
|
54
|
+
["an unclosed block", "---\nid: x\n", /no front matter/],
|
|
55
|
+
["invalid YAML", "---\nid: [unclosed\n---\n", /not valid YAML/],
|
|
56
|
+
["a duplicate key", "---\nid: a\nid: b\n---\n", /not valid YAML/],
|
|
57
|
+
["a list at the top", "---\n- a\n---\n", /must be a mapping/],
|
|
58
|
+
["an alias", "---\na: &x {k: 1}\nb: *x\n---\n", /aliases/],
|
|
59
|
+
])("refuses %s", (_label, text, why) => {
|
|
60
|
+
const fm = parseFrontMatter(text);
|
|
61
|
+
expect(fm.ok).toBe(false);
|
|
62
|
+
if (!fm.ok) expect(fm.message).toMatch(why);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe("readRecords", () => {
|
|
67
|
+
test("every well-formed decision is valid, in path order", async () => {
|
|
68
|
+
write("ws-002-b.md", decision("ws-002"));
|
|
69
|
+
write("ws-001-a.md", decision("ws-001"));
|
|
70
|
+
write("README.md", "# not a record\n");
|
|
71
|
+
const result = await read();
|
|
72
|
+
expect(result.records.map((r) => [r.id, r.path, r.valid, r.state])).toEqual([
|
|
73
|
+
["ws-001", "decisions/ws-001-a.md", true, "decided"],
|
|
74
|
+
["ws-002", "decisions/ws-002-b.md", true, "decided"],
|
|
75
|
+
]);
|
|
76
|
+
expect(result.summary).toEqual({ total: 2, valid: 2, invalid: 0, superseded: 0 });
|
|
77
|
+
expect(result.records[0].data?.title).toBe("Seal scope");
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("a malformed file is returned with record-unparseable", async () => {
|
|
81
|
+
write("ws-001-a.md", "no front matter here\n");
|
|
82
|
+
const [r] = (await read()).records;
|
|
83
|
+
expect(r).toMatchObject({ id: null, state: null, valid: false, data: null });
|
|
84
|
+
expect(codes(r)).toEqual(["record-unparseable"]);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("a schema violation is returned with record-schema-invalid, and keeps its data", async () => {
|
|
88
|
+
write("ws-001-a.md", decision("ws-001").replace(/^state: .*$/m, 'state: "maybe"'));
|
|
89
|
+
const [r] = (await read()).records;
|
|
90
|
+
expect(codes(r)).toEqual(["record-schema-invalid"]);
|
|
91
|
+
expect(r.reasons[0].message).toMatch(/\/state/);
|
|
92
|
+
expect(r.id).toBe("ws-001");
|
|
93
|
+
expect(r.data).not.toBeNull();
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test("a supersedes link to a missing id is record-supersedes-unknown", async () => {
|
|
97
|
+
write("ws-002-b.md", decision("ws-002", "decided", ["ws-404"]));
|
|
98
|
+
const [r] = (await read()).records;
|
|
99
|
+
expect(codes(r)).toEqual(["record-supersedes-unknown"]);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("a repeated id is flagged on the later file only", async () => {
|
|
103
|
+
write("ws-001-a.md", decision("ws-001"));
|
|
104
|
+
write("ws-001-b.md", decision("ws-001"));
|
|
105
|
+
const records = (await read()).records;
|
|
106
|
+
expect(records.map(codes)).toEqual([[], ["record-id-duplicate"]]);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test("supersession comes from a closed record's link, never from the old record's own state", async () => {
|
|
110
|
+
write("ws-001-a.md", decision("ws-001", "superseded"));
|
|
111
|
+
write("ws-002-b.md", decision("ws-002", "ratified"));
|
|
112
|
+
write("ws-003-c.md", decision("ws-003", "ratified"));
|
|
113
|
+
write("ws-004-d.md", decision("ws-004", "ratified", ["ws-003"]));
|
|
114
|
+
const all = await read();
|
|
115
|
+
expect(all.records.map((r) => [r.id, r.supersededBy])).toEqual([
|
|
116
|
+
["ws-001", null],
|
|
117
|
+
["ws-002", null],
|
|
118
|
+
["ws-003", "ws-004"],
|
|
119
|
+
["ws-004", null],
|
|
120
|
+
]);
|
|
121
|
+
expect(all.summary.superseded).toBe(1);
|
|
122
|
+
const current = await read({ current: true });
|
|
123
|
+
expect(current.records.map((r) => r.id)).toEqual(["ws-001", "ws-002", "ws-004"]);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
test("a link from a decided record does not supersede yet", async () => {
|
|
127
|
+
write("ws-001-a.md", decision("ws-001", "ratified"));
|
|
128
|
+
write("ws-002-b.md", decision("ws-002", "decided", ["ws-001"]));
|
|
129
|
+
const current = await read({ current: true });
|
|
130
|
+
expect(current.records.map((r) => [r.id, r.valid])).toEqual([
|
|
131
|
+
["ws-001", true],
|
|
132
|
+
["ws-002", true],
|
|
133
|
+
]);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
test("a record superseded twice keeps the first and flags the second", async () => {
|
|
137
|
+
write("ws-001-a.md", decision("ws-001", "ratified"));
|
|
138
|
+
write("ws-002-b.md", decision("ws-002", "ratified", ["ws-001"]));
|
|
139
|
+
write("ws-003-c.md", decision("ws-003", "ratified", ["ws-001"]));
|
|
140
|
+
const records = (await read()).records;
|
|
141
|
+
expect(records[0].supersededBy).toBe("ws-002");
|
|
142
|
+
expect(codes(records[2])).toEqual(["record-supersedes-conflict"]);
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test("a missing records directory is location-missing", async () => {
|
|
146
|
+
const loaded = await loadRecordKind(join(dir, "decisions", "decision.kind.mjs"));
|
|
147
|
+
const moved: LoadedRecordKind = { ...loaded, dir: join(dir, "nowhere") };
|
|
148
|
+
await expect(readRecords(moved, { root: dir, source: workingTreeSource(dir) })).rejects.toMatchObject({ code: "location-missing" });
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
describe("loadRecordKind", () => {
|
|
153
|
+
const load = () => loadRecordKind(join(dir, "decisions", "decision.kind.mjs"));
|
|
154
|
+
|
|
155
|
+
test("the chant repo's decision kind loads with its schema", async () => {
|
|
156
|
+
const loaded = await loadRecordKind(join(DECISIONS, "decision.kind.mjs"));
|
|
157
|
+
expect(loaded.kind.name).toBe("decision");
|
|
158
|
+
expect(loaded.schema.$id).toBe("urn:intentius:chant:decision:1");
|
|
159
|
+
expect(loaded.dir).toBe(DECISIONS);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test("loads through the #2520 path loader and leaves nothing registered", async () => {
|
|
163
|
+
const file = join(DECISIONS, "decision.kind.mjs");
|
|
164
|
+
await loadRecordKind(file);
|
|
165
|
+
expect(lexiconModulePath(`record-kind:${file}`)).toBeUndefined();
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test("a missing file is kind-unreadable", async () => {
|
|
169
|
+
await expect(loadRecordKind(join(dir, "none.kind.ts"))).rejects.toMatchObject({ code: "kind-unreadable" });
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test("a module with no recordKind is kind-invalid", async () => {
|
|
173
|
+
writeFileSync(join(dir, "decisions", "decision.kind.mjs"), "export const other = 1;\n");
|
|
174
|
+
await expect(load()).rejects.toMatchObject({ code: "kind-invalid" });
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
test("an unknown field or a closed state outside states is kind-invalid", async () => {
|
|
178
|
+
const text = readFileSync(join(DECISIONS, "decision.kind.mjs"), "utf-8");
|
|
179
|
+
writeFileSync(join(dir, "decisions", "decision.kind.mjs"), text.replace('closedStates: ["ratified", "superseded"]', 'closedStates: ["sealed"]'));
|
|
180
|
+
await expect(load()).rejects.toMatchObject({ code: "kind-invalid" });
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
test("a schema whose $id differs is schema-id-mismatch", async () => {
|
|
184
|
+
const schema = JSON.parse(readFileSync(join(DECISIONS, "decision.schema.json"), "utf-8"));
|
|
185
|
+
writeFileSync(join(dir, "decisions", "decision.schema.json"), JSON.stringify({ ...schema, $id: "urn:other" }));
|
|
186
|
+
await expect(load()).rejects.toMatchObject({ code: "schema-id-mismatch" });
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test("an unreadable schema is schema-unreadable", async () => {
|
|
190
|
+
writeFileSync(join(dir, "decisions", "decision.schema.json"), "{ not json");
|
|
191
|
+
await expect(load()).rejects.toBeInstanceOf(RecordReadError);
|
|
192
|
+
await expect(load()).rejects.toMatchObject({ code: "schema-unreadable" });
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
describe("reading at a revision", () => {
|
|
197
|
+
const git = (...args: string[]) =>
|
|
198
|
+
execFileSync("git", args, {
|
|
199
|
+
cwd: dir,
|
|
200
|
+
encoding: "utf-8",
|
|
201
|
+
env: { ...process.env, GIT_AUTHOR_NAME: "t", GIT_AUTHOR_EMAIL: "t@example.com", GIT_COMMITTER_NAME: "t", GIT_COMMITTER_EMAIL: "t@example.com" },
|
|
202
|
+
}).trim();
|
|
203
|
+
|
|
204
|
+
test("reads the committed files, not the working tree", async () => {
|
|
205
|
+
write("ws-001-a.md", decision("ws-001"));
|
|
206
|
+
git("init", "-q", "-b", "main");
|
|
207
|
+
git("add", "-A");
|
|
208
|
+
git("commit", "-q", "-m", "one");
|
|
209
|
+
const first = git("rev-parse", "HEAD");
|
|
210
|
+
write("ws-002-b.md", decision("ws-002"));
|
|
211
|
+
write("ws-001-a.md", "broken now\n");
|
|
212
|
+
|
|
213
|
+
expect(gitRoot(join(dir, "decisions"))).toBe(dir);
|
|
214
|
+
const loaded = await loadRecordKind(join(dir, "decisions", "decision.kind.mjs"));
|
|
215
|
+
const at = await readRecords(loaded, { root: dir, source: gitRevisionSource(dir, resolveRevision(dir, "main")) });
|
|
216
|
+
expect(at.records.map((r) => [r.id, r.valid])).toEqual([["ws-001", true]]);
|
|
217
|
+
expect(resolveRevision(dir, first.slice(0, 8))).toBe(first);
|
|
218
|
+
|
|
219
|
+
const tree = await readRecords(loaded, { root: dir, source: workingTreeSource(dir) });
|
|
220
|
+
expect(tree.records.map((r) => [r.path, r.valid])).toEqual([
|
|
221
|
+
["decisions/ws-001-a.md", false],
|
|
222
|
+
["decisions/ws-002-b.md", true],
|
|
223
|
+
]);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
test("an unknown revision is revision-unknown, and a directory missing at the revision is location-missing", async () => {
|
|
227
|
+
write("ws-001-a.md", decision("ws-001"));
|
|
228
|
+
writeFileSync(join(dir, "other.txt"), "x\n");
|
|
229
|
+
git("init", "-q", "-b", "main");
|
|
230
|
+
git("add", "other.txt");
|
|
231
|
+
git("commit", "-q", "-m", "no records yet");
|
|
232
|
+
expect(() => resolveRevision(dir, "no-such-ref")).toThrow(expect.objectContaining({ code: "revision-unknown" }));
|
|
233
|
+
expect(() => resolveRevision(dir, "--all")).toThrow(expect.objectContaining({ code: "revision-unknown" }));
|
|
234
|
+
const loaded = await loadRecordKind(join(dir, "decisions", "decision.kind.mjs"));
|
|
235
|
+
await expect(
|
|
236
|
+
readRecords(loaded, { root: dir, source: gitRevisionSource(dir, resolveRevision(dir, "HEAD")) }),
|
|
237
|
+
).rejects.toMatchObject({ code: "location-missing" });
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
test("outside a repository there is no git root", () => {
|
|
241
|
+
expect(gitRoot(dir)).toBeUndefined();
|
|
242
|
+
});
|
|
243
|
+
});
|