@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
@@ -0,0 +1,222 @@
1
+ /*
2
+ <MODULE_CONTRACT>
3
+ <purpose>Property-based test for the spec.live.merge idempotency law (RFC-1230,
4
+ AC-10 / DNA-101): merge(merge(spec, rfc), rfc) ≡ merge(spec, rfc) — a second
5
+ merge of the same RFC is already-merged and byte-identical — and
6
+ force∘force ≡ force — a forced re-merge stabilizes after one application.</purpose>
7
+ <non-goals>
8
+ <item>Do not test rebuild or validate — covered by their own test files.</item>
9
+ </non-goals>
10
+ </MODULE_CONTRACT>
11
+ <CHANGE_SUMMARY>
12
+ <item>RFC-1230: initial PBT for the merge idempotency law.</item>
13
+ <item>RFC-1230: review findings — scoped droppedSections to namespaced headings, warn on unreadable spec, fail-fast merge on corrupt frontmatter, CHANGE_SUMMARY dedupe</item>
14
+ </CHANGE_SUMMARY>
15
+ */
16
+
17
+ import { describe, test, expect } from "vitest";
18
+ import fs from "node:fs/promises";
19
+ import path from "node:path";
20
+ import os from "node:os";
21
+ import fc from "fast-check";
22
+ import { runSpecLiveMerge } from "./live-spec-merge.ts";
23
+ import { escapeRegex } from "./live-spec-shared.ts";
24
+ import type { ForgeRuntimeContext } from "../../src/types.ts";
25
+
26
+ function makeContext(workspaceRoot: string): ForgeRuntimeContext {
27
+ return {
28
+ workspaceRoot,
29
+ logger: {
30
+ section: () => {},
31
+ info: () => {},
32
+ warn: () => {},
33
+ error: () => {},
34
+ success: () => {},
35
+ },
36
+ dryRun: false,
37
+ outputFormat: "json",
38
+ };
39
+ }
40
+
41
+ // Heading text: single-line, non-empty after trim, never carrying an (RFC-XXXX)
42
+ // marker — namespaceHeadings leaves already-marked headings alone, which would
43
+ // make sections belong to a different RFC id.
44
+ const headingTextArb = fc
45
+ .string({
46
+ unit: fc.constantFrom(
47
+ ..."abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 -_".split(""),
48
+ ),
49
+ minLength: 1,
50
+ maxLength: 40,
51
+ })
52
+ .filter((s) => s.trim().length > 0 && !/\(RFC-\d{4}\)/.test(s));
53
+
54
+ const bodyLineArb = fc
55
+ .string({
56
+ unit: fc.constantFrom(
57
+ ..."abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 -_.,;:!?()[]".split(""),
58
+ ),
59
+ maxLength: 80,
60
+ })
61
+ .filter((s) => !s.trimStart().startsWith("#"));
62
+
63
+ const sectionArb = fc.record({
64
+ heading: headingTextArb,
65
+ body: fc.array(bodyLineArb, { maxLength: 6 }).map((lines) => lines.join("\n")),
66
+ });
67
+
68
+ function rfcSource(id: string, design: string): string {
69
+ return `---
70
+ id: ${id}
71
+ title: "PBT RFC ${id}"
72
+ status: implemented
73
+ kind: architecture
74
+ scope: workspace
75
+ owners:
76
+ - architecture
77
+ reviewers:
78
+ - human:test
79
+ createdAt: 2026-08-06
80
+ updatedAt: 2026-08-06
81
+ implementedAt: 2026-08-06
82
+ versionBump: patch
83
+ liveSpec: true
84
+ packagesImpacted:
85
+ - packages/forge
86
+ commands:
87
+ proposed: []
88
+ added: []
89
+ changed: []
90
+ removed: []
91
+ appsImpacted: []
92
+ successSignals: []
93
+ nonGoals: []
94
+ ---
95
+
96
+ # ${id}
97
+
98
+ ## Design
99
+
100
+ ${design}
101
+
102
+ ## Rollout
103
+
104
+ Done.
105
+ `;
106
+ }
107
+
108
+ describe("spec.live.merge idempotency law (AC-10)", () => {
109
+ test("merge(merge(spec, rfc), rfc) ≡ merge(spec, rfc)", async () => {
110
+ await fc.assert(
111
+ fc.asyncProperty(
112
+ fc.array(sectionArb, { minLength: 1, maxLength: 6 }),
113
+ fc.boolean(), // seed a spec via a different RFC first → exercise the existing-spec path
114
+ async (sections, seedWithOther) => {
115
+ const design = sections
116
+ .map((s) => `### ${s.heading}\n\n${s.body}`)
117
+ .join("\n\n");
118
+
119
+ const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "spec-pbt-"));
120
+ try {
121
+ const rfcDir = path.join(tmpDir, "docs/rfcs");
122
+ await fs.mkdir(rfcDir, { recursive: true });
123
+ await fs.writeFile(
124
+ path.join(rfcDir, "rfc-9001-pbt.md"),
125
+ rfcSource("RFC-9001", design),
126
+ );
127
+ if (seedWithOther) {
128
+ await fs.writeFile(
129
+ path.join(rfcDir, "rfc-9002-seed.md"),
130
+ rfcSource("RFC-9002", "### Seed\n\nseed body."),
131
+ );
132
+ await runSpecLiveMerge(
133
+ { argv: [], flags: { id: "RFC-9002" } },
134
+ makeContext(tmpDir),
135
+ );
136
+ }
137
+
138
+ const mergeInput = { argv: [], flags: { id: "RFC-9001" } };
139
+ const first = await runSpecLiveMerge(mergeInput, makeContext(tmpDir));
140
+ const specFile = path.join(tmpDir, "docs/specs/live/forge.md");
141
+ const afterFirst = await fs.readFile(specFile, "utf-8");
142
+
143
+ const second = await runSpecLiveMerge(mergeInput, makeContext(tmpDir));
144
+ const afterSecond = await fs.readFile(specFile, "utf-8");
145
+
146
+ expect(first.exitCode).toBe(0);
147
+ expect(second.exitCode).toBe(0);
148
+ expect(second.data?.operation).toBe("already-merged");
149
+ expect(second.data?.deltas).toEqual([]);
150
+ expect(afterSecond).toBe(afterFirst);
151
+ } finally {
152
+ await fs.rm(tmpDir, { recursive: true, force: true });
153
+ }
154
+ },
155
+ ),
156
+ { numRuns: 30 },
157
+ );
158
+ });
159
+
160
+ test("force(force(spec)) ≡ force(spec) — forced re-merge stabilizes", async () => {
161
+ await fc.assert(
162
+ fc.asyncProperty(
163
+ fc.array(sectionArb, { minLength: 1, maxLength: 6 }),
164
+ async (sections) => {
165
+ const design = sections
166
+ .map((s) => `### ${s.heading}\n\n${s.body}`)
167
+ .join("\n\n");
168
+
169
+ const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "spec-pbt-"));
170
+ try {
171
+ const rfcDir = path.join(tmpDir, "docs/rfcs");
172
+ await fs.mkdir(rfcDir, { recursive: true });
173
+ await fs.writeFile(
174
+ path.join(rfcDir, "rfc-9001-pbt.md"),
175
+ rfcSource("RFC-9001", design),
176
+ );
177
+
178
+ await runSpecLiveMerge(
179
+ { argv: [], flags: { id: "RFC-9001" } },
180
+ makeContext(tmpDir),
181
+ );
182
+ const specFile = path.join(tmpDir, "docs/specs/live/forge.md");
183
+
184
+ const forceInput = {
185
+ argv: [],
186
+ flags: { id: "RFC-9001", force: true },
187
+ };
188
+ const force1 = await runSpecLiveMerge(forceInput, makeContext(tmpDir));
189
+ const afterForce1 = await fs.readFile(specFile, "utf-8");
190
+
191
+ const force2 = await runSpecLiveMerge(forceInput, makeContext(tmpDir));
192
+ const afterForce2 = await fs.readFile(specFile, "utf-8");
193
+
194
+ expect(force1.exitCode).toBe(0);
195
+ expect(force2.exitCode).toBe(0);
196
+ expect(force1.data?.operation).toBe("modified");
197
+ expect(force2.data?.operation).toBe("modified");
198
+ // Exactly one namespaced section set per heading and one history entry.
199
+ expect(afterForce2).toBe(afterForce1);
200
+ // Each namespaced heading occurs exactly as often as the RFC's
201
+ // design repeats that heading text (headings are trimmed on parse).
202
+ // Count heading lines — substring matching would let "a (RFC-9001)"
203
+ // match inside "aa (RFC-9001)".
204
+ for (const s of sections) {
205
+ const namespaced = `${s.heading.trim()} (RFC-9001)`;
206
+ const occurrences = (afterForce2.match(
207
+ new RegExp(`^### ${escapeRegex(namespaced)}$`, "gm"),
208
+ ) ?? []).length;
209
+ const expected = sections.filter(
210
+ (x) => x.heading.trim() === s.heading.trim(),
211
+ ).length;
212
+ expect(occurrences).toBe(expected);
213
+ }
214
+ } finally {
215
+ await fs.rm(tmpDir, { recursive: true, force: true });
216
+ }
217
+ },
218
+ ),
219
+ { numRuns: 30 },
220
+ );
221
+ });
222
+ });
@@ -7,6 +7,7 @@
7
7
  </MODULE_CONTRACT>
8
8
  <CHANGE_SUMMARY>
9
9
  <item>RFC-0711: initial unit tests for spec.live.list and spec.live.validate.</item>
10
+ <item>RFC-1234: zero-violations fixture now produced by runSpecLiveMerge so the spec is byte-canonical under the V-LS-09 replay check; RFC-9001 fixture gained liveSpec: true.</item>
10
11
  </CHANGE_SUMMARY>
11
12
  */
12
13
 
@@ -17,6 +18,7 @@ import os from "node:os";
17
18
  import { runSpecLiveList } from "./live-spec-list.ts";
18
19
  import { runSpecLiveShow } from "./live-spec-show.ts";
19
20
  import { runSpecLiveValidate } from "./live-spec-validate.ts";
21
+ import { runSpecLiveMerge } from "./live-spec-merge.ts";
20
22
  import type { ForgeCommandInput, ForgeRuntimeContext } from "../../src/types.ts";
21
23
 
22
24
  function makeContext(workspaceRoot: string): ForgeRuntimeContext {
@@ -157,7 +159,9 @@ createdAt: 2026-08-06
157
159
  updatedAt: 2026-08-06
158
160
  implementedAt: 2026-08-06
159
161
  versionBump: patch
160
- packagesImpacted: []
162
+ liveSpec: true
163
+ packagesImpacted:
164
+ - packages/forge
161
165
  commands:
162
166
  proposed: []
163
167
  added: []
@@ -184,13 +188,13 @@ Content.
184
188
  });
185
189
 
186
190
  it("passes with zero violations on a valid living spec", async () => {
187
- await fs.writeFile(
188
- path.join(tmpDir, "docs/specs/live/forge.md"),
189
- VALID_SPEC,
190
- );
191
+ // V-LS-09 (RFC-1234) byte-compares against the canonical replay — the
192
+ // fixture spec is produced by the real merge writer, not hand-written.
193
+ const ctx = makeContext(tmpDir);
194
+ await runSpecLiveMerge({ argv: [], flags: { id: "RFC-9001" } }, ctx);
191
195
 
192
196
  const input: ForgeCommandInput = { argv: [], flags: {} };
193
- const result = await runSpecLiveValidate(input, makeContext(tmpDir));
197
+ const result = await runSpecLiveValidate(input, ctx);
194
198
 
195
199
  expect(result.exitCode).toBe(0);
196
200
  expect(result.data?.status).toBe("pass");
@@ -1,13 +1,17 @@
1
1
  /*
2
2
  <MODULE_CONTRACT>
3
3
  <purpose>Unit tests for spec.live.merge handler — covers creation, modification,
4
- conflict detection, dry-run, no-op cases (RFC-0711).</purpose>
4
+ idempotent already-merged skip, --force re-merge, conflict detection, dry-run,
5
+ no-op cases (RFC-0711, RFC-1230).</purpose>
5
6
  <non-goals>
6
7
  <item>Do not test docs.archive integration — covered separately.</item>
8
+ <item>Do not test spec.live.rebuild — covered by live-spec-rebuild.test.ts.</item>
7
9
  </non-goals>
8
10
  </MODULE_CONTRACT>
9
11
  <CHANGE_SUMMARY>
10
12
  <item>RFC-0711: initial unit tests for spec.live.merge.</item>
13
+ <item>RFC-1230: updated re-merge expectations to already-merged; added --force surgical re-merge and byte-identity cases.</item>
14
+ <item>RFC-1230: review findings — scoped droppedSections to namespaced headings, warn on unreadable spec, fail-fast merge on corrupt frontmatter, CHANGE_SUMMARY dedupe</item>
11
15
  </CHANGE_SUMMARY>
12
16
  */
13
17
 
@@ -201,6 +205,57 @@ Different types from RFC-9004.
201
205
  Some rollout.
202
206
  `;
203
207
 
208
+ const SAMPLE_RFC_FENCED = `---
209
+ id: RFC-9005
210
+ title: "RFC with fenced heading-like lines"
211
+ status: implemented
212
+ kind: architecture
213
+ scope: workspace
214
+ owners:
215
+ - architecture
216
+ reviewers:
217
+ - human:test
218
+ createdAt: 2026-08-06
219
+ updatedAt: 2026-08-06
220
+ implementedAt: 2026-08-06
221
+ versionBump: patch
222
+ liveSpec: true
223
+ packagesImpacted:
224
+ - packages/forge
225
+ commands:
226
+ proposed: []
227
+ added: []
228
+ changed: []
229
+ removed: []
230
+ appsImpacted: []
231
+ successSignals: []
232
+ nonGoals: []
233
+ ---
234
+
235
+ # RFC-9005: RFC with fenced heading-like lines
236
+
237
+ ## Design
238
+
239
+ ### CLI surface
240
+
241
+ \`\`\`sh
242
+ ### this is a shell comment, not a heading
243
+ werkstatt run spec.live.merge --id RFC-9005
244
+ \`\`\`
245
+
246
+ ### TypeScript contracts
247
+
248
+ ~~~text
249
+ ### markdown-looking line inside tilde fence
250
+ ~~~
251
+
252
+ Real text.
253
+
254
+ ## Rollout
255
+
256
+ Done.
257
+ `;
258
+
204
259
  async function setupWorkspace(): Promise<string> {
205
260
  const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "spec-live-test-"));
206
261
  const rfcDir = path.join(tmpDir, "docs/rfcs");
@@ -209,6 +264,7 @@ async function setupWorkspace(): Promise<string> {
209
264
  await fs.writeFile(path.join(rfcDir, "rfc-9002-test-rfc-without-livespec.md"), SAMPLE_RFC_NO_LIVE_SPEC);
210
265
  await fs.writeFile(path.join(rfcDir, "rfc-9003-rejected-rfc-with-livespec.md"), SAMPLE_RFC_REJECTED);
211
266
  await fs.writeFile(path.join(rfcDir, "rfc-9004-second-rfc-with-same-headings.md"), SAMPLE_RFC_9004);
267
+ await fs.writeFile(path.join(rfcDir, "rfc-9005-rfc-with-fenced-heading-like-lines.md"), SAMPLE_RFC_FENCED);
212
268
  return tmpDir;
213
269
  }
214
270
 
@@ -243,18 +299,61 @@ describe("spec.live.merge", () => {
243
299
  expect(content).toContain("lastMergedRfc: RFC-9001");
244
300
  });
245
301
 
246
- it("modifies an existing living spec", async () => {
302
+ it("skips an already-merged RFC and leaves the spec byte-identical (AC-1)", async () => {
247
303
  // First merge creates the spec
248
304
  const input1: ForgeCommandInput = { argv: [], flags: { id: "RFC-9001" } };
249
305
  await runSpecLiveMerge(input1, makeContext(tmpDir));
250
306
 
251
- // Second merge modifies it
307
+ const specFile = path.join(tmpDir, "docs/specs/live/forge.md");
308
+ const before = await fs.readFile(specFile, "utf-8");
309
+
310
+ // Second merge of the same RFC is a no-op
252
311
  const input2: ForgeCommandInput = { argv: [], flags: { id: "RFC-9001" } };
253
312
  const result = await runSpecLiveMerge(input2, makeContext(tmpDir));
254
313
 
255
314
  expect(result.exitCode).toBe(0);
256
- expect(result.data?.operation).toBe("modified");
315
+ expect(result.data?.operation).toBe("already-merged");
257
316
  expect(result.data?.domain).toBe("forge");
317
+ expect(result.data?.deltas).toEqual([]);
318
+
319
+ const after = await fs.readFile(specFile, "utf-8");
320
+ expect(after).toBe(before);
321
+ });
322
+
323
+ it("re-merges an already-merged RFC surgically with --force (AC-2)", async () => {
324
+ await runSpecLiveMerge({ argv: [], flags: { id: "RFC-9001" } }, makeContext(tmpDir));
325
+ await runSpecLiveMerge({ argv: [], flags: { id: "RFC-9004" } }, makeContext(tmpDir));
326
+
327
+ const specFile = path.join(tmpDir, "docs/specs/live/forge.md");
328
+
329
+ // Simulate prior corruption: a duplicated RFC-9001 section + history entry.
330
+ const corrupted = (await fs.readFile(specFile, "utf-8"))
331
+ .replace(
332
+ "### TypeScript contracts (RFC-9001)\n\nSome types.",
333
+ "### TypeScript contracts (RFC-9001)\n\nSome types.\n\n### CLI surface (RFC-9001)\n\nDuplicated stale section.",
334
+ )
335
+ .replace(
336
+ " - rfc: RFC-9004",
337
+ " - rfc: RFC-9001\n mergedAt: 2026-08-07\n operation: modified\n - rfc: RFC-9004",
338
+ );
339
+ await fs.writeFile(specFile, corrupted);
340
+
341
+ const result = await runSpecLiveMerge(
342
+ { argv: [], flags: { id: "RFC-9001", force: true } },
343
+ makeContext(tmpDir),
344
+ );
345
+
346
+ expect(result.exitCode).toBe(0);
347
+ expect(result.data?.operation).toBe("modified");
348
+
349
+ const after = await fs.readFile(specFile, "utf-8");
350
+ // Exactly one section set for RFC-9001 remains; stale duplicate is gone.
351
+ expect(after.match(/\(RFC-9001\)/g)?.length).toBe(3);
352
+ expect(after).not.toContain("Duplicated stale section.");
353
+ // Exactly one history entry for RFC-9001.
354
+ expect(after.match(/rfc: RFC-9001/g)?.length).toBe(1);
355
+ // RFC-9004 sections untouched.
356
+ expect(after).toContain("CLI surface (RFC-9004)");
258
357
  });
259
358
 
260
359
  it("skips RFCs without liveSpec field (no-op)", async () => {
@@ -323,15 +422,96 @@ describe("spec.live.merge", () => {
323
422
  expect(content).toContain("CLI surface (RFC-9004)");
324
423
  });
325
424
 
326
- it("re-merges namespaced headings without conflicts on subsequent docs.archive runs", async () => {
425
+ it("reports already-merged on re-run instead of duplicating sections (RFC-1230)", async () => {
327
426
  // Initial merges
328
427
  await runSpecLiveMerge({ argv: [], flags: { id: "RFC-9001" } }, makeContext(tmpDir));
329
428
  await runSpecLiveMerge({ argv: [], flags: { id: "RFC-9004" } }, makeContext(tmpDir));
330
429
 
331
- // Re-merge RFC-9001 (simulates docs.archive re-running all merges)
430
+ // Re-merge RFC-9001 (simulates a repeated caller) — idempotent no-op
332
431
  const result = await runSpecLiveMerge({ argv: [], flags: { id: "RFC-9001" } }, makeContext(tmpDir));
333
432
 
334
433
  expect(result.exitCode).toBe(0);
335
- expect(result.data?.conflicts.length).toBe(0);
434
+ expect(result.data?.operation).toBe("already-merged");
435
+ expect(result.data?.deltas).toEqual([]);
436
+ });
437
+
438
+ it("writes nothing under --dry-run on an existing spec (AC-9)", async () => {
439
+ await runSpecLiveMerge({ argv: [], flags: { id: "RFC-9001" } }, makeContext(tmpDir));
440
+ const specFile = path.join(tmpDir, "docs/specs/live/forge.md");
441
+ const before = await fs.readFile(specFile, "utf-8");
442
+
443
+ const ctx = makeContext(tmpDir);
444
+ ctx.dryRun = true;
445
+ const result = await runSpecLiveMerge(
446
+ { argv: [], flags: { id: "RFC-9001", force: true, "dry-run": true } },
447
+ ctx,
448
+ );
449
+
450
+ expect(result.data?.dryRun).toBe(true);
451
+ const after = await fs.readFile(specFile, "utf-8");
452
+ expect(after).toBe(before);
453
+ });
454
+
455
+ it("does not namespace-rewrite ### lines inside ``` and ~~~ fences (RFC-1232 AC-1/AC-5)", async () => {
456
+ const result = await runSpecLiveMerge(
457
+ { argv: [], flags: { id: "RFC-9005" } },
458
+ makeContext(tmpDir),
459
+ );
460
+
461
+ expect(result.exitCode).toBe(0);
462
+
463
+ const content = await fs.readFile(path.join(tmpDir, "docs/specs/live/forge.md"), "utf-8");
464
+ // Fence-interior ### lines pass through verbatim — no namespacing suffix.
465
+ expect(content).toContain("### this is a shell comment, not a heading\n");
466
+ expect(content).toContain("### markdown-looking line inside tilde fence\n");
467
+ expect(content).not.toContain("shell comment, not a heading (RFC-9005)");
468
+ expect(content).not.toContain("inside tilde fence (RFC-9005)");
469
+ // Real headings still get namespaced.
470
+ expect(content).toContain("### CLI surface (RFC-9005)");
471
+ expect(content).toContain("### TypeScript contracts (RFC-9005)");
472
+ });
473
+
474
+ it("--force re-merge preserves fence-interior ### (RFC-NNNN) lines outside the RFC's sections", async () => {
475
+ await runSpecLiveMerge({ argv: [], flags: { id: "RFC-9005" } }, makeContext(tmpDir));
476
+
477
+ const specFile = path.join(tmpDir, "docs/specs/live/forge.md");
478
+ const before = await fs.readFile(specFile, "utf-8");
479
+ // A fenced block carrying a phantom namespaced heading inside Overview —
480
+ // the pre-RFC-1232 scanner would slice from this line to the next real
481
+ // heading, deleting Overview tail content and leaving a stray fence.
482
+ await fs.writeFile(
483
+ specFile,
484
+ before.replace(
485
+ "## Overview\n",
486
+ "## Overview\n\n```md\n### Phantom (RFC-9005)\n```\n",
487
+ ),
488
+ );
489
+
490
+ const result = await runSpecLiveMerge(
491
+ { argv: [], flags: { id: "RFC-9005", force: true } },
492
+ makeContext(tmpDir),
493
+ );
494
+
495
+ expect(result.exitCode).toBe(0);
496
+ const after = await fs.readFile(specFile, "utf-8");
497
+ expect(after).toContain("### Phantom (RFC-9005)");
498
+ expect(after.match(/### CLI surface \(RFC-9005\)/g)?.length).toBe(1);
499
+ expect(after.match(/### TypeScript contracts \(RFC-9005\)/g)?.length).toBe(1);
500
+ });
501
+
502
+ it("fails instead of overwriting a spec with unparseable frontmatter", async () => {
503
+ const specFile = path.join(tmpDir, "docs/specs/live/forge.md");
504
+ await fs.mkdir(path.dirname(specFile), { recursive: true });
505
+ const corrupt = "# Living Spec: forge\n\nno frontmatter — orphaned content\n";
506
+ await fs.writeFile(specFile, corrupt);
507
+
508
+ const result = await runSpecLiveMerge(
509
+ { argv: [], flags: { id: "RFC-9001" } },
510
+ makeContext(tmpDir),
511
+ );
512
+
513
+ expect(result.exitCode).toBe(1);
514
+ expect(result.summary).toContain("valid frontmatter");
515
+ expect(await fs.readFile(specFile, "utf-8")).toBe(corrupt);
336
516
  });
337
517
  });