@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.
Files changed (41) hide show
  1. package/AGENTS.md +27 -1
  2. package/os/adr/adr-0000-template.md +8 -0
  3. package/os/adr/handlers/validate.test.ts +203 -0
  4. package/os/adr/handlers/validate.ts +54 -1
  5. package/os/adr/types.ts +7 -0
  6. package/os/compass/handlers/compass-inventory-handler.ts +11 -1
  7. package/os/compass/handlers/compass-inventory.ts +10 -0
  8. package/os/core/handlers/validate.ts +56 -5
  9. package/os/naming/naming-convention.ts +9 -0
  10. package/os/plugin/plugin.module.ts +1 -1
  11. package/os/rfc/acceptance.ts +133 -4
  12. package/os/rfc/handlers/implement-stamp.ts +14 -1
  13. package/os/rfc/handlers/validate-rules-rfc0997.test.ts +394 -0
  14. package/os/rfc/handlers/validate-rules-rfc1006.test.ts +478 -0
  15. package/os/rfc/handlers/validate-rules.ts +450 -9
  16. package/os/rfc/handlers/validate.ts +20 -2
  17. package/os/rfc/rfc-0000-template.md +24 -8
  18. package/os/rfc/rfc.module.ts +28 -0
  19. package/os/rfc/types.ts +72 -6
  20. package/os/rfc/verification-evidence.ts +5 -4
  21. package/os/rfc/verification-refresh.test.ts +320 -0
  22. package/os/rfc/verification-refresh.ts +216 -0
  23. package/os/session/handlers/save.ts +10 -0
  24. package/os/spec/spec-validate.test.ts +59 -0
  25. package/os/spec/spec-validate.ts +6 -4
  26. package/package.json +2 -1
  27. package/skills/fo/fo-handoff/SKILL.md +15 -6
  28. package/skills/fo/fo-idea-audit/SKILL.md +1 -1
  29. package/skills/fo/fo-idea-create-rfc/SKILL.md +1 -1
  30. package/skills/fo/fo-idea-create-rfc/acceptance-criteria-standard.md +75 -0
  31. package/skills/fo/fo-idea-implement/SKILL.md +3 -2
  32. package/src/compass/contract-registry.ts +25 -6
  33. package/src/index.ts +1 -1
  34. package/src/onboarding/doctor.ts +1 -1
  35. package/src/registry.ts +1 -1
  36. package/src/tests/acceptance-probe-kinds.test.ts +262 -0
  37. package/src/tests/plugin-manifest.test.ts +1 -1
  38. package/src/tests/session-handlers.test.ts +29 -0
  39. package/src/types/werkstatt-engine-shims.d.ts +0 -21
  40. package/src/types/werkstatt-shared-shims.d.ts +68 -147
  41. /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
+ });