@warpgogol/forge 5.3.4 → 6.2.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.
Files changed (253) hide show
  1. package/CHANGELOG.md +8 -0
  2. package/bin/cli.ts +7 -2
  3. package/dist/bin/cli.js +7 -2
  4. package/dist/bin/cli.js.map +1 -1
  5. package/dist/os/adr/adr.module.d.ts.map +1 -1
  6. package/dist/os/adr/adr.module.js +2 -0
  7. package/dist/os/adr/adr.module.js.map +1 -1
  8. package/dist/os/audit/audit.module.d.ts.map +1 -1
  9. package/dist/os/audit/audit.module.js +2 -0
  10. package/dist/os/audit/audit.module.js.map +1 -1
  11. package/dist/os/compass/compass.module.d.ts.map +1 -1
  12. package/dist/os/compass/compass.module.js +47 -15
  13. package/dist/os/compass/compass.module.js.map +1 -1
  14. package/dist/os/compass/handlers/compass-audit-handler.d.ts +2 -0
  15. package/dist/os/compass/handlers/compass-audit-handler.d.ts.map +1 -1
  16. package/dist/os/compass/handlers/compass-audit-handler.js +40 -10
  17. package/dist/os/compass/handlers/compass-audit-handler.js.map +1 -1
  18. package/dist/os/compass/handlers/compass-docs-validate.d.ts +13 -0
  19. package/dist/os/compass/handlers/compass-docs-validate.d.ts.map +1 -0
  20. package/dist/os/compass/handlers/compass-docs-validate.js +328 -0
  21. package/dist/os/compass/handlers/compass-docs-validate.js.map +1 -0
  22. package/dist/os/compass/handlers/summary-record.d.ts +11 -2
  23. package/dist/os/compass/handlers/summary-record.d.ts.map +1 -1
  24. package/dist/os/compass/handlers/summary-record.js +95 -15
  25. package/dist/os/compass/handlers/summary-record.js.map +1 -1
  26. package/dist/os/core/core.module.d.ts.map +1 -1
  27. package/dist/os/core/core.module.js +124 -73
  28. package/dist/os/core/core.module.js.map +1 -1
  29. package/dist/os/core/handlers/file-size-lint.d.ts.map +1 -1
  30. package/dist/os/core/handlers/file-size-lint.js +2 -0
  31. package/dist/os/core/handlers/file-size-lint.js.map +1 -1
  32. package/dist/os/exploration/exploration.module.d.ts.map +1 -1
  33. package/dist/os/exploration/exploration.module.js +3 -0
  34. package/dist/os/exploration/exploration.module.js.map +1 -1
  35. package/dist/os/naming/naming-convention.d.ts.map +1 -1
  36. package/dist/os/naming/naming-convention.js +11 -1
  37. package/dist/os/naming/naming-convention.js.map +1 -1
  38. package/dist/os/naming/naming.module.d.ts.map +1 -1
  39. package/dist/os/naming/naming.module.js +9 -2
  40. package/dist/os/naming/naming.module.js.map +1 -1
  41. package/dist/os/notes/notes.module.d.ts.map +1 -1
  42. package/dist/os/notes/notes.module.js +17 -6
  43. package/dist/os/notes/notes.module.js.map +1 -1
  44. package/dist/os/plan/plan.module.d.ts.map +1 -1
  45. package/dist/os/plan/plan.module.js +2 -0
  46. package/dist/os/plan/plan.module.js.map +1 -1
  47. package/dist/os/plugin/plugin.module.d.ts.map +1 -1
  48. package/dist/os/plugin/plugin.module.js +9 -2
  49. package/dist/os/plugin/plugin.module.js.map +1 -1
  50. package/dist/os/program/program.module.d.ts.map +1 -1
  51. package/dist/os/program/program.module.js +19 -4
  52. package/dist/os/program/program.module.js.map +1 -1
  53. package/dist/os/queue/handlers/queue-validate.d.ts.map +1 -1
  54. package/dist/os/queue/handlers/queue-validate.js +154 -7
  55. package/dist/os/queue/handlers/queue-validate.js.map +1 -1
  56. package/dist/os/queue/manifest.d.ts +17 -1
  57. package/dist/os/queue/manifest.d.ts.map +1 -1
  58. package/dist/os/queue/manifest.js +127 -6
  59. package/dist/os/queue/manifest.js.map +1 -1
  60. package/dist/os/queue/queue.module.d.ts.map +1 -1
  61. package/dist/os/queue/queue.module.js +32 -4
  62. package/dist/os/queue/queue.module.js.map +1 -1
  63. package/dist/os/queue/types.d.ts +100 -1
  64. package/dist/os/queue/types.d.ts.map +1 -1
  65. package/dist/os/queue/types.js +44 -0
  66. package/dist/os/queue/types.js.map +1 -1
  67. package/dist/os/rfc/handlers/implement-stamp.d.ts.map +1 -1
  68. package/dist/os/rfc/handlers/implement-stamp.js +30 -8
  69. package/dist/os/rfc/handlers/implement-stamp.js.map +1 -1
  70. package/dist/os/rfc/rfc.module.d.ts.map +1 -1
  71. package/dist/os/rfc/rfc.module.js +25 -6
  72. package/dist/os/rfc/rfc.module.js.map +1 -1
  73. package/dist/os/rfc/types.d.ts +12 -2
  74. package/dist/os/rfc/types.d.ts.map +1 -1
  75. package/dist/os/rfc/types.js +2 -2
  76. package/dist/os/rfc/types.js.map +1 -1
  77. package/dist/os/rfc/verification-evidence.d.ts +21 -0
  78. package/dist/os/rfc/verification-evidence.d.ts.map +1 -1
  79. package/dist/os/rfc/verification-evidence.js +134 -23
  80. package/dist/os/rfc/verification-evidence.js.map +1 -1
  81. package/dist/os/rfc/verification-refresh.d.ts.map +1 -1
  82. package/dist/os/rfc/verification-refresh.js +66 -37
  83. package/dist/os/rfc/verification-refresh.js.map +1 -1
  84. package/dist/os/session/handlers/save.d.ts.map +1 -1
  85. package/dist/os/session/handlers/save.js +42 -5
  86. package/dist/os/session/handlers/save.js.map +1 -1
  87. package/dist/os/session/session.module.d.ts.map +1 -1
  88. package/dist/os/session/session.module.js +5 -0
  89. package/dist/os/session/session.module.js.map +1 -1
  90. package/dist/os/spec/live-spec-merge.d.ts.map +1 -1
  91. package/dist/os/spec/live-spec-merge.js +55 -134
  92. package/dist/os/spec/live-spec-merge.js.map +1 -1
  93. package/dist/os/spec/live-spec-rebuild.d.ts +32 -0
  94. package/dist/os/spec/live-spec-rebuild.d.ts.map +1 -0
  95. package/dist/os/spec/live-spec-rebuild.js +272 -0
  96. package/dist/os/spec/live-spec-rebuild.js.map +1 -0
  97. package/dist/os/spec/live-spec-shared.d.ts +62 -0
  98. package/dist/os/spec/live-spec-shared.d.ts.map +1 -0
  99. package/dist/os/spec/live-spec-shared.js +315 -0
  100. package/dist/os/spec/live-spec-shared.js.map +1 -0
  101. package/dist/os/spec/live-spec-types.d.ts +17 -1
  102. package/dist/os/spec/live-spec-types.d.ts.map +1 -1
  103. package/dist/os/spec/live-spec-types.js +3 -0
  104. package/dist/os/spec/live-spec-types.js.map +1 -1
  105. package/dist/os/spec/live-spec-validate.d.ts.map +1 -1
  106. package/dist/os/spec/live-spec-validate.js +105 -2
  107. package/dist/os/spec/live-spec-validate.js.map +1 -1
  108. package/dist/os/spec/spec-materialize.d.ts +2 -0
  109. package/dist/os/spec/spec-materialize.d.ts.map +1 -1
  110. package/dist/os/spec/spec-materialize.js +45 -5
  111. package/dist/os/spec/spec-materialize.js.map +1 -1
  112. package/dist/os/spec/spec-schema.d.ts +19 -0
  113. package/dist/os/spec/spec-schema.d.ts.map +1 -1
  114. package/dist/os/spec/spec-schema.js +30 -0
  115. package/dist/os/spec/spec-schema.js.map +1 -1
  116. package/dist/os/spec/spec-status.d.ts +4 -0
  117. package/dist/os/spec/spec-status.d.ts.map +1 -1
  118. package/dist/os/spec/spec-status.js +19 -1
  119. package/dist/os/spec/spec-status.js.map +1 -1
  120. package/dist/os/spec/spec-validate.d.ts.map +1 -1
  121. package/dist/os/spec/spec-validate.js +55 -8
  122. package/dist/os/spec/spec-validate.js.map +1 -1
  123. package/dist/os/spec/spec.module.d.ts.map +1 -1
  124. package/dist/os/spec/spec.module.js +84 -8
  125. package/dist/os/spec/spec.module.js.map +1 -1
  126. package/dist/os/werkstatt/handlers/werkstatt-operation-validate.d.ts.map +1 -1
  127. package/dist/os/werkstatt/handlers/werkstatt-operation-validate.js +10 -4
  128. package/dist/os/werkstatt/handlers/werkstatt-operation-validate.js.map +1 -1
  129. package/dist/os/werkstatt/werkstatt.module.d.ts.map +1 -1
  130. package/dist/os/werkstatt/werkstatt.module.js +13 -4
  131. package/dist/os/werkstatt/werkstatt.module.js.map +1 -1
  132. package/dist/os/workflow/workflow.module.d.ts.map +1 -1
  133. package/dist/os/workflow/workflow.module.js +13 -5
  134. package/dist/os/workflow/workflow.module.js.map +1 -1
  135. package/dist/src/cli-flags.d.ts.map +1 -1
  136. package/dist/src/cli-flags.js +25 -5
  137. package/dist/src/cli-flags.js.map +1 -1
  138. package/dist/src/config/forge-config.d.ts +3 -0
  139. package/dist/src/config/forge-config.d.ts.map +1 -1
  140. package/dist/src/config/forge-config.js +2 -0
  141. package/dist/src/config/forge-config.js.map +1 -1
  142. package/dist/src/onboarding/doctor.d.ts.map +1 -1
  143. package/dist/src/onboarding/doctor.js +2 -3
  144. package/dist/src/onboarding/doctor.js.map +1 -1
  145. package/dist/src/onboarding/init.d.ts.map +1 -1
  146. package/dist/src/onboarding/init.js +8 -3
  147. package/dist/src/onboarding/init.js.map +1 -1
  148. package/dist/src/pipeline-status.d.ts +14 -1
  149. package/dist/src/pipeline-status.d.ts.map +1 -1
  150. package/dist/src/pipeline-status.js +28 -8
  151. package/dist/src/pipeline-status.js.map +1 -1
  152. package/dist/src/types.d.ts +7 -1
  153. package/dist/src/types.d.ts.map +1 -1
  154. package/dist/src/types.js +6 -0
  155. package/dist/src/types.js.map +1 -1
  156. package/dist/src/utils/generated-marker.d.ts +8 -0
  157. package/dist/src/utils/generated-marker.d.ts.map +1 -1
  158. package/dist/src/utils/generated-marker.js +25 -0
  159. package/dist/src/utils/generated-marker.js.map +1 -1
  160. package/dist/src/validators/skill-validate.d.ts.map +1 -1
  161. package/dist/src/validators/skill-validate.js +2 -4
  162. package/dist/src/validators/skill-validate.js.map +1 -1
  163. package/os/adr/adr.module.ts +2 -0
  164. package/os/audit/audit.module.ts +2 -0
  165. package/os/compass/compass.module.ts +49 -15
  166. package/os/compass/handlers/compass-audit-handler.ts +51 -10
  167. package/os/compass/handlers/compass-docs-validate.ts +361 -0
  168. package/os/compass/handlers/summary-record.ts +106 -15
  169. package/os/compass/handlers/tests/compass-audit-record.test.ts +2 -1
  170. package/os/compass/handlers/tests/compass-audit-validate.test.ts +92 -3
  171. package/os/compass/handlers/tests/compass-docs-validate.test.ts +203 -0
  172. package/os/compass/handlers/tests/compass-ledger-scope.test.ts +2 -1
  173. package/os/compass/handlers/tests/compass-policy.test.ts +7 -1
  174. package/os/compass/handlers/tests/summary-record.test.ts +127 -0
  175. package/os/core/core.module.ts +147 -75
  176. package/os/core/docs-archive.test.ts +136 -0
  177. package/os/core/handlers/file-size-lint.ts +2 -0
  178. package/os/exploration/exploration.module.ts +3 -0
  179. package/os/naming/naming-convention.ts +11 -1
  180. package/os/naming/naming.module.ts +9 -2
  181. package/os/notes/notes.module.ts +17 -6
  182. package/os/plan/plan-0000-template.md +150 -0
  183. package/os/plan/plan.module.ts +2 -0
  184. package/os/plugin/plugin.module.ts +9 -2
  185. package/os/program/program.module.ts +19 -4
  186. package/os/queue/decision-ledger.test.ts +163 -0
  187. package/os/queue/handlers/queue-validate.ts +190 -8
  188. package/os/queue/manifest.ts +201 -7
  189. package/os/queue/queue-validate.test.ts +303 -0
  190. package/os/queue/queue.module.ts +32 -4
  191. package/os/queue/types.ts +56 -1
  192. package/os/rfc/handlers/implement-stamp.ts +37 -8
  193. package/os/rfc/rfc-read-only-no-side-effects.test.ts +1 -0
  194. package/os/rfc/rfc.module.ts +27 -6
  195. package/os/rfc/types.ts +18 -3
  196. package/os/rfc/verification-emit.test.ts +305 -0
  197. package/os/rfc/verification-evidence.ts +149 -24
  198. package/os/rfc/verification-refresh.test.ts +222 -1
  199. package/os/rfc/verification-refresh.ts +74 -38
  200. package/os/session/handlers/save.ts +37 -5
  201. package/os/session/session.module.ts +5 -0
  202. package/os/spec/live-spec-idempotency.pbt.test.ts +222 -0
  203. package/os/spec/live-spec-list-show-validate.test.ts +10 -6
  204. package/os/spec/live-spec-merge.test.ts +187 -7
  205. package/os/spec/live-spec-merge.ts +71 -155
  206. package/os/spec/live-spec-rebuild.test.ts +317 -0
  207. package/os/spec/live-spec-rebuild.ts +356 -0
  208. package/os/spec/live-spec-shared.ts +378 -0
  209. package/os/spec/live-spec-types.ts +21 -1
  210. package/os/spec/live-spec-validate.test.ts +317 -0
  211. package/os/spec/live-spec-validate.ts +106 -2
  212. package/os/spec/spec-materialize.test.ts +192 -0
  213. package/os/spec/spec-materialize.ts +48 -4
  214. package/os/spec/spec-schema.ts +49 -0
  215. package/os/spec/spec-status.ts +24 -0
  216. package/os/spec/spec-validate.test.ts +66 -0
  217. package/os/spec/spec-validate.ts +62 -8
  218. package/os/spec/spec.module.ts +85 -8
  219. package/os/werkstatt/handlers/werkstatt-operation-validate.ts +10 -4
  220. package/os/werkstatt/werkstatt.module.ts +13 -4
  221. package/os/workflow/workflow.module.ts +13 -5
  222. package/package.json +1 -1
  223. package/skills/_shared/fo-pipeline-conventions.md +126 -1
  224. package/skills/fo/fo-doc-audit/SKILL.md +4 -4
  225. package/skills/fo/fo-explore/SKILL.md +2 -1
  226. package/skills/fo/fo-fix/SKILL.md +11 -3
  227. package/skills/fo/fo-handoff/SKILL.md +4 -6
  228. package/skills/fo/fo-idea-create-rfc/SKILL.md +2 -1
  229. package/skills/fo/fo-idea-enhance/SKILL.md +23 -4
  230. package/skills/fo/fo-idea-i-just-want-to-see-the-result/SKILL.md +86 -22
  231. package/skills/fo/fo-idea-implement/SKILL.md +53 -10
  232. package/skills/fo/fo-idea-plan/SKILL.md +28 -6
  233. package/skills/fo/fo-review/SKILL.md +69 -8
  234. package/skills/fo/fo-session-retro/SKILL.md +3 -2
  235. package/skills/fo/fo-step-commit/SKILL.md +2 -0
  236. package/skills/shared/grilling/SKILL.md +12 -0
  237. package/skills/shared/writing-great-skills/GLOSSARY.md +2 -0
  238. package/skills/shared/writing-great-skills/SKILL.md +2 -1
  239. package/src/cli-flags.ts +23 -5
  240. package/src/config/forge-config.ts +4 -0
  241. package/src/onboarding/doctor.ts +2 -3
  242. package/src/onboarding/init.ts +8 -3
  243. package/src/pipeline-status.ts +32 -8
  244. package/src/tests/cli-flags.test.ts +20 -0
  245. package/src/tests/cli-output.test.ts +13 -7
  246. package/src/tests/fixtures/agents-generate-business-before.txt +7 -7
  247. package/src/tests/implement-stamp.test.ts +38 -0
  248. package/src/tests/session-handlers.test.ts +33 -0
  249. package/src/types.ts +13 -1
  250. package/src/utils/generated-marker.ts +28 -0
  251. package/src/validators/skill-validate.ts +2 -4
  252. package/skills/fo/fo-idea-implement/ADR-FLOW.md +0 -179
  253. package/skills/fo/fo-review/AXES.md +0 -70
package/os/rfc/types.ts CHANGED
@@ -12,7 +12,6 @@
12
12
  * </non-goals>
13
13
  * </MODULE_CONTRACT>
14
14
  * * <CHANGE_SUMMARY>
15
- <item>RFC-0795: added dependsOn, batch fields to RfcFrontmatter and RFC_KNOWN_KEYS; added RFC-IMP-07 to RfcImplementStampRule; added batch/dependsOn to RfcListEntry.</item>
16
15
  <item>RFC-0998: added TestProbe and JsonSchemaProbe to AcceptanceProbe union for test and json-schema acceptance probe kinds.</item>
17
16
  <item>RFC-0999: added lastRefreshedAt to VerificationEvidence; added RfcVerificationRefreshResult interface.</item>
18
17
  <item>RFC-1097: step 6 — compass.migrate codemod run
@@ -21,7 +20,8 @@ Mechanical v1 to v2 header migration across the workspace: 942 files rewritten
21
20
  <item>RFC-1097: sweep — packages/forge + services clean
22
21
 
23
22
  Sweep batch 2: real KEY_DECISIONS on 10 files, expanded purposes (CONTRACT-02/PURPOSE-02), headers on mission/index + gen-upstreams, sanitizeItemText in summary.record (literal Compass tags corrupted history), excludedPaths for wrangler types, test-fixtures testPattern. forge+services now 0 diagnostics under --mode error.</item>
24
- <history>RFC-0465, RFC-0478, RFC-0480</history>
23
+ <item>RFC-1249: wire werkstatt.commands.validate into packages.check + retire CMD-OUTPUT debt</item>
24
+ <history>RFC-0465, RFC-0478, RFC-0480, RFC-0795</history>
25
25
  </CHANGE_SUMMARY>
26
26
  ***************************************************************/
27
27
 
@@ -338,12 +338,25 @@ export interface VerificationEvidence {
338
338
  filesModified?: string[];
339
339
  }
340
340
 
341
+ export interface VerificationDelta {
342
+ /** Envelopes that flipped fail → pass since the committed version. */
343
+ recovered: string[];
344
+ /** Envelopes that flipped pass → fail since the committed version. */
345
+ regressed: string[];
346
+ }
347
+
341
348
  export interface RfcVerificationEmitResult {
342
349
  command: "rfc.verification.emit";
343
350
  status: "pass" | "fail";
344
- emitted: Array<{ rfcId: string; file: string; overall: "pass" | "fail" }>;
351
+ emitted: Array<{
352
+ rfcId: string;
353
+ file: string;
354
+ overall: "pass" | "fail";
355
+ previousOverall?: "pass" | "fail";
356
+ }>;
345
357
  skipped: Array<{ rfcId: string; reason: "no-probes" }>;
346
358
  diagnostics: Diagnostic[];
359
+ delta?: VerificationDelta;
347
360
  }
348
361
 
349
362
  export interface RfcVerificationRefreshResult {
@@ -353,11 +366,13 @@ export interface RfcVerificationRefreshResult {
353
366
  rfcId: string;
354
367
  file: string;
355
368
  overall: "pass" | "fail";
369
+ previousOverall?: "pass" | "fail";
356
370
  probesTotal: number;
357
371
  probesFailed: number;
358
372
  }>;
359
373
  skipped: Array<{ rfcId: string; reason: "no evidence envelope" | "not implemented" }>;
360
374
  diagnostics: Diagnostic[];
375
+ delta?: VerificationDelta;
361
376
  summary: { total: number; passed: number; failed: number; skipped: number };
362
377
  } // ─── Validation ──────────────────────────────────────────────────────────────
363
378
  export interface RfcValidationViolation {
@@ -0,0 +1,305 @@
1
+ // @vitest-environment node
2
+ import { test, expect, describe, vi, beforeEach, afterEach } from "vitest";
3
+ import fs from "node:fs/promises";
4
+ import path from "node:path";
5
+ import os from "node:os";
6
+ import { stringify as yamlStringify } from "yaml";
7
+
8
+ import {
9
+ runRfcVerificationEmit,
10
+ mapWithConcurrency,
11
+ resolveConcurrency,
12
+ } from "./verification-evidence.ts";
13
+ import type { ForgeCommandInput, ForgeRuntimeContext } from "../../src/types.ts";
14
+
15
+ const { mockRunProbe } = vi.hoisted(() => ({
16
+ mockRunProbe: vi.fn(),
17
+ }));
18
+
19
+ vi.mock("./acceptance.ts", () => ({
20
+ runProbe: mockRunProbe,
21
+ }));
22
+
23
+ const RFC_FRONTMATTER = (id: string, status: string, probes: Record<string, unknown>[] = []) => {
24
+ const probeYaml = probes
25
+ .map(
26
+ (p) =>
27
+ ` - probe: ${p.probe}\n path: ${p.path ?? ""}\n criterion: ${p.criterion ?? ""}`,
28
+ )
29
+ .join("\n");
30
+ return `---\nid: ${id}\ntitle: "Test ${id}"\nstatus: ${status}\nkind: command\nscope: workspace\nowners:\n - architecture\ncreatedAt: 2026-01-01\nupdatedAt: 2026-01-01\nacceptance:\n${probeYaml}\n---\n\n# ${id}\n## Context\nTest.\n`;
31
+ };
32
+
33
+ const PROBE = { probe: "file-exists", path: "src/index.ts", criterion: "AC-1" };
34
+
35
+ function makeEnvelope(rfcId: string, overall: "pass" | "fail"): Record<string, unknown> {
36
+ return {
37
+ rfcId,
38
+ title: `Test ${rfcId}`,
39
+ rfcStatus: "implemented",
40
+ emittedAt: "2026-01-01T00:00:00.000Z",
41
+ commit: "abc123",
42
+ workingTreeDirty: false,
43
+ kernelVersion: "1.0.0",
44
+ rfcFileHash: "hash123",
45
+ acceptanceHash: "accHash123",
46
+ probes: [{ probe: PROBE, ok: overall === "pass", detail: "", durationMs: 5 }],
47
+ overall,
48
+ };
49
+ }
50
+
51
+ async function setupWorkspace(opts: {
52
+ rfcs: Array<{ id: string; status: string; probes?: unknown[] }>;
53
+ envelopes?: Array<{ id: string; overall: "pass" | "fail" }>;
54
+ }): Promise<{ workspaceRoot: string; cleanup: () => Promise<void> }> {
55
+ const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "rfc-emit-test-"));
56
+ const rfcDir = path.join(tmpDir, "docs", "rfcs");
57
+ const verificationDir = path.join(rfcDir, "verification");
58
+ await fs.mkdir(rfcDir, { recursive: true });
59
+
60
+ for (const rfc of opts.rfcs) {
61
+ const slug = rfc.id.toLowerCase();
62
+ const probes = rfc.probes ?? [PROBE];
63
+ await fs.writeFile(
64
+ path.join(rfcDir, `${slug}-test.md`),
65
+ RFC_FRONTMATTER(rfc.id, rfc.status, probes),
66
+ );
67
+ }
68
+
69
+ for (const env of opts.envelopes ?? []) {
70
+ await fs.mkdir(verificationDir, { recursive: true });
71
+ await fs.writeFile(
72
+ path.join(verificationDir, `${env.id.toLowerCase()}.generated.yaml`),
73
+ yamlStringify(makeEnvelope(env.id, env.overall)) + "\n",
74
+ );
75
+ }
76
+
77
+ return {
78
+ workspaceRoot: tmpDir,
79
+ cleanup: async () => {
80
+ await fs.rm(tmpDir, { recursive: true, force: true });
81
+ },
82
+ };
83
+ }
84
+
85
+ function makeContext(workspaceRoot: string, overrides: Record<string, unknown> = {}) {
86
+ return {
87
+ workspaceRoot,
88
+ logger: { info: () => {} },
89
+ outputFormat: "json" as const,
90
+ commandRegistry: undefined,
91
+ dryRun: false,
92
+ ...overrides,
93
+ };
94
+ }
95
+
96
+ const envelopePath = (root: string, rfcId: string) =>
97
+ path.join(root, "docs", "rfcs", "verification", `${rfcId.toLowerCase()}.generated.yaml`);
98
+
99
+ beforeEach(() => {
100
+ mockRunProbe.mockReset();
101
+ });
102
+
103
+ afterEach(() => {
104
+ vi.restoreAllMocks();
105
+ });
106
+
107
+ describe("rfc.verification.emit — baseline emission", () => {
108
+ test("writes the emit-canonical structured marker header + envelope", async () => {
109
+ const { workspaceRoot, cleanup } = await setupWorkspace({
110
+ rfcs: [{ id: "RFC-0100", status: "implemented" }],
111
+ });
112
+
113
+ try {
114
+ mockRunProbe.mockResolvedValue({ probe: PROBE, ok: true, detail: "exists" });
115
+
116
+ const result = await runRfcVerificationEmit(
117
+ { flags: { id: "RFC-0100" } } as unknown as ForgeCommandInput,
118
+ makeContext(workspaceRoot) as unknown as ForgeRuntimeContext,
119
+ );
120
+
121
+ expect(result.data!.emitted).toHaveLength(1);
122
+ expect(result.exitCode).toBe(0);
123
+
124
+ const raw = await fs.readFile(envelopePath(workspaceRoot, "RFC-0100"), "utf-8");
125
+ // RFC-1237 canonical format: structured marker keys, not '#' comments.
126
+ expect(raw).toMatch(/^generatedMarker: GENERATED\./);
127
+ expect(raw).toContain("doNotEdit: DO NOT EDIT THIS FILE.");
128
+ expect(raw).toContain("ownerCommand: rfc.verification.emit");
129
+ expect(raw).toContain("regenerateCommand: pnpm exec werkstatt run rfc.verification.emit");
130
+ expect(raw).toContain("rfcId: RFC-0100");
131
+ expect(raw).toContain("overall: pass");
132
+ expect(raw.split("\n").every((line) => !line.startsWith("#"))).toBe(true);
133
+ } finally {
134
+ await cleanup();
135
+ }
136
+ });
137
+ });
138
+
139
+ describe("rfc.verification.emit — sweep delta report", () => {
140
+ test("reports recovered (fail→pass) and regressed (pass→fail) transitions", async () => {
141
+ const { workspaceRoot, cleanup } = await setupWorkspace({
142
+ rfcs: [
143
+ { id: "RFC-0100", status: "implemented" },
144
+ { id: "RFC-0101", status: "implemented" },
145
+ { id: "RFC-0102", status: "implemented" },
146
+ ],
147
+ envelopes: [
148
+ { id: "RFC-0100", overall: "fail" },
149
+ { id: "RFC-0101", overall: "pass" },
150
+ { id: "RFC-0102", overall: "fail" },
151
+ ],
152
+ });
153
+
154
+ try {
155
+ mockRunProbe
156
+ .mockResolvedValueOnce({ probe: PROBE, ok: true, detail: "exists" })
157
+ .mockResolvedValueOnce({ probe: PROBE, ok: false, detail: "missing" })
158
+ .mockResolvedValueOnce({ probe: PROBE, ok: false, detail: "missing" });
159
+
160
+ const result = await runRfcVerificationEmit(
161
+ { flags: { status: "implemented" } } as unknown as ForgeCommandInput,
162
+ makeContext(workspaceRoot) as unknown as ForgeRuntimeContext,
163
+ );
164
+
165
+ expect(result.data!.delta).toEqual({
166
+ recovered: ["RFC-0100"],
167
+ regressed: ["RFC-0101"],
168
+ });
169
+ const entries = Object.fromEntries(result.data!.emitted.map((e) => [e.rfcId, e]));
170
+ expect(entries["RFC-0100"]?.previousOverall).toBe("fail");
171
+ expect(entries["RFC-0101"]?.previousOverall).toBe("pass");
172
+ expect(entries["RFC-0102"]?.previousOverall).toBe("fail");
173
+ const r101Diag = result.data!.diagnostics.find((d) => d.file?.includes("rfc-0101"));
174
+ expect(r101Diag?.message).toContain("regressed: was pass");
175
+ expect(result.summary).toContain("1 regressed, 1 recovered");
176
+ } finally {
177
+ await cleanup();
178
+ }
179
+ });
180
+ });
181
+
182
+ describe("rfc.verification.emit — RFC-1237 AC-6: dry-run", () => {
183
+ test("context.dryRun (the real werkstatt run --dry-run path) writes zero envelopes", async () => {
184
+ const { workspaceRoot, cleanup } = await setupWorkspace({
185
+ rfcs: [{ id: "RFC-0100", status: "implemented" }],
186
+ });
187
+
188
+ try {
189
+ mockRunProbe.mockResolvedValue({ probe: PROBE, ok: true, detail: "exists" });
190
+
191
+ // The real CLI path: consumeCommonFlags strips --dry-run from argv into
192
+ // context.dryRun — input.flags carries no "dry-run" key at all.
193
+ const result = await runRfcVerificationEmit(
194
+ { flags: { id: "RFC-0100" } } as unknown as ForgeCommandInput,
195
+ makeContext(workspaceRoot, { dryRun: true }) as unknown as ForgeRuntimeContext,
196
+ );
197
+
198
+ expect(result.data!.emitted).toHaveLength(1);
199
+ expect(result.summary).toContain("dry-run");
200
+ await expect(fs.stat(envelopePath(workspaceRoot, "RFC-0100"))).rejects.toThrow();
201
+ // The verification dir itself is also not created under dry-run.
202
+ await expect(
203
+ fs.stat(path.join(workspaceRoot, "docs", "rfcs", "verification")),
204
+ ).rejects.toThrow();
205
+ } finally {
206
+ await cleanup();
207
+ }
208
+ });
209
+
210
+ test('input.flags["dry-run"] (direct/programmatic callers) writes zero envelopes', async () => {
211
+ const { workspaceRoot, cleanup } = await setupWorkspace({
212
+ rfcs: [{ id: "RFC-0100", status: "implemented" }],
213
+ });
214
+
215
+ try {
216
+ mockRunProbe.mockResolvedValue({ probe: PROBE, ok: true, detail: "exists" });
217
+
218
+ const result = await runRfcVerificationEmit(
219
+ { flags: { id: "RFC-0100", "dry-run": true } } as unknown as ForgeCommandInput,
220
+ makeContext(workspaceRoot) as unknown as ForgeRuntimeContext,
221
+ );
222
+
223
+ expect(result.summary).toContain("dry-run");
224
+ await expect(fs.stat(envelopePath(workspaceRoot, "RFC-0100"))).rejects.toThrow();
225
+ } finally {
226
+ await cleanup();
227
+ }
228
+ });
229
+
230
+ test("dry-run still runs probes and reports overall: fail", async () => {
231
+ const { workspaceRoot, cleanup } = await setupWorkspace({
232
+ rfcs: [{ id: "RFC-0100", status: "implemented" }],
233
+ });
234
+
235
+ try {
236
+ mockRunProbe.mockResolvedValue({ probe: PROBE, ok: false, detail: "does not exist" });
237
+
238
+ const result = await runRfcVerificationEmit(
239
+ { flags: { id: "RFC-0100" } } as unknown as ForgeCommandInput,
240
+ makeContext(workspaceRoot, { dryRun: true }) as unknown as ForgeRuntimeContext,
241
+ );
242
+
243
+ expect(mockRunProbe).toHaveBeenCalledOnce();
244
+ expect(result.data!.emitted[0]?.overall).toBe("fail");
245
+ expect(result.exitCode).toBe(1);
246
+ await expect(fs.stat(envelopePath(workspaceRoot, "RFC-0100"))).rejects.toThrow();
247
+ } finally {
248
+ await cleanup();
249
+ }
250
+ });
251
+ });
252
+
253
+ describe("verification sweep — --concurrency", () => {
254
+ test("mapWithConcurrency preserves input order and bounds parallelism", async () => {
255
+ let inFlight = 0;
256
+ let maxInFlight = 0;
257
+ const items = Array.from({ length: 10 }, (_, i) => i);
258
+ const results = await mapWithConcurrency(items, 3, async (n) => {
259
+ inFlight += 1;
260
+ maxInFlight = Math.max(maxInFlight, inFlight);
261
+ await new Promise((r) => setTimeout(r, Math.random() * 5));
262
+ inFlight -= 1;
263
+ return n * 2;
264
+ });
265
+ expect(results).toEqual(items.map((n) => n * 2));
266
+ expect(maxInFlight).toBeLessThanOrEqual(3);
267
+ expect(maxInFlight).toBeGreaterThan(1);
268
+ });
269
+
270
+ test("resolveConcurrency: absent→1, garbage→1+warn, >16→clamped", () => {
271
+ const warnings: string[] = [];
272
+ const warn = (m: string) => warnings.push(m);
273
+ expect(resolveConcurrency(undefined)).toBe(1);
274
+ expect(resolveConcurrency("abc", warn)).toBe(1);
275
+ expect(resolveConcurrency("0", warn)).toBe(1);
276
+ expect(resolveConcurrency("4", warn)).toBe(4);
277
+ expect(resolveConcurrency("99", warn)).toBe(16);
278
+ expect(warnings).toHaveLength(3);
279
+ });
280
+
281
+ test("--concurrency 3 emits identical results to sequential", async () => {
282
+ const ids = ["RFC-0201", "RFC-0202", "RFC-0203", "RFC-0204"];
283
+ const { workspaceRoot, cleanup } = await setupWorkspace({
284
+ rfcs: ids.map((id) => ({ id, status: "implemented" })),
285
+ });
286
+
287
+ try {
288
+ mockRunProbe.mockResolvedValue({ probe: PROBE, ok: true, detail: "exists" });
289
+
290
+ const result = await runRfcVerificationEmit(
291
+ { flags: { status: "implemented", concurrency: "3" } } as unknown as ForgeCommandInput,
292
+ makeContext(workspaceRoot) as unknown as ForgeRuntimeContext,
293
+ );
294
+
295
+ expect(result.data!.emitted.map((e) => e.rfcId)).toEqual(ids);
296
+ expect(result.data!.emitted.every((e) => e.overall === "pass")).toBe(true);
297
+ expect(result.exitCode).toBe(0);
298
+ for (const id of ids) {
299
+ await expect(fs.stat(envelopePath(workspaceRoot, id))).resolves.toBeDefined();
300
+ }
301
+ } finally {
302
+ await cleanup();
303
+ }
304
+ });
305
+ });
@@ -14,6 +14,12 @@ a JSON evidence envelope to docs/rfcs/verification/<slug>.generated.yaml.
14
14
  <CHANGE_SUMMARY>
15
15
  <item>RFC-0330: initial implementation.</item>
16
16
  <item>RFC-0999: exported captureGitContext, getKernelVersion, byteHashHex, VERIFICATION_DIR for reuse by verification-refresh.ts.</item>
17
+ <item>RFC-1237: dual-read dry-run (context.dryRun || flags["dry-run"]),
18
+ io.writeFile port routing, dry-run summary marker; serializeEvidenceEnvelope —
19
+ shared emit-canonical serializer restoring the structured YAML marker-key
20
+ header (generatedMarker/doNotEdit/ownerCommand/editInstead/regenerateCommand
21
+ with "pnpm exec werkstatt run" prefix), also consumed by refresh.</item>
22
+ <item>RFC-1249: wire werkstatt.commands.validate into packages.check + retire CMD-OUTPUT debt</item>
17
23
  </CHANGE_SUMMARY>
18
24
  */
19
25
 
@@ -26,9 +32,8 @@ import { byteHash } from "../../src/utils/hash.ts";
26
32
 
27
33
  import { runProbe } from "./acceptance.ts";
28
34
  import { listRfcFiles, readAndParseRfc } from "./frontmatter-io.ts";
29
- import { writeFileAtomic } from "../../src/utils/fs-atomic.ts";
30
- import { buildGeneratedHeader } from "../../src/utils/generated-marker.ts";
31
- import { stringify as yamlStringify } from "yaml";
35
+ import { generatedHeaderFields } from "../../src/utils/generated-marker.ts";
36
+ import { parse as yamlParse, stringify as yamlStringify } from "yaml";
32
37
  import { RFC_DIR } from "./types.ts";
33
38
 
34
39
  // Ambient default for helper fns without a context param — handlers override
@@ -90,6 +95,71 @@ export function byteHashHex(content: string): string {
90
95
  return byteHash(content).slice(HASH_PREFIX.length);
91
96
  }
92
97
 
98
+ /**
99
+ * Bounded-parallel map preserving input order — one in-flight worker per slot,
100
+ * results land in the same positions as their inputs. Used by the emit/refresh
101
+ * sweeps so `--concurrency N` overlaps probe subprocesses across RFCs.
102
+ */
103
+ export async function mapWithConcurrency<T, R>(
104
+ items: readonly T[],
105
+ concurrency: number,
106
+ worker: (item: T, index: number) => Promise<R>,
107
+ ): Promise<R[]> {
108
+ const results = new Array<R>(items.length);
109
+ let cursor = 0;
110
+ const lanes = Array.from({ length: Math.min(Math.max(concurrency, 1), items.length) }, () =>
111
+ (async () => {
112
+ while (cursor < items.length) {
113
+ const index = cursor;
114
+ cursor += 1;
115
+ results[index] = await worker(items[index]!, index);
116
+ }
117
+ })(),
118
+ );
119
+ await Promise.all(lanes);
120
+ return results;
121
+ }
122
+
123
+ /**
124
+ * Resolve the `--concurrency` flag (string flag; the kernel schema has no
125
+ * number kind). Garbage/NaN/non-positive degrades to 1 with a warning;
126
+ * clamped to 16 — probe workers spawn vitest/site-kernel children and wider
127
+ * fan-out starves CPU rather than speeding the sweep.
128
+ */
129
+ export function resolveConcurrency(flag: unknown, warn?: (message: string) => void): number {
130
+ if (flag === undefined) return 1;
131
+ const raw = Array.isArray(flag) ? flag[0] : flag;
132
+ const parsed = Number.parseInt(String(raw), 10);
133
+ if (!Number.isFinite(parsed) || parsed < 1) {
134
+ warn?.(`--concurrency "${String(raw)}" is not a positive integer — falling back to 1.`);
135
+ return 1;
136
+ }
137
+ if (parsed > 16) {
138
+ warn?.(
139
+ `--concurrency ${parsed} exceeds the 16 cap — clamped (probe workers spawn subprocesses).`,
140
+ );
141
+ return 16;
142
+ }
143
+ return parsed;
144
+ }
145
+
146
+ /**
147
+ * RFC-1237: the emit-canonical envelope serialization — structured marker keys
148
+ * (generatedMarker/doNotEdit/ownerCommand/editInstead/regenerateCommand) as
149
+ * leading YAML fields, then the envelope body. emit and refresh MUST share
150
+ * this serializer: refresh owns content freshness, never envelope identity
151
+ * (OWN-DUP-01), so refresh output is identical-by-construction.
152
+ */
153
+ export function serializeEvidenceEnvelope(envelope: VerificationEvidence): string {
154
+ return `${yamlStringify({
155
+ ...generatedHeaderFields({
156
+ ownerCommand: "rfc.verification.emit",
157
+ commandPrefix: "pnpm exec werkstatt run",
158
+ }),
159
+ ...envelope,
160
+ })}\n`;
161
+ }
162
+
93
163
  function normalizeProbes(probes: AcceptanceProbe[]): string {
94
164
  return JSON.stringify(probes, Object.keys(probes[0] ?? {}).sort());
95
165
  }
@@ -130,6 +200,10 @@ export async function runRfcVerificationEmit(
130
200
  const rfcDirPath = join(workspaceRoot, RFC_DIR);
131
201
  const targetId = input.flags["id"] as string | undefined;
132
202
  const targetStatus = input.flags["status"] as string | undefined;
203
+ // RFC-1237: consumeCommonFlags strips --dry-run into context.dryRun before
204
+ // dispatch — the input.flags read stays for direct/programmatic callers.
205
+ const dryRun = context.dryRun === true || input.flags["dry-run"] === true;
206
+ const concurrency = resolveConcurrency(input.flags["concurrency"], (m) => logger.warn(m));
133
207
 
134
208
  if (!targetId && !targetStatus) {
135
209
  return {
@@ -161,22 +235,27 @@ export async function runRfcVerificationEmit(
161
235
  }
162
236
  const kernelVersion = await getKernelVersion(workspaceRoot);
163
237
  const verificationDirAbs = join(workspaceRoot, VERIFICATION_DIR);
164
- await io.mkdir(verificationDirAbs);
238
+ if (!dryRun) await io.mkdir(verificationDirAbs);
165
239
 
166
- for (const fileName of allFiles) {
240
+ const processFile = async (
241
+ fileName: string,
242
+ ): Promise<{
243
+ emittedEntry?: NonNullable<RfcVerificationEmitResult["emitted"]>[number];
244
+ skippedEntry?: NonNullable<RfcVerificationEmitResult["skipped"]>[number];
245
+ diagnostic?: Diagnostic;
246
+ }> => {
167
247
  const parsedFile = await readAndParseRfc(rfcDirPath, fileName);
168
- if (!parsedFile) continue;
169
- if ("error" in parsedFile) continue;
248
+ if (!parsedFile) return {};
249
+ if ("error" in parsedFile) return {};
170
250
  const fm = parsedFile.parsed.frontmatter;
171
251
  const rfcId = String(fm["id"] ?? "");
172
252
 
173
- if (targetId && rfcId.toLowerCase() !== targetId.toLowerCase()) continue;
174
- if (targetStatus && String(fm["status"] ?? "") !== targetStatus) continue;
253
+ if (targetId && rfcId.toLowerCase() !== targetId.toLowerCase()) return {};
254
+ if (targetStatus && String(fm["status"] ?? "") !== targetStatus) return {};
175
255
 
176
256
  const acceptance = fm["acceptance"];
177
257
  if (!Array.isArray(acceptance) || acceptance.length === 0) {
178
- skipped.push({ rfcId, reason: "no-probes" });
179
- continue;
258
+ return { skippedEntry: { rfcId, reason: "no-probes" } };
180
259
  }
181
260
 
182
261
  const probes = acceptance as AcceptanceProbe[];
@@ -213,29 +292,74 @@ export async function runRfcVerificationEmit(
213
292
  const evidenceFileName = `${slug}.generated.yaml`;
214
293
  const evidenceRelPath = join(VERIFICATION_DIR, evidenceFileName);
215
294
  const evidenceAbsPath = join(workspaceRoot, evidenceRelPath);
216
- const jsonContent = `${buildGeneratedHeader({ filePath: evidenceRelPath, ownerCommand: "rfc.verification.emit" })}${yamlStringify(envelope)}\n`;
217
- await writeFileAtomic(evidenceAbsPath, jsonContent);
218
-
219
- emitted.push({ rfcId, file: evidenceRelPath, overall: envelope.overall });
220
295
 
221
- if (envelope.overall === "fail") {
222
- diagnostics.push({
223
- ruleId: "RFC-EVID-02",
224
- severity: "error",
225
- file: evidenceRelPath,
226
- message: `${rfcId}: evidence overall is "fail" — ${probeRecords.filter((r) => !r.ok).length} probe(s) failed.`,
227
- });
296
+ // Committed-envelope baseline for the sweep delta — lets a fail-heavy run
297
+ // separate still-failing drift from fresh regressions without git diff.
298
+ let previousOverall: "pass" | "fail" | undefined;
299
+ try {
300
+ const existing = yamlParse(await io.readFile(evidenceAbsPath)) as VerificationEvidence | null;
301
+ if (existing && (existing.overall === "pass" || existing.overall === "fail")) {
302
+ previousOverall = existing.overall;
303
+ }
304
+ } catch {
305
+ // No committed envelope — first emission for this RFC.
228
306
  }
229
307
 
308
+ // RFC-1237: port write — the recording IO adapter intercepts under kernel
309
+ // --dry-run even if the explicit guard regresses.
310
+ if (!dryRun) await io.writeFile(evidenceAbsPath, serializeEvidenceEnvelope(envelope));
311
+
230
312
  if (outputFormat === "pretty") {
313
+ const drift =
314
+ previousOverall !== undefined && previousOverall !== envelope.overall
315
+ ? `, was ${previousOverall}`
316
+ : "";
231
317
  logger.info(
232
- `[evidence] ${rfcId} → ${evidenceRelPath} (${envelope.overall}, ${probeRecords.length} probes)`,
318
+ `[evidence] ${rfcId} → ${evidenceRelPath} (${envelope.overall}, ${probeRecords.length} probes${drift}${dryRun ? ", dry-run" : ""})`,
233
319
  );
234
320
  }
321
+
322
+ return {
323
+ emittedEntry: {
324
+ rfcId,
325
+ file: evidenceRelPath,
326
+ overall: envelope.overall,
327
+ ...(previousOverall !== undefined ? { previousOverall } : {}),
328
+ },
329
+ ...(envelope.overall === "fail"
330
+ ? {
331
+ diagnostic: {
332
+ ruleId: "RFC-EVID-02",
333
+ severity: "error",
334
+ file: evidenceRelPath,
335
+ message: `${rfcId}: evidence overall is "fail" — ${probeRecords.filter((r) => !r.ok).length} probe(s) failed${previousOverall === "pass" ? " (regressed: was pass)" : ""}.`,
336
+ } satisfies Diagnostic,
337
+ }
338
+ : {}),
339
+ };
340
+ };
341
+
342
+ const results = await mapWithConcurrency(allFiles, concurrency, processFile);
343
+ for (const r of results) {
344
+ if (r.emittedEntry) emitted.push(r.emittedEntry);
345
+ if (r.skippedEntry) skipped.push(r.skippedEntry);
346
+ if (r.diagnostic) diagnostics.push(r.diagnostic);
235
347
  }
236
348
 
237
349
  const hasFailures = emitted.some((e) => e.overall === "fail");
238
350
  const status: RfcVerificationEmitResult["status"] = hasFailures ? "fail" : "pass";
351
+ const delta = {
352
+ recovered: emitted
353
+ .filter((e) => e.previousOverall === "fail" && e.overall === "pass")
354
+ .map((e) => e.rfcId),
355
+ regressed: emitted
356
+ .filter((e) => e.previousOverall === "pass" && e.overall === "fail")
357
+ .map((e) => e.rfcId),
358
+ };
359
+ const deltaSummary =
360
+ delta.recovered.length + delta.regressed.length > 0
361
+ ? `, ${delta.regressed.length} regressed, ${delta.recovered.length} recovered`
362
+ : "";
239
363
 
240
364
  return {
241
365
  data: {
@@ -244,8 +368,9 @@ export async function runRfcVerificationEmit(
244
368
  emitted,
245
369
  skipped,
246
370
  diagnostics,
371
+ delta,
247
372
  },
248
373
  exitCode: hasFailures ? 1 : 0,
249
- summary: `rfc.verification.emit: ${emitted.length} emitted, ${skipped.length} skipped`,
374
+ summary: `rfc.verification.emit: ${emitted.length} emitted, ${skipped.length} skipped${deltaSummary}${dryRun ? ", dry-run" : ""}`,
250
375
  };
251
376
  }