@warpgogol/forge 2.21.6 → 2.21.7
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/AGENTS.md +27 -1
- package/os/adr/adr-0000-template.md +8 -0
- package/os/adr/handlers/validate.test.ts +203 -0
- package/os/adr/handlers/validate.ts +54 -1
- package/os/adr/types.ts +7 -0
- package/os/compass/handlers/compass-inventory-handler.ts +11 -1
- package/os/compass/handlers/compass-inventory.ts +10 -0
- package/os/core/handlers/validate.ts +56 -5
- package/os/naming/naming-convention.ts +9 -0
- package/os/plugin/plugin.module.ts +1 -1
- package/os/rfc/acceptance.ts +133 -4
- package/os/rfc/handlers/implement-stamp.ts +14 -1
- package/os/rfc/handlers/validate-rules-rfc0997.test.ts +394 -0
- package/os/rfc/handlers/validate-rules-rfc1006.test.ts +478 -0
- package/os/rfc/handlers/validate-rules.ts +450 -9
- package/os/rfc/handlers/validate.ts +20 -2
- package/os/rfc/rfc-0000-template.md +24 -8
- package/os/rfc/rfc.module.ts +28 -0
- package/os/rfc/types.ts +72 -6
- package/os/rfc/verification-evidence.ts +5 -4
- package/os/rfc/verification-refresh.test.ts +320 -0
- package/os/rfc/verification-refresh.ts +216 -0
- package/os/session/handlers/save.ts +10 -0
- package/os/spec/spec-validate.test.ts +59 -0
- package/os/spec/spec-validate.ts +6 -4
- package/package.json +2 -1
- package/skills/fo/fo-handoff/SKILL.md +15 -6
- package/skills/fo/fo-idea-audit/SKILL.md +1 -1
- package/skills/fo/fo-idea-create-rfc/SKILL.md +1 -1
- package/skills/fo/fo-idea-create-rfc/acceptance-criteria-standard.md +75 -0
- package/skills/fo/fo-idea-implement/SKILL.md +3 -2
- package/src/compass/contract-registry.ts +25 -6
- package/src/index.ts +1 -1
- package/src/onboarding/doctor.ts +1 -1
- package/src/registry.ts +1 -1
- package/src/tests/acceptance-probe-kinds.test.ts +262 -0
- package/src/tests/plugin-manifest.test.ts +1 -1
- package/src/tests/session-handlers.test.ts +29 -0
- package/src/types/werkstatt-engine-shims.d.ts +0 -21
- package/src/types/werkstatt-shared-shims.d.ts +68 -147
- /package/src/plugin/{ForgePluginManifest.ts → forge-plugin-manifest.ts} +0 -0
|
@@ -0,0 +1,478 @@
|
|
|
1
|
+
import { test, expect, describe } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
evaluateAcceptanceCriteria,
|
|
4
|
+
evaluateDocumentReadiness,
|
|
5
|
+
type AddViolationFn,
|
|
6
|
+
validateSingleRfc,
|
|
7
|
+
} from "./validate-rules.ts";
|
|
8
|
+
import type { ParsedRfc } from "../frontmatter-io.ts";
|
|
9
|
+
import { join } from "node:path";
|
|
10
|
+
import { tmpdir } from "node:os";
|
|
11
|
+
|
|
12
|
+
const testWorkspace = join(tmpdir(), "test-workspace-rfc1006");
|
|
13
|
+
|
|
14
|
+
function makeParsed(
|
|
15
|
+
status: string,
|
|
16
|
+
body: string,
|
|
17
|
+
extraFm: Record<string, unknown> = {},
|
|
18
|
+
): ParsedRfc {
|
|
19
|
+
return {
|
|
20
|
+
frontmatter: {
|
|
21
|
+
id: "RFC-9999",
|
|
22
|
+
title: "Test RFC",
|
|
23
|
+
status,
|
|
24
|
+
kind: "command",
|
|
25
|
+
scope: "workspace",
|
|
26
|
+
owners: ["architecture"],
|
|
27
|
+
createdAt: "2026-09-03",
|
|
28
|
+
updatedAt: "2026-09-03",
|
|
29
|
+
...extraFm,
|
|
30
|
+
},
|
|
31
|
+
body,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const BASE_BODY = `
|
|
36
|
+
# RFC-9999: Test RFC
|
|
37
|
+
|
|
38
|
+
## Context
|
|
39
|
+
|
|
40
|
+
Test context.
|
|
41
|
+
|
|
42
|
+
## Problem
|
|
43
|
+
|
|
44
|
+
Test problem.
|
|
45
|
+
|
|
46
|
+
## Decision
|
|
47
|
+
|
|
48
|
+
Test decision.
|
|
49
|
+
|
|
50
|
+
## Architectural fit
|
|
51
|
+
|
|
52
|
+
Test fit.
|
|
53
|
+
|
|
54
|
+
## Design
|
|
55
|
+
|
|
56
|
+
### CLI surface
|
|
57
|
+
|
|
58
|
+
Test CLI.
|
|
59
|
+
|
|
60
|
+
### TypeScript contracts
|
|
61
|
+
|
|
62
|
+
Test types.
|
|
63
|
+
|
|
64
|
+
### File system responsibilities
|
|
65
|
+
|
|
66
|
+
| Path | Role |
|
|
67
|
+
|---|---|
|
|
68
|
+
| \`test.ts\` | test |
|
|
69
|
+
|
|
70
|
+
### Output format
|
|
71
|
+
|
|
72
|
+
Test output.
|
|
73
|
+
|
|
74
|
+
### Failure modes
|
|
75
|
+
|
|
76
|
+
Test failures.
|
|
77
|
+
|
|
78
|
+
## Rollout
|
|
79
|
+
|
|
80
|
+
Test rollout.
|
|
81
|
+
|
|
82
|
+
## Alternatives considered
|
|
83
|
+
|
|
84
|
+
Test alternatives.
|
|
85
|
+
|
|
86
|
+
## Risks
|
|
87
|
+
|
|
88
|
+
Test risks.
|
|
89
|
+
|
|
90
|
+
DOCUMENT_READINESS_HERE
|
|
91
|
+
|
|
92
|
+
## Acceptance criteria
|
|
93
|
+
|
|
94
|
+
ACCEPTANCE_HERE
|
|
95
|
+
|
|
96
|
+
## Implementation notes for agents
|
|
97
|
+
|
|
98
|
+
Test notes.
|
|
99
|
+
`;
|
|
100
|
+
|
|
101
|
+
function makeViolationsCollector(): {
|
|
102
|
+
add: AddViolationFn;
|
|
103
|
+
violations: { rfcId: string; rule: string; message: string; severity: string }[];
|
|
104
|
+
} {
|
|
105
|
+
const violations: { rfcId: string; rule: string; message: string; severity: string }[] = [];
|
|
106
|
+
const add: AddViolationFn = (rfcId, _file, rule, message, severity = "error") => {
|
|
107
|
+
violations.push({ rfcId, rule, message, severity });
|
|
108
|
+
};
|
|
109
|
+
return { add, violations };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function runValidate(
|
|
113
|
+
parsed: ParsedRfc,
|
|
114
|
+
): Promise<{ rfcId: string; rule: string; message: string; severity: string }[]> {
|
|
115
|
+
const { add, violations } = makeViolationsCollector();
|
|
116
|
+
await validateSingleRfc(
|
|
117
|
+
"rfc-9999-test.md",
|
|
118
|
+
parsed,
|
|
119
|
+
new Map(),
|
|
120
|
+
new Map(),
|
|
121
|
+
new Set(),
|
|
122
|
+
new Set(),
|
|
123
|
+
new Set(Object.keys(parsed.frontmatter)),
|
|
124
|
+
testWorkspace,
|
|
125
|
+
add,
|
|
126
|
+
);
|
|
127
|
+
return violations;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function filterRule(
|
|
131
|
+
violations: { rfcId: string; rule: string; message: string; severity: string }[],
|
|
132
|
+
rule: string,
|
|
133
|
+
): { rfcId: string; rule: string; message: string; severity: string }[] {
|
|
134
|
+
return violations.filter((v) => v.rule === rule);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ─── evaluateDocumentReadiness ─────────────────────────────────────────────
|
|
138
|
+
|
|
139
|
+
describe("evaluateDocumentReadiness (RFC-1006)", () => {
|
|
140
|
+
test("returns empty when no Document readiness section", () => {
|
|
141
|
+
const body = BASE_BODY.replace("DOCUMENT_READINESS_HERE", "").replace(
|
|
142
|
+
"ACCEPTANCE_HERE",
|
|
143
|
+
"- [x] AC-1: test (evidence: test.ts:1)",
|
|
144
|
+
);
|
|
145
|
+
const result = evaluateDocumentReadiness(body);
|
|
146
|
+
expect(result.totalChecked).toBe(0);
|
|
147
|
+
expect(result.totalUnchecked).toBe(0);
|
|
148
|
+
expect(result.criterionIds).toEqual([]);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
test("parses DR-N ids and checked/unchecked items", () => {
|
|
152
|
+
const body = BASE_BODY.replace(
|
|
153
|
+
"DOCUMENT_READINESS_HERE",
|
|
154
|
+
"## Document readiness\n\n- [x] DR-1: alternatives listed (evidence: file: test.md:1)\n- [ ] DR-2: reversibility stated",
|
|
155
|
+
).replace("ACCEPTANCE_HERE", "- [x] AC-1: test (evidence: test.ts:1)");
|
|
156
|
+
const result = evaluateDocumentReadiness(body);
|
|
157
|
+
expect(result.totalChecked).toBe(1);
|
|
158
|
+
expect(result.totalUnchecked).toBe(1);
|
|
159
|
+
expect(result.criterionIds).toEqual(["DR-1", "DR-2"]);
|
|
160
|
+
expect(result.uncheckedLines).toHaveLength(1);
|
|
161
|
+
});
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
// ─── V-38: document readiness completeness ──────────────────────────────────
|
|
165
|
+
|
|
166
|
+
describe("V-38: document readiness completeness (RFC-1006)", () => {
|
|
167
|
+
test("fires for accepted post-cutoff RFC with unchecked DR items", async () => {
|
|
168
|
+
const body = BASE_BODY.replace(
|
|
169
|
+
"DOCUMENT_READINESS_HERE",
|
|
170
|
+
"## Document readiness\n\n- [ ] DR-1: alternatives listed",
|
|
171
|
+
).replace("ACCEPTANCE_HERE", "- [x] AC-1: test (evidence: test.ts:1)\n- [x] AC-2: test2 (evidence: test.ts:2)\n- [x] AC-3: test3 (evidence: test.ts:3)");
|
|
172
|
+
const parsed = makeParsed("accepted", body);
|
|
173
|
+
const violations = await runValidate(parsed);
|
|
174
|
+
const v38 = filterRule(violations, "V-38");
|
|
175
|
+
expect(v38).toHaveLength(1);
|
|
176
|
+
expect(v38[0]!.message).toContain("1 document readiness criteria are unchecked");
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test("does not fire when all DR items are checked", async () => {
|
|
180
|
+
const body = BASE_BODY.replace(
|
|
181
|
+
"DOCUMENT_READINESS_HERE",
|
|
182
|
+
"## Document readiness\n\n- [x] DR-1: alternatives listed (evidence: file: test.md:1)",
|
|
183
|
+
).replace("ACCEPTANCE_HERE", "- [x] AC-1: test (evidence: test.ts:1)\n- [x] AC-2: test2 (evidence: test.ts:2)\n- [x] AC-3: test3 (evidence: test.ts:3)");
|
|
184
|
+
const parsed = makeParsed("accepted", body);
|
|
185
|
+
const violations = await runValidate(parsed);
|
|
186
|
+
const v38 = filterRule(violations, "V-38");
|
|
187
|
+
expect(v38).toHaveLength(0);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test("does not fire for pre-cutoff RFCs", async () => {
|
|
191
|
+
const body = BASE_BODY.replace(
|
|
192
|
+
"DOCUMENT_READINESS_HERE",
|
|
193
|
+
"## Document readiness\n\n- [ ] DR-1: alternatives listed",
|
|
194
|
+
).replace("ACCEPTANCE_HERE", "- [x] AC-1: test (evidence: test.ts:1)\n- [x] AC-2: test2 (evidence: test.ts:2)\n- [x] AC-3: test3 (evidence: test.ts:3)");
|
|
195
|
+
const parsed = makeParsed("accepted", body, { createdAt: "2026-08-01" });
|
|
196
|
+
const violations = await runValidate(parsed);
|
|
197
|
+
const v38 = filterRule(violations, "V-38");
|
|
198
|
+
expect(v38).toHaveLength(0);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
test("does not fire for draft RFCs (warning only)", async () => {
|
|
202
|
+
const body = BASE_BODY.replace(
|
|
203
|
+
"DOCUMENT_READINESS_HERE",
|
|
204
|
+
"## Document readiness\n\n- [ ] DR-1: alternatives listed",
|
|
205
|
+
).replace("ACCEPTANCE_HERE", "- [x] AC-1: test (evidence: test.ts:1)\n- [x] AC-2: test2 (evidence: test.ts:2)\n- [x] AC-3: test3 (evidence: test.ts:3)");
|
|
206
|
+
const parsed = makeParsed("draft", body);
|
|
207
|
+
const violations = await runValidate(parsed);
|
|
208
|
+
const v38 = filterRule(violations, "V-38");
|
|
209
|
+
expect(v38).toHaveLength(0);
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
// ─── V-39: non-atomic criterion ────────────────────────────────────────────
|
|
214
|
+
|
|
215
|
+
describe("V-39: non-atomic criterion (RFC-1006)", () => {
|
|
216
|
+
test("detects SHALL ... and <verb> pattern", () => {
|
|
217
|
+
const body = BASE_BODY.replace("DOCUMENT_READINESS_HERE", "").replace(
|
|
218
|
+
"ACCEPTANCE_HERE",
|
|
219
|
+
"- [x] AC-1: THE system SHALL validate input and persist results (evidence: test.ts:1)",
|
|
220
|
+
);
|
|
221
|
+
const result = evaluateAcceptanceCriteria(body);
|
|
222
|
+
expect(result.nonAtomicViolations).toHaveLength(1);
|
|
223
|
+
expect(result.nonAtomicViolations[0]!.acId).toBe("AC-1");
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
test("does not fire for single-verb criteria", () => {
|
|
227
|
+
const body = BASE_BODY.replace("DOCUMENT_READINESS_HERE", "").replace(
|
|
228
|
+
"ACCEPTANCE_HERE",
|
|
229
|
+
"- [x] AC-1: THE system SHALL validate input (evidence: test.ts:1)",
|
|
230
|
+
);
|
|
231
|
+
const result = evaluateAcceptanceCriteria(body);
|
|
232
|
+
expect(result.nonAtomicViolations).toHaveLength(0);
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
test("does not fire for 'and' inside noun phrases", () => {
|
|
236
|
+
const body = BASE_BODY.replace("DOCUMENT_READINESS_HERE", "").replace(
|
|
237
|
+
"ACCEPTANCE_HERE",
|
|
238
|
+
"- [x] AC-1: THE system SHALL process input and output streams (evidence: test.ts:1)",
|
|
239
|
+
);
|
|
240
|
+
const result = evaluateAcceptanceCriteria(body);
|
|
241
|
+
// "streams" is not in the verb list, so this should not fire
|
|
242
|
+
expect(result.nonAtomicViolations).toHaveLength(0);
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
test("fires as error for accepted post-cutoff RFCs", async () => {
|
|
246
|
+
const body = BASE_BODY.replace("DOCUMENT_READINESS_HERE", "").replace(
|
|
247
|
+
"ACCEPTANCE_HERE",
|
|
248
|
+
"- [x] AC-1: THE system SHALL validate input and persist results (evidence: test.ts:1)\n- [x] AC-2: test2 (evidence: test.ts:2)\n- [x] AC-3: test3 (evidence: test.ts:3)",
|
|
249
|
+
);
|
|
250
|
+
const parsed = makeParsed("accepted", body);
|
|
251
|
+
const violations = await runValidate(parsed);
|
|
252
|
+
const v39 = filterRule(violations, "V-39");
|
|
253
|
+
expect(v39).toHaveLength(1);
|
|
254
|
+
expect(v39[0]!.severity).toBe("error");
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
test("fires as warning for draft post-cutoff RFCs", async () => {
|
|
258
|
+
const body = BASE_BODY.replace("DOCUMENT_READINESS_HERE", "").replace(
|
|
259
|
+
"ACCEPTANCE_HERE",
|
|
260
|
+
"- [x] AC-1: THE system SHALL validate input and persist results (evidence: test.ts:1)\n- [x] AC-2: test2 (evidence: test.ts:2)\n- [x] AC-3: test3 (evidence: test.ts:3)",
|
|
261
|
+
);
|
|
262
|
+
const parsed = makeParsed("draft", body);
|
|
263
|
+
const violations = await runValidate(parsed);
|
|
264
|
+
const v39 = filterRule(violations, "V-39");
|
|
265
|
+
expect(v39).toHaveLength(1);
|
|
266
|
+
expect(v39[0]!.severity).toBe("warning");
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
test("does not fire for pre-cutoff RFCs", async () => {
|
|
270
|
+
const body = BASE_BODY.replace("DOCUMENT_READINESS_HERE", "").replace(
|
|
271
|
+
"ACCEPTANCE_HERE",
|
|
272
|
+
"- [x] AC-1: THE system SHALL validate input and persist results (evidence: test.ts:1)\n- [x] AC-2: test2 (evidence: test.ts:2)\n- [x] AC-3: test3 (evidence: test.ts:3)",
|
|
273
|
+
);
|
|
274
|
+
const parsed = makeParsed("accepted", body, { createdAt: "2026-08-01" });
|
|
275
|
+
const violations = await runValidate(parsed);
|
|
276
|
+
const v39 = filterRule(violations, "V-39");
|
|
277
|
+
expect(v39).toHaveLength(0);
|
|
278
|
+
});
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
// ─── V-40: unbounded quantity ──────────────────────────────────────────────
|
|
282
|
+
|
|
283
|
+
describe("V-40: unbounded quantity (RFC-1006)", () => {
|
|
284
|
+
test("detects 'fast' trigger", () => {
|
|
285
|
+
const body = BASE_BODY.replace("DOCUMENT_READINESS_HERE", "").replace(
|
|
286
|
+
"ACCEPTANCE_HERE",
|
|
287
|
+
"- [x] AC-1: THE system SHALL respond fast (evidence: test.ts:1)",
|
|
288
|
+
);
|
|
289
|
+
const result = evaluateAcceptanceCriteria(body);
|
|
290
|
+
expect(result.unboundedQuantityViolations).toHaveLength(1);
|
|
291
|
+
expect(result.unboundedQuantityViolations[0]!.trigger).toBe("fast");
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
test("detects 'scalable' trigger", () => {
|
|
295
|
+
const body = BASE_BODY.replace("DOCUMENT_READINESS_HERE", "").replace(
|
|
296
|
+
"ACCEPTANCE_HERE",
|
|
297
|
+
"- [x] AC-1: THE system SHALL be scalable (evidence: test.ts:1)",
|
|
298
|
+
);
|
|
299
|
+
const result = evaluateAcceptanceCriteria(body);
|
|
300
|
+
expect(result.unboundedQuantityViolations).toHaveLength(1);
|
|
301
|
+
expect(result.unboundedQuantityViolations[0]!.trigger).toBe("scalable");
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
test("does not fire for specific numbers", () => {
|
|
305
|
+
const body = BASE_BODY.replace("DOCUMENT_READINESS_HERE", "").replace(
|
|
306
|
+
"ACCEPTANCE_HERE",
|
|
307
|
+
"- [x] AC-1: THE system SHALL respond within 5 minutes (evidence: test.ts:1)",
|
|
308
|
+
);
|
|
309
|
+
const result = evaluateAcceptanceCriteria(body);
|
|
310
|
+
expect(result.unboundedQuantityViolations).toHaveLength(0);
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
test("fires as error for accepted post-cutoff RFCs", async () => {
|
|
314
|
+
const body = BASE_BODY.replace("DOCUMENT_READINESS_HERE", "").replace(
|
|
315
|
+
"ACCEPTANCE_HERE",
|
|
316
|
+
"- [x] AC-1: THE system SHALL respond fast (evidence: test.ts:1)\n- [x] AC-2: test2 (evidence: test.ts:2)\n- [x] AC-3: test3 (evidence: test.ts:3)",
|
|
317
|
+
);
|
|
318
|
+
const parsed = makeParsed("accepted", body);
|
|
319
|
+
const violations = await runValidate(parsed);
|
|
320
|
+
const v40 = filterRule(violations, "V-40");
|
|
321
|
+
expect(v40).toHaveLength(1);
|
|
322
|
+
expect(v40[0]!.severity).toBe("error");
|
|
323
|
+
expect(v40[0]!.message).toContain("fast");
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
test("does not fire for pre-cutoff RFCs", async () => {
|
|
327
|
+
const body = BASE_BODY.replace("DOCUMENT_READINESS_HERE", "").replace(
|
|
328
|
+
"ACCEPTANCE_HERE",
|
|
329
|
+
"- [x] AC-1: THE system SHALL respond fast (evidence: test.ts:1)\n- [x] AC-2: test2 (evidence: test.ts:2)\n- [x] AC-3: test3 (evidence: test.ts:3)",
|
|
330
|
+
);
|
|
331
|
+
const parsed = makeParsed("accepted", body, { createdAt: "2026-08-01" });
|
|
332
|
+
const violations = await runValidate(parsed);
|
|
333
|
+
const v40 = filterRule(violations, "V-40");
|
|
334
|
+
expect(v40).toHaveLength(0);
|
|
335
|
+
});
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
// ─── V-41: weasel verb ──────────────────────────────────────────────────────
|
|
339
|
+
|
|
340
|
+
describe("V-41: weasel verb (RFC-1006)", () => {
|
|
341
|
+
test("detects 'handle gracefully'", () => {
|
|
342
|
+
const body = BASE_BODY.replace("DOCUMENT_READINESS_HERE", "").replace(
|
|
343
|
+
"ACCEPTANCE_HERE",
|
|
344
|
+
"- [x] AC-1: THE system SHALL handle gracefully invalid input (evidence: test.ts:1)",
|
|
345
|
+
);
|
|
346
|
+
const result = evaluateAcceptanceCriteria(body);
|
|
347
|
+
expect(result.weaselVerbViolations).toHaveLength(1);
|
|
348
|
+
expect(result.weaselVerbViolations[0]!.trigger).toBe("handle gracefully");
|
|
349
|
+
});
|
|
350
|
+
|
|
351
|
+
test("detects 'robust'", () => {
|
|
352
|
+
const body = BASE_BODY.replace("DOCUMENT_READINESS_HERE", "").replace(
|
|
353
|
+
"ACCEPTANCE_HERE",
|
|
354
|
+
"- [x] AC-1: THE system SHALL be robust (evidence: test.ts:1)",
|
|
355
|
+
);
|
|
356
|
+
const result = evaluateAcceptanceCriteria(body);
|
|
357
|
+
expect(result.weaselVerbViolations).toHaveLength(1);
|
|
358
|
+
expect(result.weaselVerbViolations[0]!.trigger).toBe("robust");
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
test("does not fire for specific behavior", () => {
|
|
362
|
+
const body = BASE_BODY.replace("DOCUMENT_READINESS_HERE", "").replace(
|
|
363
|
+
"ACCEPTANCE_HERE",
|
|
364
|
+
"- [x] AC-1: THE system SHALL return exit code 1 for invalid input (evidence: test.ts:1)",
|
|
365
|
+
);
|
|
366
|
+
const result = evaluateAcceptanceCriteria(body);
|
|
367
|
+
expect(result.weaselVerbViolations).toHaveLength(0);
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
test("fires as error for accepted post-cutoff RFCs", async () => {
|
|
371
|
+
const body = BASE_BODY.replace("DOCUMENT_READINESS_HERE", "").replace(
|
|
372
|
+
"ACCEPTANCE_HERE",
|
|
373
|
+
"- [x] AC-1: THE system SHALL be robust (evidence: test.ts:1)\n- [x] AC-2: test2 (evidence: test.ts:2)\n- [x] AC-3: test3 (evidence: test.ts:3)",
|
|
374
|
+
);
|
|
375
|
+
const parsed = makeParsed("accepted", body);
|
|
376
|
+
const violations = await runValidate(parsed);
|
|
377
|
+
const v41 = filterRule(violations, "V-41");
|
|
378
|
+
expect(v41).toHaveLength(1);
|
|
379
|
+
expect(v41[0]!.severity).toBe("error");
|
|
380
|
+
expect(v41[0]!.message).toContain("robust");
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
test("does not fire for pre-cutoff RFCs", async () => {
|
|
384
|
+
const body = BASE_BODY.replace("DOCUMENT_READINESS_HERE", "").replace(
|
|
385
|
+
"ACCEPTANCE_HERE",
|
|
386
|
+
"- [x] AC-1: THE system SHALL be robust (evidence: test.ts:1)\n- [x] AC-2: test2 (evidence: test.ts:2)\n- [x] AC-3: test3 (evidence: test.ts:3)",
|
|
387
|
+
);
|
|
388
|
+
const parsed = makeParsed("accepted", body, { createdAt: "2026-08-01" });
|
|
389
|
+
const violations = await runValidate(parsed);
|
|
390
|
+
const v41 = filterRule(violations, "V-41");
|
|
391
|
+
expect(v41).toHaveLength(0);
|
|
392
|
+
});
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
// ─── V-42: criterion versioning ─────────────────────────────────────────────
|
|
396
|
+
|
|
397
|
+
describe("V-42: criterion versioning (RFC-1006)", () => {
|
|
398
|
+
test("superseded criteria are excluded from unchecked count", () => {
|
|
399
|
+
const body = BASE_BODY.replace("DOCUMENT_READINESS_HERE", "").replace(
|
|
400
|
+
"ACCEPTANCE_HERE",
|
|
401
|
+
"> Superseded AC-1 (2026-09-15): too broad\n- [ ] AC-1: old criterion\n- [x] AC-1a: new criterion (evidence: test.ts:1)",
|
|
402
|
+
);
|
|
403
|
+
const result = evaluateAcceptanceCriteria(body);
|
|
404
|
+
expect(result.supersededIds).toEqual(["AC-1"]);
|
|
405
|
+
expect(result.totalUnchecked).toBe(0);
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
test("superseded criteria without annotation still count as unchecked", () => {
|
|
409
|
+
const body = BASE_BODY.replace("DOCUMENT_READINESS_HERE", "").replace(
|
|
410
|
+
"ACCEPTANCE_HERE",
|
|
411
|
+
"- [ ] AC-1: old criterion\n- [x] AC-1a: new criterion (evidence: test.ts:1)",
|
|
412
|
+
);
|
|
413
|
+
const result = evaluateAcceptanceCriteria(body);
|
|
414
|
+
expect(result.supersededIds).toEqual([]);
|
|
415
|
+
expect(result.totalUnchecked).toBe(1);
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
test("malformed supersession annotation is detected", () => {
|
|
419
|
+
const body = BASE_BODY.replace("DOCUMENT_READINESS_HERE", "").replace(
|
|
420
|
+
"ACCEPTANCE_HERE",
|
|
421
|
+
"> Superseded AC-1 (not-a-date): too broad\n- [ ] AC-1: old criterion\n- [x] AC-1a: new (evidence: test.ts:1)",
|
|
422
|
+
);
|
|
423
|
+
const result = evaluateAcceptanceCriteria(body);
|
|
424
|
+
expect(result.malformedSupersessionAnnotations).toHaveLength(1);
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
test("well-formed supersession annotation is not flagged", () => {
|
|
428
|
+
const body = BASE_BODY.replace("DOCUMENT_READINESS_HERE", "").replace(
|
|
429
|
+
"ACCEPTANCE_HERE",
|
|
430
|
+
"> Superseded AC-1 (2026-09-15): too broad\n- [ ] AC-1: old criterion\n- [x] AC-1a: new (evidence: test.ts:1)",
|
|
431
|
+
);
|
|
432
|
+
const result = evaluateAcceptanceCriteria(body);
|
|
433
|
+
expect(result.malformedSupersessionAnnotations).toHaveLength(0);
|
|
434
|
+
});
|
|
435
|
+
|
|
436
|
+
test("V-42 fires for malformed annotation on post-cutoff RFCs", async () => {
|
|
437
|
+
const body = BASE_BODY.replace("DOCUMENT_READINESS_HERE", "").replace(
|
|
438
|
+
"ACCEPTANCE_HERE",
|
|
439
|
+
"> Superseded AC-1 (not-a-date): too broad\n- [ ] AC-1: old criterion\n- [x] AC-1a: new (evidence: test.ts:1)\n- [x] AC-2: test2 (evidence: test.ts:2)\n- [x] AC-3: test3 (evidence: test.ts:3)",
|
|
440
|
+
);
|
|
441
|
+
const parsed = makeParsed("accepted", body);
|
|
442
|
+
const violations = await runValidate(parsed);
|
|
443
|
+
const v42 = filterRule(violations, "V-42");
|
|
444
|
+
expect(v42).toHaveLength(1);
|
|
445
|
+
expect(v42[0]!.severity).toBe("error");
|
|
446
|
+
});
|
|
447
|
+
|
|
448
|
+
test("V-42 does not fire for pre-cutoff RFCs", async () => {
|
|
449
|
+
const body = BASE_BODY.replace("DOCUMENT_READINESS_HERE", "").replace(
|
|
450
|
+
"ACCEPTANCE_HERE",
|
|
451
|
+
"> Superseded AC-1 (not-a-date): too broad\n- [ ] AC-1: old criterion\n- [x] AC-1a: new (evidence: test.ts:1)\n- [x] AC-2: test2 (evidence: test.ts:2)\n- [x] AC-3: test3 (evidence: test.ts:3)",
|
|
452
|
+
);
|
|
453
|
+
const parsed = makeParsed("accepted", body, { createdAt: "2026-08-01" });
|
|
454
|
+
const violations = await runValidate(parsed);
|
|
455
|
+
const v42 = filterRule(violations, "V-42");
|
|
456
|
+
expect(v42).toHaveLength(0);
|
|
457
|
+
});
|
|
458
|
+
});
|
|
459
|
+
|
|
460
|
+
// ─── Pre-cutoff exemption: no V-38..V-42 ────────────────────────────────────
|
|
461
|
+
|
|
462
|
+
describe("Pre-cutoff exemption (RFC-1006)", () => {
|
|
463
|
+
test("pre-cutoff RFCs produce no V-38..V-42 diagnostics", async () => {
|
|
464
|
+
const body = BASE_BODY.replace(
|
|
465
|
+
"DOCUMENT_READINESS_HERE",
|
|
466
|
+
"## Document readiness\n\n- [ ] DR-1: unchecked",
|
|
467
|
+
).replace(
|
|
468
|
+
"ACCEPTANCE_HERE",
|
|
469
|
+
"- [x] AC-1: THE system SHALL handle gracefully fast input and persist results (evidence: test.ts:1)\n- [x] AC-2: test2 (evidence: test.ts:2)\n- [x] AC-3: test3 (evidence: test.ts:3)",
|
|
470
|
+
);
|
|
471
|
+
const parsed = makeParsed("accepted", body, { createdAt: "2026-08-01" });
|
|
472
|
+
const violations = await runValidate(parsed);
|
|
473
|
+
const newRules = violations.filter(
|
|
474
|
+
(v) => ["V-38", "V-39", "V-40", "V-41", "V-42"].includes(v.rule),
|
|
475
|
+
);
|
|
476
|
+
expect(newRules).toHaveLength(0);
|
|
477
|
+
});
|
|
478
|
+
});
|