@warpgogol/forge 2.21.5 → 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 (43) hide show
  1. package/AGENTS.md +27 -1
  2. package/README.md +93 -0
  3. package/README.uk.md +93 -0
  4. package/os/adr/adr-0000-template.md +8 -0
  5. package/os/adr/handlers/validate.test.ts +203 -0
  6. package/os/adr/handlers/validate.ts +54 -1
  7. package/os/adr/types.ts +7 -0
  8. package/os/compass/handlers/compass-inventory-handler.ts +11 -1
  9. package/os/compass/handlers/compass-inventory.ts +10 -0
  10. package/os/core/handlers/validate.ts +56 -5
  11. package/os/naming/naming-convention.ts +9 -0
  12. package/os/plugin/plugin.module.ts +1 -1
  13. package/os/rfc/acceptance.ts +133 -4
  14. package/os/rfc/handlers/implement-stamp.ts +14 -1
  15. package/os/rfc/handlers/validate-rules-rfc0997.test.ts +394 -0
  16. package/os/rfc/handlers/validate-rules-rfc1006.test.ts +478 -0
  17. package/os/rfc/handlers/validate-rules.ts +450 -9
  18. package/os/rfc/handlers/validate.ts +20 -2
  19. package/os/rfc/rfc-0000-template.md +24 -8
  20. package/os/rfc/rfc.module.ts +28 -0
  21. package/os/rfc/types.ts +72 -6
  22. package/os/rfc/verification-evidence.ts +5 -4
  23. package/os/rfc/verification-refresh.test.ts +320 -0
  24. package/os/rfc/verification-refresh.ts +216 -0
  25. package/os/session/handlers/save.ts +10 -0
  26. package/os/spec/spec-validate.test.ts +59 -0
  27. package/os/spec/spec-validate.ts +6 -4
  28. package/package.json +2 -1
  29. package/skills/fo/fo-handoff/SKILL.md +15 -6
  30. package/skills/fo/fo-idea-audit/SKILL.md +1 -1
  31. package/skills/fo/fo-idea-create-rfc/SKILL.md +1 -1
  32. package/skills/fo/fo-idea-create-rfc/acceptance-criteria-standard.md +75 -0
  33. package/skills/fo/fo-idea-implement/SKILL.md +3 -2
  34. package/src/compass/contract-registry.ts +25 -6
  35. package/src/index.ts +1 -1
  36. package/src/onboarding/doctor.ts +1 -1
  37. package/src/registry.ts +1 -1
  38. package/src/tests/acceptance-probe-kinds.test.ts +262 -0
  39. package/src/tests/plugin-manifest.test.ts +1 -1
  40. package/src/tests/session-handlers.test.ts +29 -0
  41. package/src/types/werkstatt-engine-shims.d.ts +0 -21
  42. package/src/types/werkstatt-shared-shims.d.ts +68 -147
  43. /package/src/plugin/{ForgePluginManifest.ts → forge-plugin-manifest.ts} +0 -0
@@ -0,0 +1,394 @@
1
+ import { test, expect, describe } from "vitest";
2
+ import {
3
+ evaluateAcceptanceCriteria,
4
+ computeProbeCoverage,
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-rfc0997");
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-01",
28
+ updatedAt: "2026-09-01",
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
+ ## Acceptance criteria
91
+
92
+ ACCEPTANCE_HERE
93
+
94
+ ## Implementation notes for agents
95
+
96
+ Test notes.
97
+ `;
98
+
99
+ function makeViolationsCollector(): {
100
+ add: AddViolationFn;
101
+ violations: { rfcId: string; rule: string; message: string; severity: string }[];
102
+ } {
103
+ const violations: { rfcId: string; rule: string; message: string; severity: string }[] = [];
104
+ const add: AddViolationFn = (rfcId, _file, rule, message, severity = "error") => {
105
+ violations.push({ rfcId, rule, message, severity });
106
+ };
107
+ return { add, violations };
108
+ }
109
+
110
+ async function runValidate(
111
+ parsed: ParsedRfc,
112
+ ): Promise<{ rfcId: string; rule: string; message: string; severity: string }[]> {
113
+ const { add, violations } = makeViolationsCollector();
114
+ await validateSingleRfc(
115
+ "rfc-9999-test.md",
116
+ parsed,
117
+ new Map(),
118
+ new Map(),
119
+ new Set(),
120
+ new Set(),
121
+ new Set(Object.keys(parsed.frontmatter)),
122
+ testWorkspace,
123
+ add,
124
+ );
125
+ return violations;
126
+ }
127
+
128
+ function filterRule(
129
+ violations: { rfcId: string; rule: string; message: string; severity: string }[],
130
+ rule: string,
131
+ ): { rfcId: string; rule: string; message: string; severity: string }[] {
132
+ return violations.filter((v) => v.rule === rule);
133
+ }
134
+
135
+ // ─── evaluateAcceptanceCriteria: AC-N parsing ──────────────────────────────
136
+
137
+ describe("evaluateAcceptanceCriteria: AC-N id parsing (RFC-0997)", () => {
138
+ test("parses AC-N ids from checklist lines", () => {
139
+ const body = BASE_BODY.replace(
140
+ "ACCEPTANCE_HERE",
141
+ "- [x] AC-1: first criterion (evidence: test.ts:1)\n- [ ] AC-2: second criterion\n- [x] AC-3: third (evidence: test.ts:2)",
142
+ );
143
+ const result = evaluateAcceptanceCriteria(body);
144
+ expect(result.criterionIds).toEqual(["AC-1", "AC-2", "AC-3"]);
145
+ expect(result.duplicateCriterionIds).toEqual([]);
146
+ expect(result.linesWithoutId).toEqual([]);
147
+ });
148
+
149
+ test("detects duplicate AC-N ids", () => {
150
+ const body = BASE_BODY.replace(
151
+ "ACCEPTANCE_HERE",
152
+ "- [x] AC-1: first (evidence: test.ts:1)\n- [ ] AC-1: duplicate\n- [x] AC-2: third (evidence: test.ts:2)",
153
+ );
154
+ const result = evaluateAcceptanceCriteria(body);
155
+ expect(result.criterionIds).toEqual(["AC-1", "AC-2"]);
156
+ expect(result.duplicateCriterionIds).toEqual(["AC-1"]);
157
+ });
158
+
159
+ test("detects checklist lines without AC-N prefix", () => {
160
+ const body = BASE_BODY.replace(
161
+ "ACCEPTANCE_HERE",
162
+ "- [x] AC-1: first (evidence: test.ts:1)\n- [ ] no id here\n- [x] AC-2: third (evidence: test.ts:2)",
163
+ );
164
+ const result = evaluateAcceptanceCriteria(body);
165
+ expect(result.linesWithoutId).toEqual(["- [ ] no id here"]);
166
+ });
167
+ });
168
+
169
+ // ─── computeProbeCoverage ───────────────────────────────────────────────────
170
+
171
+ describe("computeProbeCoverage (RFC-0997)", () => {
172
+ test("full coverage when all criteria have probes", () => {
173
+ const ids = ["AC-1", "AC-2", "AC-3"];
174
+ const acceptance = [
175
+ { probe: "file-exists", path: "a.ts", criterion: "AC-1" },
176
+ { probe: "file-exists", path: "b.ts", criterion: "AC-2" },
177
+ { probe: "file-exists", path: "c.ts", criterion: "AC-3" },
178
+ ];
179
+ const report = computeProbeCoverage(ids, acceptance);
180
+ expect(report.totalCriteria).toBe(3);
181
+ expect(report.probeBackedCriteria).toBe(3);
182
+ expect(report.coverageRatio).toBe(1);
183
+ expect(report.uncoveredCriteria).toEqual([]);
184
+ expect(report.unboundProbes).toEqual([]);
185
+ });
186
+
187
+ test("partial coverage when some criteria lack probes", () => {
188
+ const ids = ["AC-1", "AC-2", "AC-3"];
189
+ const acceptance = [
190
+ { probe: "file-exists", path: "a.ts", criterion: "AC-1" },
191
+ { probe: "file-exists", path: "b.ts", criterion: "AC-2" },
192
+ ];
193
+ const report = computeProbeCoverage(ids, acceptance);
194
+ expect(report.totalCriteria).toBe(3);
195
+ expect(report.probeBackedCriteria).toBe(2);
196
+ expect(report.coverageRatio).toBeCloseTo(0.667, 2);
197
+ expect(report.uncoveredCriteria).toEqual(["AC-3"]);
198
+ });
199
+
200
+ test("unbound probe references nonexistent criterion", () => {
201
+ const ids = ["AC-1"];
202
+ const acceptance = [
203
+ { probe: "file-exists", path: "a.ts", criterion: "AC-1" },
204
+ { probe: "file-exists", path: "b.ts", criterion: "AC-99" },
205
+ ];
206
+ const report = computeProbeCoverage(ids, acceptance);
207
+ expect(report.unboundProbes).toEqual(["AC-99"]);
208
+ });
209
+
210
+ test("probe without criterion field is unbound", () => {
211
+ const ids = ["AC-1"];
212
+ const acceptance = [{ probe: "file-exists", path: "a.ts" }];
213
+ const report = computeProbeCoverage(ids, acceptance);
214
+ expect(report.unboundProbes).toEqual(["(missing)"]);
215
+ expect(report.probeBackedCriteria).toBe(0);
216
+ });
217
+
218
+ test("zero criteria and zero probes gives ratio 0", () => {
219
+ const report = computeProbeCoverage([], []);
220
+ expect(report.totalCriteria).toBe(0);
221
+ expect(report.coverageRatio).toBe(0);
222
+ });
223
+ });
224
+
225
+ // ─── V-35: probe→criterion referential integrity ───────────────────────────
226
+
227
+ describe("V-35: probe→criterion referential integrity (RFC-0997)", () => {
228
+ test("post-cutoff probe without criterion fires V-35", async () => {
229
+ const body = BASE_BODY.replace(
230
+ "ACCEPTANCE_HERE",
231
+ "- [x] AC-1: first (evidence: test.ts:1)\n- [ ] AC-2: second\n- [x] AC-3: third (evidence: test.ts:2)",
232
+ );
233
+ const parsed = makeParsed("accepted", body, {
234
+ acceptance: [{ probe: "file-exists", path: "a.ts" }],
235
+ });
236
+ const violations = await runValidate(parsed);
237
+ const v35 = filterRule(violations, "V-35");
238
+ expect(v35.length).toBeGreaterThanOrEqual(1);
239
+ expect(v35[0]!.message).toContain('lacks required "criterion"');
240
+ });
241
+
242
+ test("post-cutoff probe with nonexistent criterion fires V-35", async () => {
243
+ const body = BASE_BODY.replace(
244
+ "ACCEPTANCE_HERE",
245
+ "- [x] AC-1: first (evidence: test.ts:1)\n- [ ] AC-2: second\n- [x] AC-3: third (evidence: test.ts:2)",
246
+ );
247
+ const parsed = makeParsed("accepted", body, {
248
+ acceptance: [{ probe: "file-exists", path: "a.ts", criterion: "AC-99" }],
249
+ });
250
+ const violations = await runValidate(parsed);
251
+ const v35 = filterRule(violations, "V-35");
252
+ expect(v35.length).toBeGreaterThanOrEqual(1);
253
+ expect(v35[0]!.message).toContain("does not exist");
254
+ });
255
+
256
+ test("post-cutoff probe with valid criterion does NOT fire V-35", async () => {
257
+ const body = BASE_BODY.replace(
258
+ "ACCEPTANCE_HERE",
259
+ "- [x] AC-1: first (evidence: test.ts:1)\n- [ ] AC-2: second\n- [x] AC-3: third (evidence: test.ts:2)",
260
+ );
261
+ const parsed = makeParsed("accepted", body, {
262
+ acceptance: [{ probe: "file-exists", path: "a.ts", criterion: "AC-1" }],
263
+ });
264
+ const violations = await runValidate(parsed);
265
+ const v35 = filterRule(violations, "V-35");
266
+ expect(v35).toHaveLength(0);
267
+ });
268
+
269
+ test("pre-cutoff probe without criterion does NOT fire V-35", async () => {
270
+ const body = BASE_BODY.replace(
271
+ "ACCEPTANCE_HERE",
272
+ "- [x] AC-1: first (evidence: test.ts:1)\n- [ ] AC-2: second\n- [x] AC-3: third (evidence: test.ts:2)",
273
+ );
274
+ const parsed = makeParsed("accepted", body, {
275
+ createdAt: "2026-01-01",
276
+ acceptance: [{ probe: "file-exists", path: "a.ts" }],
277
+ });
278
+ const violations = await runValidate(parsed);
279
+ const v35 = filterRule(violations, "V-35");
280
+ expect(v35).toHaveLength(0);
281
+ });
282
+ });
283
+
284
+ // ─── V-36: criterion identifier discipline ─────────────────────────────────
285
+
286
+ describe("V-36: criterion identifier discipline (RFC-0997)", () => {
287
+ test("post-cutoff line without AC-N fires V-36", async () => {
288
+ const body = BASE_BODY.replace(
289
+ "ACCEPTANCE_HERE",
290
+ "- [x] AC-1: first (evidence: test.ts:1)\n- [ ] no id here\n- [x] AC-3: third (evidence: test.ts:2)",
291
+ );
292
+ const parsed = makeParsed("accepted", body);
293
+ const violations = await runValidate(parsed);
294
+ const v36 = filterRule(violations, "V-36");
295
+ expect(v36.length).toBeGreaterThanOrEqual(1);
296
+ expect(v36[0]!.message).toContain("lacks");
297
+ });
298
+
299
+ test("post-cutoff duplicate AC-N fires V-36", async () => {
300
+ const body = BASE_BODY.replace(
301
+ "ACCEPTANCE_HERE",
302
+ "- [x] AC-1: first (evidence: test.ts:1)\n- [ ] AC-1: dup\n- [x] AC-2: third (evidence: test.ts:2)",
303
+ );
304
+ const parsed = makeParsed("accepted", body);
305
+ const violations = await runValidate(parsed);
306
+ const v36 = filterRule(violations, "V-36");
307
+ expect(v36.length).toBeGreaterThanOrEqual(1);
308
+ expect(v36[0]!.message).toContain("duplicate");
309
+ });
310
+
311
+ test("pre-cutoff line without AC-N does NOT fire V-36", async () => {
312
+ const body = BASE_BODY.replace(
313
+ "ACCEPTANCE_HERE",
314
+ "- [x] AC-1: first (evidence: test.ts:1)\n- [ ] no id here\n- [x] AC-3: third (evidence: test.ts:2)",
315
+ );
316
+ const parsed = makeParsed("accepted", body, { createdAt: "2026-01-01" });
317
+ const violations = await runValidate(parsed);
318
+ const v36 = filterRule(violations, "V-36");
319
+ expect(v36).toHaveLength(0);
320
+ });
321
+ });
322
+
323
+ // ─── V-37: evidence mechanism validity ─────────────────────────────────────
324
+
325
+ describe("V-37: evidence mechanism validity (RFC-0997)", () => {
326
+ test("post-cutoff probe:AC-N with no probe fires V-37", async () => {
327
+ const body = BASE_BODY.replace(
328
+ "ACCEPTANCE_HERE",
329
+ "- [x] AC-1: first (evidence: probe:AC-1)\n- [ ] AC-2: second\n- [x] AC-3: third (evidence: probe:AC-3)",
330
+ );
331
+ const parsed = makeParsed("accepted", body, {
332
+ acceptance: [],
333
+ });
334
+ const violations = await runValidate(parsed);
335
+ const v37 = filterRule(violations, "V-37");
336
+ expect(v37.length).toBeGreaterThanOrEqual(1);
337
+ expect(v37[0]!.message).toContain("no bound probe");
338
+ });
339
+
340
+ test("post-cutoff probe:AC-N referencing nonexistent criterion fires V-37", async () => {
341
+ const body = BASE_BODY.replace(
342
+ "ACCEPTANCE_HERE",
343
+ "- [x] AC-1: first (evidence: probe:AC-99)\n- [ ] AC-2: second\n- [x] AC-3: third (evidence: probe:AC-3)",
344
+ );
345
+ const parsed = makeParsed("accepted", body, {
346
+ acceptance: [],
347
+ });
348
+ const violations = await runValidate(parsed);
349
+ const v37 = filterRule(violations, "V-37");
350
+ expect(v37.length).toBeGreaterThanOrEqual(1);
351
+ expect(v37[0]!.message).toContain("does not exist");
352
+ });
353
+
354
+ test("post-cutoff test:path with missing file fires V-37", async () => {
355
+ const body = BASE_BODY.replace(
356
+ "ACCEPTANCE_HERE",
357
+ "- [x] AC-1: first (evidence: test:nonexistent/file.ts)\n- [ ] AC-2: second\n- [x] AC-3: third (evidence: test:nonexistent/file2.ts)",
358
+ );
359
+ const parsed = makeParsed("accepted", body, {
360
+ acceptance: [],
361
+ });
362
+ const violations = await runValidate(parsed);
363
+ const v37 = filterRule(violations, "V-37");
364
+ expect(v37.length).toBeGreaterThanOrEqual(1);
365
+ expect(v37[0]!.message).toContain("does not exist");
366
+ });
367
+
368
+ test("pre-cutoff does NOT fire V-37", async () => {
369
+ const body = BASE_BODY.replace(
370
+ "ACCEPTANCE_HERE",
371
+ "- [x] AC-1: first (evidence: probe:AC-99)\n- [ ] AC-2: second\n- [x] AC-3: third (evidence: probe:AC-99)",
372
+ );
373
+ const parsed = makeParsed("accepted", body, {
374
+ createdAt: "2026-01-01",
375
+ acceptance: [],
376
+ });
377
+ const violations = await runValidate(parsed);
378
+ const v37 = filterRule(violations, "V-37");
379
+ expect(v37).toHaveLength(0);
380
+ });
381
+ });
382
+
383
+ // ─── V-37: null-safety guard (no acceptance section) ───────────────────────
384
+
385
+ describe("V-37: null-safety when no acceptance criteria section (RFC-0997)", () => {
386
+ test("post-cutoff RFC with no acceptance section does not crash", async () => {
387
+ const body = BASE_BODY.replace("## Acceptance criteria\n\nACCEPTANCE_HERE\n", "");
388
+ const parsed = makeParsed("accepted", body, {
389
+ acceptance: [{ probe: "file-exists", path: "a.ts", criterion: "AC-1" }],
390
+ });
391
+ const violations = await runValidate(parsed);
392
+ expect(violations).toBeDefined();
393
+ });
394
+ });