@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,378 @@
1
+ /*
2
+ <MODULE_CONTRACT>
3
+ <purpose>Shared pure helpers for living feature specs — heading parsing, RFC-namespaced
4
+ section mechanics, spec (de)serialization, domain derivation, and the docs.archive
5
+ this-run merge-target selection (RFC-0711, RFC-0957, RFC-1230).</purpose>
6
+ <non-goals>
7
+ <item>Do not implement command handlers — spec.live.merge/validate/rebuild handlers live in their own files.</item>
8
+ <item>Do not touch the filesystem — these helpers are pure; IO stays in handlers.</item>
9
+ </non-goals>
10
+ <CHANGE_SUMMARY>
11
+ <item>RFC-1230: extracted shared helpers from live-spec-merge.ts; added removeNamespacedSections, seedSpecPrefix, and collectLiveMergeTargets for the idempotent-merge + rebuild design.</item>
12
+ <item>RFC-1230: review findings — scoped droppedSections to namespaced headings, warn on unreadable spec, fail-fast merge on corrupt frontmatter, CHANGE_SUMMARY dedupe</item>
13
+ <item>RFC-1232: fence-aware heading mechanics — private fencedLineIndexes (CommonMark-subset ``` / ~~~ map) consumed by parseHeadings, extractDesignSection, namespaceHeadings, removeNamespacedSections, and applyDeltasToSpecBody (regex lookaheads replaced with line-slice boundaries).</item>
14
+ <item>RFC-1235: extracted docs.archive merge-outcome accounting — classifyLiveMergeOutcome, liveMergeFailureEntry, formatLiveMergeFailure, buildLiveMergeBlock — so exit-nonzero merges record to failed[] instead of merged[].</item>
15
+ </CHANGE_SUMMARY>
16
+ */
17
+
18
+ import path from "node:path";
19
+ import YAML from "yaml";
20
+ import { listRfcFiles } from "../rfc/frontmatter-io.ts";
21
+ import type {
22
+ LivingSpec,
23
+ DeltaOperation,
24
+ } from "./live-spec-types.ts";
25
+
26
+ export const LIVE_SPECS_DIR = "docs/specs/live";
27
+
28
+ export interface ParsedHeading {
29
+ level: number;
30
+ text: string;
31
+ body: string;
32
+ }
33
+
34
+ // RFC-1232: CommonMark-subset fence map — line indexes inside fenced code
35
+ // blocks. A run of ` or ~ of length >=3 with <=3 leading spaces opens a fence;
36
+ // the same-or-longer run of the same character closes it. Opener and closer
37
+ // lines count as interior. An unterminated fence makes the rest of the input
38
+ // interior (CommonMark behavior). Indented code blocks are out of scope.
39
+ const FENCE_OPEN_REGEX = /^ {0,3}(`{3,}|~{3,})/;
40
+ // CommonMark: a closing fence carries no info string — only trailing
41
+ // whitespace. A same-char run followed by text is interior content, so the
42
+ // map over-masks rather than exposing a would-be heading (safe direction).
43
+ // CRLF note: `[ \t]*$` deliberately excludes \r, so a CRLF closer never
44
+ // matches and the tail stays interior — over-masking is the safe direction
45
+ // and repo content is LF-normalized.
46
+ const FENCE_CLOSE_REGEX = /^ {0,3}(`{3,}|~{3,})[ \t]*$/;
47
+ function fencedLineIndexes(lines: string[]): ReadonlySet<number> {
48
+ const inside = new Set<number>();
49
+ let fenceChar: "`" | "~" | null = null;
50
+ let fenceLength = 0;
51
+ for (let i = 0; i < lines.length; i++) {
52
+ const match = lines[i]!.match(FENCE_OPEN_REGEX);
53
+ if (fenceChar === null) {
54
+ if (match) {
55
+ fenceChar = match[1]![0] as "`" | "~";
56
+ fenceLength = match[1]!.length;
57
+ inside.add(i);
58
+ }
59
+ continue;
60
+ }
61
+ inside.add(i);
62
+ const closeMatch = lines[i]!.match(FENCE_CLOSE_REGEX);
63
+ if (closeMatch && closeMatch[1]![0] === fenceChar && closeMatch[1]!.length >= fenceLength) {
64
+ fenceChar = null;
65
+ fenceLength = 0;
66
+ }
67
+ }
68
+ return inside;
69
+ }
70
+
71
+ export function extractDesignSection(rfcBody: string): string {
72
+ const lines = rfcBody.split("\n");
73
+ const fenced = fencedLineIndexes(lines);
74
+ let startLine = -1;
75
+ for (let i = 0; i < lines.length; i++) {
76
+ if (!fenced.has(i) && /^##\s+Design\s*$/.test(lines[i]!)) {
77
+ startLine = i;
78
+ break;
79
+ }
80
+ }
81
+ if (startLine === -1) return "";
82
+ let endLine = lines.length;
83
+ for (let i = startLine + 1; i < lines.length; i++) {
84
+ if (!fenced.has(i) && /^##\s/.test(lines[i]!)) {
85
+ endLine = i;
86
+ break;
87
+ }
88
+ }
89
+ return lines.slice(startLine + 1, endLine).join("\n").trim();
90
+ }
91
+
92
+ export function parseHeadings(content: string): ParsedHeading[] {
93
+ const lines = content.split("\n");
94
+ const fenced = fencedLineIndexes(lines);
95
+ const headings: ParsedHeading[] = [];
96
+ let currentHeading: ParsedHeading | null = null;
97
+ let currentBody: string[] = [];
98
+
99
+ for (let i = 0; i < lines.length; i++) {
100
+ const line = lines[i]!;
101
+ const headingMatch = fenced.has(i) ? null : line.match(/^(#{3,})\s+(.+)$/);
102
+ if (headingMatch) {
103
+ if (currentHeading) {
104
+ currentHeading.body = currentBody.join("\n").trim();
105
+ headings.push(currentHeading);
106
+ }
107
+ currentHeading = {
108
+ level: headingMatch[1].length,
109
+ text: headingMatch[2].trim(),
110
+ body: "",
111
+ };
112
+ currentBody = [];
113
+ } else if (currentHeading) {
114
+ currentBody.push(line);
115
+ }
116
+ }
117
+ if (currentHeading) {
118
+ currentHeading.body = currentBody.join("\n").trim();
119
+ headings.push(currentHeading);
120
+ }
121
+ return headings;
122
+ }
123
+
124
+ export function deriveDomain(frontmatter: Record<string, unknown>): string | null {
125
+ const liveSpec = frontmatter["liveSpec"];
126
+ if (typeof liveSpec === "string" && liveSpec.length > 0) {
127
+ return liveSpec;
128
+ }
129
+ if (liveSpec === true) {
130
+ const packagesImpacted = frontmatter["packagesImpacted"];
131
+ if (Array.isArray(packagesImpacted) && packagesImpacted.length > 0) {
132
+ const firstPkg = String(packagesImpacted[0]);
133
+ return firstPkg.replace(/^packages\//, "").replace(/^@[^/]+\//, "");
134
+ }
135
+ return null;
136
+ }
137
+ return null;
138
+ }
139
+
140
+ export function parseLivingSpec(content: string): LivingSpec | null {
141
+ const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
142
+ if (!match) return null;
143
+ const fm = (YAML.parse(match[1]!) ?? {}) as Record<string, unknown>;
144
+ return {
145
+ domain: String(fm["domain"] ?? ""),
146
+ title: String(fm["title"] ?? ""),
147
+ lastMergedRfc: String(fm["lastMergedRfc"] ?? ""),
148
+ updatedAt: String(fm["updatedAt"] ?? ""),
149
+ createdAt: String(fm["createdAt"] ?? ""),
150
+ history: (Array.isArray(fm["history"]) ? fm["history"] : []) as LivingSpec["history"],
151
+ body: match[2] ?? "",
152
+ };
153
+ }
154
+
155
+ export function serializeLivingSpec(spec: LivingSpec): string {
156
+ const fm: Record<string, unknown> = {
157
+ domain: spec.domain,
158
+ title: spec.title,
159
+ lastMergedRfc: spec.lastMergedRfc,
160
+ updatedAt: spec.updatedAt,
161
+ createdAt: spec.createdAt,
162
+ history: spec.history,
163
+ };
164
+ const fmStr = YAML.stringify(fm).trimEnd();
165
+ return `---\n${fmStr}\n---\n\n${spec.body}`;
166
+ }
167
+
168
+ export function findHeadingInSpec(spec: LivingSpec, headingText: string): ParsedHeading | null {
169
+ const headings = parseHeadings(spec.body);
170
+ return headings.find((h) => h.text === headingText) ?? null;
171
+ }
172
+
173
+ export function applyDeltasToSpecBody(
174
+ specBody: string,
175
+ deltas: ParsedHeading[],
176
+ operations: DeltaOperation[],
177
+ ): string {
178
+ const lines = specBody.split("\n");
179
+ for (const delta of deltas) {
180
+ const op = operations.find((o) => o.heading === delta.text);
181
+ if (!op) continue;
182
+
183
+ if (op.type === "added") {
184
+ lines.push("", `${"#".repeat(delta.level)} ${delta.text}`, "", ...delta.body.split("\n"));
185
+ continue;
186
+ }
187
+
188
+ // RFC-1232: line-slice boundaries instead of `(?=#{3,}|$)` regex lookaheads —
189
+ // those were unanchored and matched `###` inside fenced code blocks.
190
+ const fenced = fencedLineIndexes(lines);
191
+ let start = -1;
192
+ let next = lines.length;
193
+ for (let i = 0; i < lines.length; i++) {
194
+ if (fenced.has(i)) continue;
195
+ const m = lines[i]!.match(/^(#{3,})\s+(.+)$/);
196
+ if (!m) continue;
197
+ if (start === -1) {
198
+ if (m[1]!.length === delta.level && m[2]!.trim() === delta.text) start = i;
199
+ } else {
200
+ next = i;
201
+ break;
202
+ }
203
+ }
204
+ if (start === -1) continue;
205
+
206
+ if (op.type === "modified") {
207
+ lines.splice(start + 1, next - start - 1, "", ...delta.body.split("\n"));
208
+ } else {
209
+ // removed — absorb the blank lines separating the heading from the
210
+ // preceding content (old `\n*` regex prefix), then collapse the section
211
+ // to a single empty line.
212
+ let removeFrom = start;
213
+ while (removeFrom > 0 && lines[removeFrom - 1]!.trim() === "") removeFrom--;
214
+ lines.splice(removeFrom, next - removeFrom, "");
215
+ }
216
+ }
217
+ return lines.join("\n").trim();
218
+ }
219
+
220
+ export function escapeRegex(str: string): string {
221
+ return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
222
+ }
223
+
224
+ export function namespaceHeadings(content: string, rfcId: string): string {
225
+ const lines = content.split("\n");
226
+ const fenced = fencedLineIndexes(lines);
227
+ return lines
228
+ .map((line, i) => {
229
+ if (fenced.has(i)) return line;
230
+ const m = line.match(/^(#{3,})\s+(.+)$/);
231
+ if (!m) return line;
232
+ // parseHeadings trims heading text — normalize here too so a heading like
233
+ // "### Foo " round-trips to "### Foo (RFC-XXXX)", not "### Foo (RFC-XXXX)".
234
+ const normalized = m[2]!.trim();
235
+ if (/\(RFC-\d{4}\)$/.test(normalized)) return `${m[1]} ${normalized}`;
236
+ return `${m[1]} ${normalized} (${rfcId})`;
237
+ })
238
+ .join("\n");
239
+ }
240
+
241
+ // RFC-1230: strip every ###+ section whose heading carries the ` (RFC-XXXX)`
242
+ // marker. Line-based and byte-preserving for kept sections — parseHeadings is
243
+ // lossy on whitespace, so surgical replacement filters lines instead.
244
+ export function removeNamespacedSections(body: string, rfcId: string): string {
245
+ const suffix = ` (${rfcId})`;
246
+ const lines = body.split("\n");
247
+ const fenced = fencedLineIndexes(lines);
248
+ const out: string[] = [];
249
+ let skipping = false;
250
+ for (let i = 0; i < lines.length; i++) {
251
+ const line = lines[i]!;
252
+ const headingMatch = fenced.has(i) ? null : line.match(/^(#{3,})\s+(.+)$/);
253
+ if (headingMatch) {
254
+ skipping = headingMatch[2]!.trim().endsWith(suffix);
255
+ }
256
+ if (!skipping) out.push(line);
257
+ }
258
+ return out.join("\n").replace(/\n{3,}/g, "\n\n").trim();
259
+ }
260
+
261
+ export function findRfcFile(rfcDir: string, rfcId: string): Promise<string | null> {
262
+ return (async () => {
263
+ const files = await listRfcFiles(rfcDir);
264
+ for (const file of files) {
265
+ const basename = path.basename(file);
266
+ if (basename.toLowerCase().startsWith(rfcId.toLowerCase())) {
267
+ return file;
268
+ }
269
+ }
270
+ return null;
271
+ })();
272
+ }
273
+
274
+ // Canonical spec seed — identical to what the spec.live.merge creation path
275
+ // emits, so rebuild and first-merge produce the same skeleton.
276
+ export function seedSpecPrefix(domain: string, generatedHeader: string): string {
277
+ return `${generatedHeader}# Living Spec: ${domain}\n\n## Overview\n\n`;
278
+ }
279
+
280
+ export interface LiveMergeTargetSelection {
281
+ // RFCs moved into archive/implemented/ this run — merge candidates (still
282
+ // gated on a liveSpec frontmatter check by the caller).
283
+ candidates: string[];
284
+ // RFCs moved into archive/rejected/ this run — counted as skipped when they
285
+ // carry liveSpec (a rejected RFC must never merge).
286
+ rejected: string[];
287
+ }
288
+
289
+ // RFC-1230: the docs.archive post-loop merges only RFCs moved this run — the
290
+ // selection derives from rfc.archive's `moved` result, not a rescan of the
291
+ // whole docs/rfcs tree (which is what caused the duplicate-merge corruption).
292
+ export function collectLiveMergeTargets(
293
+ moved: ReadonlyArray<{ id: string; status: string; direction: string }>,
294
+ ): LiveMergeTargetSelection {
295
+ const candidates: string[] = [];
296
+ const rejected: string[] = [];
297
+ for (const m of moved) {
298
+ if (m.direction !== "into-archive") continue;
299
+ if (m.status === "implemented") candidates.push(m.id);
300
+ else if (m.status === "rejected") rejected.push(m.id);
301
+ }
302
+ return { candidates, rejected };
303
+ }
304
+
305
+ // ── RFC-1235: docs.archive post-loop merge-outcome accounting ────────────────
306
+ // spec.live.merge can return exitCode !== 0 with a populated data object
307
+ // (RFC-1230 fail-fast on corrupt spec frontmatter) — the post-loop must record
308
+ // those distinctly instead of inflating merged[]. Helper-local shapes only;
309
+ // spec.live.merge's own result contract is untouched.
310
+
311
+ export interface LiveMergeEntry {
312
+ id: string;
313
+ domain: string;
314
+ operation: string;
315
+ conflicts: number;
316
+ }
317
+
318
+ export interface LiveMergeFailure {
319
+ id: string;
320
+ exitCode?: number;
321
+ error: string;
322
+ }
323
+
324
+ export type LiveMergeOutcome =
325
+ | { kind: "merged"; entry: LiveMergeEntry }
326
+ | { kind: "failed"; entry: LiveMergeFailure };
327
+
328
+ export function classifyLiveMergeOutcome(
329
+ rfcId: string,
330
+ result: { exitCode?: number; data?: unknown; summary?: string },
331
+ ): LiveMergeOutcome {
332
+ const data = result.data as
333
+ | { domain?: unknown; operation?: unknown; conflicts?: unknown }
334
+ | undefined;
335
+ if (result.exitCode === 0 && data) {
336
+ return {
337
+ kind: "merged",
338
+ entry: {
339
+ id: rfcId,
340
+ domain: String(data.domain ?? ""),
341
+ operation: String(data.operation ?? ""),
342
+ conflicts: Array.isArray(data.conflicts) ? data.conflicts.length : 0,
343
+ },
344
+ };
345
+ }
346
+ return {
347
+ kind: "failed",
348
+ entry: {
349
+ id: rfcId,
350
+ exitCode: result.exitCode,
351
+ error:
352
+ result.summary ??
353
+ (result.exitCode === undefined
354
+ ? "spec.live.merge returned no exit code"
355
+ : `spec.live.merge exited ${result.exitCode}`),
356
+ },
357
+ };
358
+ }
359
+
360
+ export function liveMergeFailureEntry(rfcId: string, error: unknown): LiveMergeFailure {
361
+ return { id: rfcId, error: String(error instanceof Error ? error.message : error) };
362
+ }
363
+
364
+ export function formatLiveMergeFailure(entry: LiveMergeFailure): string {
365
+ return `spec.live.merge: failed for ${entry.id}: ${entry.error}`;
366
+ }
367
+
368
+ /** Emission gate + block assembly for results["spec.live.merge"] — undefined
369
+ * when nothing was attempted, so an all-failed run still surfaces. */
370
+ export function buildLiveMergeBlock(
371
+ merged: LiveMergeEntry[],
372
+ failed: LiveMergeFailure[],
373
+ skipped: number,
374
+ dryRun: boolean,
375
+ ): { merged: LiveMergeEntry[]; failed: LiveMergeFailure[]; skipped: number; dryRun: boolean } | undefined {
376
+ if (merged.length === 0 && failed.length === 0 && skipped === 0) return undefined;
377
+ return { merged, failed, skipped, dryRun };
378
+ }
@@ -8,6 +8,9 @@ merge results, and validation (RFC-0711).</purpose>
8
8
  </MODULE_CONTRACT>
9
9
  <CHANGE_SUMMARY>
10
10
  <item>RFC-0711: initial living spec types — LivingSpec, DeltaOperation, DeltaConflict, merge/list/show/validate result types.</item>
11
+ <item>RFC-1230: added already-merged to SpecLiveMergeResult.operation; added SpecLiveRebuildResult for spec.live.rebuild.</item>
12
+ <item>RFC-1230: review findings — scoped droppedSections to namespaced headings, warn on unreadable spec, fail-fast merge on corrupt frontmatter, CHANGE_SUMMARY dedupe</item>
13
+ <item>RFC-1234: LivingSpecViolation gains optional severity field (absent = error; errors drive exit code); uniqueRfcs contract comment states deduplicated-history semantics.</item>
11
14
  </CHANGE_SUMMARY>
12
15
  */
13
16
 
@@ -47,12 +50,26 @@ export interface DeltaConflict {
47
50
  export interface SpecLiveMergeResult {
48
51
  command: "spec.live.merge";
49
52
  domain: string;
50
- operation: "created" | "modified";
53
+ operation: "created" | "modified" | "already-merged";
51
54
  deltas: DeltaOperation[];
52
55
  conflicts: DeltaConflict[];
53
56
  dryRun: boolean;
54
57
  }
55
58
 
59
+ export interface SpecLiveRebuildResult {
60
+ command: "spec.live.rebuild";
61
+ domain: string;
62
+ operation: "rebuilt" | "unchanged" | "skipped";
63
+ /** Unique RFC ids present in the deduplicated history — unreadable or
64
+ * not-implemented ids are still counted; the replayed count is
65
+ * uniqueRfcs − unreadableRfcs.length. */
66
+ uniqueRfcs: number;
67
+ droppedHistoryEntries: number;
68
+ droppedSections: number;
69
+ unreadableRfcs: string[];
70
+ dryRun: boolean;
71
+ }
72
+
56
73
  export interface SpecLiveListEntry {
57
74
  domain: string;
58
75
  title: string;
@@ -83,6 +100,9 @@ export interface LivingSpecViolation {
83
100
  rule: string;
84
101
  message: string;
85
102
  domain?: string;
103
+ /** Absent === "error"; only error-severity violations drive a non-zero
104
+ * exit code (RFC-1234). */
105
+ severity?: "error" | "warning";
86
106
  }
87
107
 
88
108
  export interface SpecLiveValidateResult {