@intentius/chant 0.84.0 → 0.86.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/main.d.ts.map +1 -1
- package/dist/cli/mcp/resource-handlers.d.ts +2 -1
- package/dist/cli/mcp/resource-handlers.d.ts.map +1 -1
- package/dist/cli/mcp/server.d.ts +1 -0
- package/dist/cli/mcp/server.d.ts.map +1 -1
- package/dist/cli/mcp/tools/composites.d.ts +44 -0
- package/dist/cli/mcp/tools/composites.d.ts.map +1 -0
- package/dist/cli/mcp/tools/search.d.ts.map +1 -1
- package/dist/cli/registry.d.ts +13 -1
- package/dist/cli/registry.d.ts.map +1 -1
- package/dist/components/cli-support.d.ts +4 -0
- package/dist/components/cli-support.d.ts.map +1 -1
- package/dist/composite.d.ts +6 -0
- package/dist/composite.d.ts.map +1 -1
- package/dist/lexicon.d.ts +44 -0
- package/dist/lexicon.d.ts.map +1 -1
- package/dist/lifecycle/gate-ledger.d.ts +13 -0
- package/dist/lifecycle/gate-ledger.d.ts.map +1 -1
- package/dist/workspace/__fixtures__/contract-repo.d.ts +7 -0
- package/dist/workspace/__fixtures__/contract-repo.d.ts.map +1 -1
- package/dist/workspace/__fixtures__/sessions.d.ts +23 -0
- package/dist/workspace/__fixtures__/sessions.d.ts.map +1 -0
- package/dist/workspace/checks/records.d.ts +1 -0
- package/dist/workspace/checks/records.d.ts.map +1 -1
- package/dist/workspace/checks.d.ts +4 -0
- package/dist/workspace/checks.d.ts.map +1 -1
- package/dist/workspace/composites.d.ts +152 -0
- package/dist/workspace/composites.d.ts.map +1 -0
- package/dist/workspace/conformance/index.d.ts +211 -0
- package/dist/workspace/conformance/index.d.ts.map +1 -0
- package/dist/workspace/conformance/vitest.d.ts +11 -0
- package/dist/workspace/conformance/vitest.d.ts.map +1 -0
- package/dist/workspace/declaration.d.ts +28 -0
- package/dist/workspace/declaration.d.ts.map +1 -1
- package/dist/workspace/declaration.schema.json +40 -0
- package/dist/workspace/declared-kinds.d.ts +43 -0
- package/dist/workspace/declared-kinds.d.ts.map +1 -0
- package/dist/workspace/graph-cli.d.ts +24 -2
- package/dist/workspace/graph-cli.d.ts.map +1 -1
- package/dist/workspace/intent-cli.d.ts +6 -1
- package/dist/workspace/intent-cli.d.ts.map +1 -1
- package/dist/workspace/intent-joins.d.ts +74 -9
- package/dist/workspace/intent-joins.d.ts.map +1 -1
- package/dist/workspace/intent.d.ts +90 -6
- package/dist/workspace/intent.d.ts.map +1 -1
- package/dist/workspace/ls.d.ts +31 -1
- package/dist/workspace/ls.d.ts.map +1 -1
- package/dist/workspace/member-commands.d.ts +7 -2
- package/dist/workspace/member-commands.d.ts.map +1 -1
- package/dist/workspace/reason-codes.d.ts +52 -2
- package/dist/workspace/reason-codes.d.ts.map +1 -1
- package/dist/workspace/record-sessions.d.ts +51 -0
- package/dist/workspace/record-sessions.d.ts.map +1 -0
- package/dist/workspace/record-source.d.ts +2 -0
- package/dist/workspace/record-source.d.ts.map +1 -1
- package/dist/workspace/records-cli.d.ts +65 -4
- package/dist/workspace/records-cli.d.ts.map +1 -1
- package/dist/workspace/records-since.d.ts +90 -0
- package/dist/workspace/records-since.d.ts.map +1 -0
- package/dist/workspace/records-write.d.ts +164 -0
- package/dist/workspace/records-write.d.ts.map +1 -0
- package/dist/workspace/records.d.ts +202 -15
- package/dist/workspace/records.d.ts.map +1 -1
- package/dist/workspace/runtimes.d.ts +60 -0
- package/dist/workspace/runtimes.d.ts.map +1 -0
- package/dist/workspace/status-gates.d.ts +90 -0
- package/dist/workspace/status-gates.d.ts.map +1 -0
- package/dist/workspace/status.d.ts +17 -0
- package/dist/workspace/status.d.ts.map +1 -1
- package/dist/workspace/work.d.ts +56 -0
- package/dist/workspace/work.d.ts.map +1 -0
- package/package.json +19 -1
- package/src/cli/handlers/graph.ts +4 -0
- package/src/cli/main.test.ts +9 -0
- package/src/cli/main.ts +56 -3
- package/src/cli/mcp/resource-handlers.ts +17 -0
- package/src/cli/mcp/server.test.ts +140 -4
- package/src/cli/mcp/server.ts +5 -1
- package/src/cli/mcp/tools/composites.ts +98 -0
- package/src/cli/mcp/tools/search.ts +47 -5
- package/src/cli/registry.ts +13 -1
- package/src/components/cli-support.test.ts +16 -0
- package/src/components/cli-support.ts +8 -2
- package/src/composite.ts +9 -0
- package/src/lexicon.ts +47 -0
- package/src/lifecycle/gate-ledger.ts +14 -0
- package/src/workspace/__fixtures__/contract-repo.ts +17 -0
- package/src/workspace/__fixtures__/sessions.ts +66 -0
- package/src/workspace/checks/records.ts +19 -0
- package/src/workspace/checks.test.ts +2 -0
- package/src/workspace/checks.ts +7 -1
- package/src/workspace/composites.schema.json +533 -0
- package/src/workspace/composites.test.ts +334 -0
- package/src/workspace/composites.ts +316 -0
- package/src/workspace/conformance/__fixture__/app/package.json +7 -0
- package/src/workspace/conformance/__fixture__/app/src/server.mjs +29 -0
- package/src/workspace/conformance/__fixture__/decisions/decision.kind.mjs +32 -0
- package/src/workspace/conformance/__fixture__/decisions/decision.schema.json +364 -0
- package/src/workspace/conformance/__fixture__/decisions/fix-001-how-the-app-is-deployed.md +40 -0
- package/src/workspace/conformance/__fixture__/delivery/chant.config.ts +7 -0
- package/src/workspace/conformance/__fixture__/delivery/lexicon/index.ts +26 -0
- package/src/workspace/conformance/__fixture__/delivery/package.json +7 -0
- package/src/workspace/conformance/__fixture__/delivery/src/app.component.ts +14 -0
- package/src/workspace/conformance/__fixture__/delivery/src/app.ts +4 -0
- package/src/workspace/conformance/conformance.test.ts +149 -0
- package/src/workspace/conformance/index.mjs +31 -0
- package/src/workspace/conformance/index.ts +453 -0
- package/src/workspace/conformance/vitest.ts +62 -0
- package/src/workspace/declaration.schema.json +40 -0
- package/src/workspace/declaration.ts +62 -0
- package/src/workspace/declared-kinds.test.ts +321 -0
- package/src/workspace/declared-kinds.ts +76 -0
- package/src/workspace/graph-cli.ts +40 -4
- package/src/workspace/intent-cli.ts +54 -7
- package/src/workspace/intent-joins.test.ts +60 -0
- package/src/workspace/intent-joins.ts +117 -20
- package/src/workspace/intent.schema.json +357 -19
- package/src/workspace/intent.test.ts +235 -20
- package/src/workspace/intent.ts +396 -51
- package/src/workspace/ls.schema.json +34 -0
- package/src/workspace/ls.ts +69 -4
- package/src/workspace/member-commands.ts +11 -5
- package/src/workspace/read-contract.test.ts +52 -3
- package/src/workspace/reason-codes.test.ts +48 -4
- package/src/workspace/reason-codes.ts +67 -2
- package/src/workspace/record-assets.test.ts +3 -1
- package/src/workspace/record-sessions.ts +105 -0
- package/src/workspace/record-source.ts +14 -5
- package/src/workspace/records-amend.schema.json +167 -0
- package/src/workspace/records-cli.ts +246 -19
- package/src/workspace/records-contract.test.ts +77 -2
- package/src/workspace/records-formats.test.ts +640 -0
- package/src/workspace/records-new.schema.json +158 -0
- package/src/workspace/records-quorum.test.ts +196 -0
- package/src/workspace/records-review.schema.json +202 -0
- package/src/workspace/records-sessions.test.ts +108 -0
- package/src/workspace/records-since.schema.json +193 -0
- package/src/workspace/records-since.test.ts +174 -0
- package/src/workspace/records-since.ts +259 -0
- package/src/workspace/records-write-contract.test.ts +125 -0
- package/src/workspace/records-write.test.ts +373 -0
- package/src/workspace/records-write.ts +736 -0
- package/src/workspace/records.schema.json +187 -9
- package/src/workspace/records.test.ts +93 -0
- package/src/workspace/records.ts +683 -49
- package/src/workspace/runtimes.ts +107 -0
- package/src/workspace/status-contract.test.ts +163 -0
- package/src/workspace/status-gates.ts +215 -0
- package/src/workspace/status.schema.json +69 -3
- package/src/workspace/status.ts +35 -2
- package/src/workspace/work.test.ts +388 -0
- package/src/workspace/work.ts +163 -0
|
@@ -7,7 +7,12 @@
|
|
|
7
7
|
* `path:app/server.mjs` and pins the spec by hash.
|
|
8
8
|
* - c2 adds `app/server.mjs` inside dec-001's window, with a `Unit: U-0001`
|
|
9
9
|
* trailer that the fixture plugin maps to a unit, a contract and evidence,
|
|
10
|
-
* and a `Made-By` trailer the plugin says claims authorship.
|
|
10
|
+
* and a `Made-By` trailer the plugin says claims authorship. U-0001's
|
|
11
|
+
* record names dec-001, so c2 is dec-001's own work.
|
|
12
|
+
* - c2b edits `app/server.mjs` inside dec-001's window too, from unit
|
|
13
|
+
* U-0002, whose record names no decision and which changed its contract's
|
|
14
|
+
* criteria. It is decided by the window only (#2656), and the plugin
|
|
15
|
+
* returns a finding for it in its own namespace, `plugin:units:`.
|
|
11
16
|
* - c3 adds dec-002, which supersedes dec-001, pins the spec at the same hash
|
|
12
17
|
* and constrains `member:design` only.
|
|
13
18
|
* - c4 edits `app/server.mjs` after that, with no decision covering it.
|
|
@@ -59,11 +64,15 @@ export function commitJoins(commit, context) {
|
|
|
59
64
|
if (!id) return undefined;
|
|
60
65
|
if (id === "U-BROKEN") throw new Error("no such unit");
|
|
61
66
|
const unit = JSON.parse(context.read(\`units/\${id}.json\`));
|
|
67
|
+
if (unit.badCode) return { findings: [{ code: "plugin:other:x", message: "wrong namespace" }] };
|
|
62
68
|
return {
|
|
63
|
-
unit: { id, role: unit.role, outcome: unit.outcome },
|
|
69
|
+
unit: { id, role: unit.role, outcome: unit.outcome, ...(unit.decisions ? { decisions: unit.decisions } : {}) },
|
|
64
70
|
contract: { id: unit.contract, status: "closed" },
|
|
65
71
|
evidence: [{ id: "E-1", ok: true }],
|
|
66
72
|
authorship: commit.trailers["Made-By"] ? ["Made-By"] : [],
|
|
73
|
+
findings: unit.criteriaChanged
|
|
74
|
+
? [{ code: "plugin:units:criteria-changed-undecided", message: \`\${unit.contract} changed its criteria in this commit with no decision\`, refs: [commit.sha, unit.contract, "dec-001", "no-such-thing"] }]
|
|
75
|
+
: [],
|
|
67
76
|
};
|
|
68
77
|
}
|
|
69
78
|
`;
|
|
@@ -72,7 +81,8 @@ export function commitJoins(commit, context) {
|
|
|
72
81
|
const DATA_PLUGIN = `export const commitJoins = { trailers: { unit: "Unit" }, records: { unit: "units/{id}.json" }, authorship: ["Made-By"] };\n`;
|
|
73
82
|
|
|
74
83
|
const SERVER_1 = "// the app\nexport const status = 'Running.';\nexport const port = 8080;\n";
|
|
75
|
-
const
|
|
84
|
+
const SERVER_1B = "// the app\nexport const status = 'Running.';\nexport const port = 9090;\n";
|
|
85
|
+
const SERVER_2 = "// the app\nexport const status = 'Up.';\nexport const port = 9090;\n";
|
|
76
86
|
|
|
77
87
|
let root: string;
|
|
78
88
|
const sha: Record<string, string> = {};
|
|
@@ -106,11 +116,15 @@ beforeAll(() => {
|
|
|
106
116
|
"plugins/units.kind.mjs": PLUGIN,
|
|
107
117
|
"plugins/units-data.kind.mjs": DATA_PLUGIN,
|
|
108
118
|
"plugins/empty.kind.mjs": "export const nothing = 1;\n",
|
|
109
|
-
"units/U-0001.json": JSON.stringify({ role: "implement", contract: "C-001", outcome: "done" }),
|
|
119
|
+
"units/U-0001.json": JSON.stringify({ role: "implement", contract: "C-001", outcome: "done", decisions: ["dec-001"] }),
|
|
120
|
+
"units/U-0002.json": JSON.stringify({ role: "implement", contract: "ctr-002", outcome: "done", criteriaChanged: true }),
|
|
121
|
+
"units/U-BAD.json": JSON.stringify({ badCode: true }),
|
|
110
122
|
});
|
|
111
123
|
sha.c1 = commit(["decide the server"]);
|
|
112
124
|
writeFiles(root, { "app/server.mjs": SERVER_1 });
|
|
113
125
|
sha.c2 = commit(["add the server", "Unit: U-0001\nMade-By: agent"]);
|
|
126
|
+
writeFiles(root, { "app/server.mjs": SERVER_1B });
|
|
127
|
+
sha.c2b = commit(["move the port", "Unit: U-0002"]);
|
|
114
128
|
writeFiles(root, { "decisions/dec-002-design.md": decision("dec-002", { constrains: ["member:design"], evidence: [pin], supersedes: ["dec-001"] }) });
|
|
115
129
|
sha.c3 = commit(["move the decision to the design member"]);
|
|
116
130
|
writeFiles(root, { "app/server.mjs": SERVER_2 });
|
|
@@ -139,8 +153,8 @@ describe("the intent graph of app/server.mjs (#2651)", () => {
|
|
|
139
153
|
expect(doc).toMatchObject({ contract: 1, at: null, workspace: { name: "studio", root: "." }, region: "region:app/server.mjs" });
|
|
140
154
|
expect(doc.history).toEqual({ rev: sha.c4, follows: "file", shallow: false });
|
|
141
155
|
expect(doc.kinds).toEqual([
|
|
142
|
-
{ file: KIND, records: "decision", joins: null },
|
|
143
|
-
{ file: "plugins/units.kind.mjs", records: null, joins: "function" },
|
|
156
|
+
{ file: KIND, name: "decision", records: "decision", joins: null },
|
|
157
|
+
{ file: "plugins/units.kind.mjs", name: "units", records: null, joins: "function" },
|
|
144
158
|
]);
|
|
145
159
|
expect(doc.reasons).toEqual([]);
|
|
146
160
|
|
|
@@ -148,10 +162,13 @@ describe("the intent graph of app/server.mjs (#2651)", () => {
|
|
|
148
162
|
"region:app/server.mjs",
|
|
149
163
|
"member:app",
|
|
150
164
|
`commit:${sha.c4}`,
|
|
165
|
+
`commit:${sha.c2b}`,
|
|
166
|
+
"unit:U-0002",
|
|
167
|
+
"contract:ctr-002",
|
|
168
|
+
"evidence:E-1",
|
|
151
169
|
`commit:${sha.c2}`,
|
|
152
170
|
"unit:U-0001",
|
|
153
171
|
"contract:C-001",
|
|
154
|
-
"evidence:E-1",
|
|
155
172
|
"record:decision/dec-001",
|
|
156
173
|
"record:decision/dec-002",
|
|
157
174
|
"artifact:design/screens/home.json",
|
|
@@ -159,6 +176,7 @@ describe("the intent graph of app/server.mjs (#2651)", () => {
|
|
|
159
176
|
"finding:intent-commit-bare:1",
|
|
160
177
|
"finding:intent-decision-superseded-live:1",
|
|
161
178
|
"finding:intent-trailer-unverified:1",
|
|
179
|
+
"finding:plugin:units:criteria-changed-undecided:1",
|
|
162
180
|
]);
|
|
163
181
|
|
|
164
182
|
expect(node(doc, "region:app/server.mjs")).toEqual({ id: "region:app/server.mjs", kind: "region", path: "app/server.mjs", lines: null, member: "app", at: null, type: "file", generated: false, node: null });
|
|
@@ -172,9 +190,13 @@ describe("the intent graph of app/server.mjs (#2651)", () => {
|
|
|
172
190
|
pullRequest: null,
|
|
173
191
|
signature: { level: "unattested" },
|
|
174
192
|
lines: null,
|
|
193
|
+
// U-0001's record names dec-001: the decision's own work.
|
|
194
|
+
state: "decided",
|
|
175
195
|
});
|
|
176
|
-
|
|
177
|
-
expect(node(doc,
|
|
196
|
+
// Inside dec-001's window, from a unit that names no decision (#2656).
|
|
197
|
+
expect(node(doc, `commit:${sha.c2b}`)).toMatchObject({ subject: "move the port", state: "decided-by-window" });
|
|
198
|
+
expect(node(doc, `commit:${sha.c4}`)).toMatchObject({ subject: "tweak the status line", trailers: {}, state: "undecided" });
|
|
199
|
+
expect(node(doc, "unit:U-0001")).toEqual({ id: "unit:U-0001", kind: "unit", ref: "U-0001", plugin: "plugins/units.kind.mjs", data: { role: "implement", outcome: "done", decisions: ["dec-001"] } });
|
|
178
200
|
expect(node(doc, "contract:C-001")).toMatchObject({ kind: "contract", ref: "C-001", data: { status: "closed" } });
|
|
179
201
|
expect(node(doc, "evidence:E-1")).toMatchObject({ kind: "evidence", ref: "E-1", data: { ok: true } });
|
|
180
202
|
expect(node(doc, "record:decision/dec-001")).toMatchObject({
|
|
@@ -206,6 +228,10 @@ describe("the intent graph of app/server.mjs (#2651)", () => {
|
|
|
206
228
|
|
|
207
229
|
expect(edges(doc)).toEqual([
|
|
208
230
|
`touched-by region:app/server.mjs -> commit:${sha.c4}`,
|
|
231
|
+
`touched-by region:app/server.mjs -> commit:${sha.c2b}`,
|
|
232
|
+
`produced-by commit:${sha.c2b} -> unit:U-0002`,
|
|
233
|
+
"serves unit:U-0002 -> contract:ctr-002",
|
|
234
|
+
"cites-evidence unit:U-0002 -> evidence:E-1",
|
|
209
235
|
`touched-by region:app/server.mjs -> commit:${sha.c2}`,
|
|
210
236
|
`produced-by commit:${sha.c2} -> unit:U-0001`,
|
|
211
237
|
"serves unit:U-0001 -> contract:C-001",
|
|
@@ -214,28 +240,82 @@ describe("the intent graph of app/server.mjs (#2651)", () => {
|
|
|
214
240
|
"supersedes record:decision/dec-002 -> record:decision/dec-001",
|
|
215
241
|
"pins record:decision/dec-001 -> artifact:design/screens/home.json (pinned)",
|
|
216
242
|
"pins record:decision/dec-002 -> artifact:design/screens/home.json (stale)",
|
|
243
|
+
`within commit:${sha.c2b} -> record:decision/dec-001`,
|
|
244
|
+
`within commit:${sha.c2} -> record:decision/dec-001`,
|
|
245
|
+
]);
|
|
246
|
+
expect(doc.edges.filter((e) => e.kind === "within")).toEqual([
|
|
247
|
+
{ kind: "within", from: `commit:${sha.c2b}`, to: "record:decision/dec-001", state: "decided-by-window" },
|
|
248
|
+
{ kind: "within", from: `commit:${sha.c2}`, to: "record:decision/dec-001", state: "decided" },
|
|
217
249
|
]);
|
|
218
250
|
|
|
219
|
-
// c2
|
|
251
|
+
// c2 and c2b fall inside dec-001's window (from c1 until c3), so neither is
|
|
252
|
+
// intent-commit-undecided; c4 comes after it. The plugin's finding is about
|
|
253
|
+
// c2b, and its refs resolve to the nodes they name, except the one that
|
|
254
|
+
// names nothing in the graph.
|
|
220
255
|
expect(findings(doc)).toEqual([
|
|
221
256
|
["intent-commit-undecided", [`commit:${sha.c4}`, "region:app/server.mjs"]],
|
|
222
257
|
["intent-commit-bare", [`commit:${sha.c4}`]],
|
|
223
258
|
["intent-decision-superseded-live", ["region:app/server.mjs", "record:decision/dec-001"]],
|
|
224
259
|
["intent-trailer-unverified", [`commit:${sha.c2}`]],
|
|
260
|
+
["plugin:units:criteria-changed-undecided", [`commit:${sha.c2b}`, "contract:ctr-002", "record:decision/dec-001"]],
|
|
225
261
|
]);
|
|
226
|
-
expect(doc
|
|
262
|
+
expect(node(doc, "finding:plugin:units:criteria-changed-undecided:1")).toEqual({
|
|
263
|
+
id: "finding:plugin:units:criteria-changed-undecided:1",
|
|
264
|
+
kind: "finding",
|
|
265
|
+
code: "plugin:units:criteria-changed-undecided",
|
|
266
|
+
message: "ctr-002 changed its criteria in this commit with no decision",
|
|
267
|
+
concerns: [`commit:${sha.c2b}`, "contract:ctr-002", "record:decision/dec-001"],
|
|
268
|
+
plugin: "plugins/units.kind.mjs",
|
|
269
|
+
refs: [sha.c2b, "ctr-002", "dec-001", "no-such-thing"],
|
|
270
|
+
});
|
|
271
|
+
expect(doc.summary).toEqual({ commits: 3, decisions: 2, artifacts: 1, findings: 5 });
|
|
227
272
|
});
|
|
228
273
|
|
|
229
|
-
test("the text walk runs region, decisions, artifacts, commits, findings, one line each", async () => {
|
|
274
|
+
test("the text walk runs region, decisions with their in-window commits, artifacts, commits, findings, one line each", async () => {
|
|
230
275
|
const text = formatIntent(await walk("app/server.mjs"));
|
|
231
276
|
const lines = text.split("\n");
|
|
232
|
-
expect(lines.map((l) => l.trim().split(/\s+/)[0])).toEqual([
|
|
277
|
+
expect(lines.map((l) => l.trim().split(/\s+/)[0])).toEqual([
|
|
278
|
+
"region",
|
|
279
|
+
"decision",
|
|
280
|
+
"within",
|
|
281
|
+
"decided",
|
|
282
|
+
"ask",
|
|
283
|
+
"decision",
|
|
284
|
+
"artifact",
|
|
285
|
+
"commit",
|
|
286
|
+
"commit",
|
|
287
|
+
"unit",
|
|
288
|
+
"contract",
|
|
289
|
+
"evidence",
|
|
290
|
+
"commit",
|
|
291
|
+
"unit",
|
|
292
|
+
"contract",
|
|
293
|
+
"evidence",
|
|
294
|
+
"finding",
|
|
295
|
+
"finding",
|
|
296
|
+
"finding",
|
|
297
|
+
"finding",
|
|
298
|
+
"finding",
|
|
299
|
+
"3",
|
|
300
|
+
]);
|
|
233
301
|
expect(lines[0]).toBe("region app/server.mjs (file, member app) in the working tree");
|
|
234
302
|
expect(lines[1]).toContain("dec-001 decided, superseded by dec-002");
|
|
235
303
|
expect(lines[1]).toContain("path:app/server.mjs (path)");
|
|
236
|
-
|
|
237
|
-
expect(lines[
|
|
238
|
-
expect(lines
|
|
304
|
+
// The in-window commits under their decision, and the question once (#2656, #2650 B).
|
|
305
|
+
expect(lines[2]).toBe(` within ${sha.c2b.slice(0, 8)} move the port; unit U-0002, in dec-001's window and not its work`);
|
|
306
|
+
expect(lines[3]).toBe(` decided ${sha.c2.slice(0, 8)} add the server; unit U-0001, dec-001's own work`);
|
|
307
|
+
expect(lines[4]).toBe(" ask is this drift, a superseding decision nobody wrote down, or the decision being wrong?");
|
|
308
|
+
expect(lines[6]).toBe(`artifact design/screens/home.json stale; pinned by dec-001 at ${HOME_SHA.slice(0, 8)} (pinned), dec-002 at ${HOME_SHA.slice(0, 8)} (stale); now ${HOME_SHA.slice(0, 8)}`);
|
|
309
|
+
expect(lines[7]).toContain(`${sha.c4.slice(0, 8)} `);
|
|
310
|
+
expect(lines.at(-2)).toBe("finding plugin:units:criteria-changed-undecided: ctr-002 changed its criteria in this commit with no decision");
|
|
311
|
+
expect(lines.at(-1)).toBe("3 commits, 2 decisions, 1 artifacts, 5 findings");
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
test("a decision whose in-window commits are all its own work asks nothing", async () => {
|
|
315
|
+
const doc = await walk("app/server.mjs", { at: sha.c2 });
|
|
316
|
+
const text = formatIntent(doc);
|
|
317
|
+
expect(text).toContain(` decided ${sha.c2.slice(0, 8)} add the server`);
|
|
318
|
+
expect(text).not.toContain(" ask ");
|
|
239
319
|
});
|
|
240
320
|
|
|
241
321
|
test("a line range follows the lines with git log -L, and names the lines each commit changed", async () => {
|
|
@@ -250,15 +330,27 @@ describe("the intent graph of app/server.mjs (#2651)", () => {
|
|
|
250
330
|
|
|
251
331
|
test("the data form of commitJoins joins the same unit, with no plugin code", async () => {
|
|
252
332
|
const doc = await walk("app/server.mjs", { kinds: [KIND, "plugins/units-data.kind.mjs"] });
|
|
253
|
-
expect(doc.kinds[1]).toEqual({ file: "plugins/units-data.kind.mjs", records: null, joins: "data" });
|
|
254
|
-
expect(node(doc, "unit:U-0001")).toEqual({
|
|
333
|
+
expect(doc.kinds[1]).toEqual({ file: "plugins/units-data.kind.mjs", name: "units-data", records: null, joins: "data" });
|
|
334
|
+
expect(node(doc, "unit:U-0001")).toEqual({
|
|
335
|
+
id: "unit:U-0001",
|
|
336
|
+
kind: "unit",
|
|
337
|
+
ref: "U-0001",
|
|
338
|
+
plugin: "plugins/units-data.kind.mjs",
|
|
339
|
+
data: { role: "implement", contract: "C-001", outcome: "done", decisions: ["dec-001"] },
|
|
340
|
+
});
|
|
255
341
|
expect(doc.edges).toContainEqual({ kind: "produced-by", from: `commit:${sha.c2}`, to: "unit:U-0001" });
|
|
342
|
+
// The unit record's decisions field makes c2 dec-001's own work in the data form too.
|
|
343
|
+
expect(node(doc, `commit:${sha.c2}`)).toMatchObject({ state: "decided" });
|
|
344
|
+
expect(node(doc, `commit:${sha.c2b}`)).toMatchObject({ state: "decided-by-window" });
|
|
345
|
+
expect(findings(doc).map(([c]) => c)).not.toContain("plugin:units:criteria-changed-undecided");
|
|
256
346
|
expect(findings(doc).map(([c]) => c)).toContain("intent-trailer-unverified");
|
|
257
347
|
});
|
|
258
348
|
|
|
259
349
|
test("without --kind there are no decisions, so no decision findings", async () => {
|
|
260
350
|
const doc = await walk("app/server.mjs", { kinds: [] });
|
|
261
|
-
expect(doc.nodes.map((n) => n.kind)).toEqual(["region", "member", "commit", "commit"]);
|
|
351
|
+
expect(doc.nodes.map((n) => n.kind)).toEqual(["region", "member", "commit", "commit", "commit"]);
|
|
352
|
+
expect(doc.nodes.filter((n) => n.kind === "commit").map((n) => n.kind === "commit" && n.state)).toEqual([null, null, null]);
|
|
353
|
+
expect(doc.edges.filter((e) => e.kind === "within")).toEqual([]);
|
|
262
354
|
expect(findings(doc)).toEqual([]);
|
|
263
355
|
});
|
|
264
356
|
|
|
@@ -360,6 +452,17 @@ describe("findings the fixture does not raise (#2651)", () => {
|
|
|
360
452
|
),
|
|
361
453
|
);
|
|
362
454
|
|
|
455
|
+
test(
|
|
456
|
+
"a commit whose unit serves a contract the decision constrains is the decision's own work (#2656)",
|
|
457
|
+
withFile("decisions/dec-001-server.md", decision("dec-001", { constrains: ["path:app/server.mjs", "ctr-002"], evidence: [pin] }), async () => {
|
|
458
|
+
const doc = await walk("app/server.mjs");
|
|
459
|
+
expect(doc.edges).toContainEqual({ kind: "constrains", from: "record:decision/dec-001", to: "contract:ctr-002", granularity: "contract", entry: "ctr-002" });
|
|
460
|
+
expect(node(doc, `commit:${sha.c2b}`)).toMatchObject({ state: "decided" });
|
|
461
|
+
expect(doc.edges).toContainEqual({ kind: "within", from: `commit:${sha.c2b}`, to: "record:decision/dec-001", state: "decided" });
|
|
462
|
+
expect(formatIntent(doc)).not.toContain(" ask ");
|
|
463
|
+
}),
|
|
464
|
+
);
|
|
465
|
+
|
|
363
466
|
test(
|
|
364
467
|
"a decision constraining an issue the region's commits name covers it at issue granularity",
|
|
365
468
|
withFile("decisions/dec-003-issue.md", decision("dec-003", { constrains: ["acme/studio#12"] }), async () => {
|
|
@@ -406,7 +509,22 @@ describe("reads that fail, and parts that can't be read (#2651)", () => {
|
|
|
406
509
|
if ("error" in doc) throw new Error(doc.error.message);
|
|
407
510
|
expect(failed).toBe(true);
|
|
408
511
|
expect(doc.reasons).toEqual([{ code: "intent-plugin-failed", message: expect.stringContaining("no such unit") }]);
|
|
409
|
-
expect(doc.summary.commits).toBe(
|
|
512
|
+
expect(doc.summary.commits).toBe(4);
|
|
513
|
+
} finally {
|
|
514
|
+
git(root, "reset", "-q", "--hard", sha.c4);
|
|
515
|
+
}
|
|
516
|
+
});
|
|
517
|
+
|
|
518
|
+
test("a plugin finding outside the kind's namespace is intent-plugin-failed (#2656)", async () => {
|
|
519
|
+
writeFiles(root, { "app/server.mjs": `${SERVER_2}// bad code\n` });
|
|
520
|
+
commit(["a finding in someone else's namespace", "Unit: U-BAD"]);
|
|
521
|
+
try {
|
|
522
|
+
const { doc, failed } = await intentGraph({ cwd: root, region: "app/server.mjs", kinds: [join(root, KIND), join(root, "plugins/units.kind.mjs")] });
|
|
523
|
+
expectValid(doc);
|
|
524
|
+
if ("error" in doc) throw new Error(doc.error.message);
|
|
525
|
+
expect(failed).toBe(true);
|
|
526
|
+
expect(doc.reasons).toEqual([{ code: "intent-plugin-failed", message: expect.stringContaining("plugin:units:<code>") }]);
|
|
527
|
+
expect(doc.nodes.some((n) => n.kind === "finding" && n.code === "plugin:other:x")).toBe(false);
|
|
410
528
|
} finally {
|
|
411
529
|
git(root, "reset", "-q", "--hard", sha.c4);
|
|
412
530
|
}
|
|
@@ -446,3 +564,100 @@ describe("reads that fail, and parts that can't be read (#2651)", () => {
|
|
|
446
564
|
expect(parseRegion("a/b.ts:0")).toHaveProperty("error");
|
|
447
565
|
});
|
|
448
566
|
});
|
|
567
|
+
|
|
568
|
+
describe("commitJoins lists records and names its findings (#2663)", () => {
|
|
569
|
+
test("a plugin joins commits by listing its records, with no trailer at all (#2663)", async () => {
|
|
570
|
+
const SCAN = `
|
|
571
|
+
export function commitJoins(commit, context) {
|
|
572
|
+
const walk = (dir) => (context.list(dir) ?? []).flatMap((p) => (p.endsWith("/") ? walk(p) : [p]));
|
|
573
|
+
for (const path of walk("unit-records")) {
|
|
574
|
+
const id = path.split("/").at(-1).replace(/\\.(json|md)$/, "");
|
|
575
|
+
const text = context.read(path);
|
|
576
|
+
if (path.endsWith(".json")) {
|
|
577
|
+
const record = JSON.parse(text);
|
|
578
|
+
if (record.result?.commit !== commit.sha) continue;
|
|
579
|
+
const probe = { missing: context.list("nope"), escape: context.list(".."), file: context.list(path), root: context.list(".").includes("unit-records/") };
|
|
580
|
+
return { unit: { id, path, probe }, contract: { id: record.contract } };
|
|
581
|
+
}
|
|
582
|
+
if (text.includes(commit.sha)) return { unit: { id, path } };
|
|
583
|
+
}
|
|
584
|
+
return undefined;
|
|
585
|
+
}
|
|
586
|
+
`;
|
|
587
|
+
writeFiles(root, {
|
|
588
|
+
"plugins/scan.kind.mjs": SCAN,
|
|
589
|
+
"unit-records/U-0100.json": JSON.stringify({ contract: "C-100", result: { commit: sha.c2 } }),
|
|
590
|
+
"unit-records/archive/U-0104.md": `---\nid: U-0104\n---\n\nLanded as ${sha.c4}.\n`,
|
|
591
|
+
});
|
|
592
|
+
commit(["record the units after their commits"]);
|
|
593
|
+
try {
|
|
594
|
+
const doc = await walk("app/server.mjs", { kinds: [KIND, "plugins/scan.kind.mjs"] });
|
|
595
|
+
// c4 carries no trailer; the markdown record in a subdirectory names it.
|
|
596
|
+
expect(node(doc, `commit:${sha.c4}`)).toMatchObject({ trailers: {} });
|
|
597
|
+
expect(doc.edges).toContainEqual({ kind: "produced-by", from: `commit:${sha.c4}`, to: "unit:U-0104" });
|
|
598
|
+
expect(node(doc, "unit:U-0104")).toMatchObject({ data: { path: "unit-records/archive/U-0104.md" } });
|
|
599
|
+
expect(doc.edges).toContainEqual({ kind: "produced-by", from: `commit:${sha.c2}`, to: "unit:U-0100" });
|
|
600
|
+
expect(doc.edges).toContainEqual({ kind: "serves", from: "unit:U-0100", to: "contract:C-100" });
|
|
601
|
+
expect(node(doc, "unit:U-0100")).toMatchObject({ data: { path: "unit-records/U-0100.json", probe: { root: true } } });
|
|
602
|
+
// Not a directory there, or outside the workspace: undefined.
|
|
603
|
+
expect((node(doc, "unit:U-0100") as { data: unknown }).data).toMatchObject({ probe: { missing: undefined, escape: undefined, file: undefined, root: true } });
|
|
604
|
+
expect(node(doc, `commit:${sha.c2b}`) && doc.edges.some((e) => e.kind === "produced-by" && e.from === `commit:${sha.c2b}`)).toBe(false);
|
|
605
|
+
expect(doc.reasons).toEqual([]);
|
|
606
|
+
|
|
607
|
+
// At c4 the records were not written yet: list answers from the tree read, so nothing joins.
|
|
608
|
+
const before = await walk("app/server.mjs", { kinds: [KIND, "plugins/scan.kind.mjs"], at: sha.c4 });
|
|
609
|
+
expect(before.nodes.filter((n) => n.kind === "unit")).toEqual([]);
|
|
610
|
+
expect(before.reasons).toEqual([]);
|
|
611
|
+
} finally {
|
|
612
|
+
git(root, "reset", "-q", "--hard", sha.c4);
|
|
613
|
+
}
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
test("commitJoinsName names a plugin's findings apart from its record kind's name (#2663)", async () => {
|
|
617
|
+
const NAMED = (name: string) => `
|
|
618
|
+
export { recordKind } from "./decision.kind.mjs";
|
|
619
|
+
export const commitJoinsName = ${JSON.stringify(name)};
|
|
620
|
+
export function commitJoins(commit) {
|
|
621
|
+
if (commit.trailers["Unit"]?.[0] !== "U-0002") return undefined;
|
|
622
|
+
return { findings: [{ code: "plugin:studio:contract-moved", message: "moved", refs: [commit.sha] }] };
|
|
623
|
+
}
|
|
624
|
+
`;
|
|
625
|
+
writeFiles(root, { "decisions/studio.kind.mjs": NAMED("studio"), "decisions/other.kind.mjs": NAMED("other") });
|
|
626
|
+
commit(["name the findings"]);
|
|
627
|
+
try {
|
|
628
|
+
const doc = await walk("app/server.mjs", { kinds: ["decisions/studio.kind.mjs"] });
|
|
629
|
+
expect(doc.kinds).toEqual([{ file: "decisions/studio.kind.mjs", name: "studio", records: "decision", joins: "function" }]);
|
|
630
|
+
expect(doc.reasons).toEqual([]);
|
|
631
|
+
expect(doc.nodes).toContainEqual(expect.objectContaining({ kind: "finding", code: "plugin:studio:contract-moved", concerns: [`commit:${sha.c2b}`] }));
|
|
632
|
+
// The decisions still come from the record kind called decision.
|
|
633
|
+
expect(node(doc, "record:decision/dec-001")).toBeDefined();
|
|
634
|
+
|
|
635
|
+
// Under another name, the same code is outside the namespace, and the record kind's name is no longer it either.
|
|
636
|
+
const { doc: other, failed } = await intentGraph({ cwd: root, region: "app/server.mjs", kinds: [join(root, "decisions/other.kind.mjs")] });
|
|
637
|
+
expectValid(other);
|
|
638
|
+
if ("error" in other) throw new Error(other.error.message);
|
|
639
|
+
expect(failed).toBe(true);
|
|
640
|
+
expect(other.reasons).toEqual([{ code: "intent-plugin-failed", message: expect.stringContaining("plugin:other:<code>") }]);
|
|
641
|
+
} finally {
|
|
642
|
+
git(root, "reset", "-q", "--hard", sha.c4);
|
|
643
|
+
}
|
|
644
|
+
});
|
|
645
|
+
|
|
646
|
+
test("a malformed commitJoinsName is kind-invalid (#2663)", async () => {
|
|
647
|
+
writeFiles(root, { "plugins/badname.kind.mjs": `export const commitJoinsName = "a:b";\nexport function commitJoins() {}\n`, "plugins/nameonly.kind.mjs": `export const commitJoinsName = "x";\n` });
|
|
648
|
+
commit(["malformed names"]);
|
|
649
|
+
try {
|
|
650
|
+
for (const [file, text] of [
|
|
651
|
+
["plugins/badname.kind.mjs", "no colon or space"],
|
|
652
|
+
["plugins/nameonly.kind.mjs", "no commitJoins"],
|
|
653
|
+
]) {
|
|
654
|
+
const { doc } = await intentGraph({ cwd: root, region: "app/server.mjs", kinds: [join(root, file)] });
|
|
655
|
+
expectValid(doc);
|
|
656
|
+
expect("error" in doc && doc.error.code).toBe("kind-invalid");
|
|
657
|
+
expect("error" in doc && doc.error.message).toContain(text);
|
|
658
|
+
}
|
|
659
|
+
} finally {
|
|
660
|
+
git(root, "reset", "-q", "--hard", sha.c4);
|
|
661
|
+
}
|
|
662
|
+
});
|
|
663
|
+
});
|