@seanmars/tospec 0.19.0-beta.8 → 0.21.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 (423) hide show
  1. package/CHANGELOG.md +89 -296
  2. package/README.md +69 -82
  3. package/assets/dashboard/app.js +11 -0
  4. package/assets/dashboard/style.css +7 -0
  5. package/bin/tospec.js +1 -1
  6. package/dist/cli/index.d.ts +6 -1
  7. package/dist/cli/index.d.ts.map +1 -1
  8. package/dist/cli/index.js +120 -112
  9. package/dist/cli/index.js.map +1 -1
  10. package/dist/commands/config.d.ts +9 -17
  11. package/dist/commands/config.d.ts.map +1 -1
  12. package/dist/commands/config.js +300 -145
  13. package/dist/commands/config.js.map +1 -1
  14. package/dist/commands/dashboard.d.ts +57 -99
  15. package/dist/commands/dashboard.d.ts.map +1 -1
  16. package/dist/commands/dashboard.js +252 -330
  17. package/dist/commands/dashboard.js.map +1 -1
  18. package/dist/commands/decision.d.ts +38 -27
  19. package/dist/commands/decision.d.ts.map +1 -1
  20. package/dist/commands/decision.js +301 -131
  21. package/dist/commands/decision.js.map +1 -1
  22. package/dist/commands/metrics.d.ts +28 -51
  23. package/dist/commands/metrics.d.ts.map +1 -1
  24. package/dist/commands/metrics.js +62 -93
  25. package/dist/commands/metrics.js.map +1 -1
  26. package/dist/commands/shared-output.d.ts +12 -27
  27. package/dist/commands/shared-output.d.ts.map +1 -1
  28. package/dist/commands/shared-output.js +22 -45
  29. package/dist/commands/shared-output.js.map +1 -1
  30. package/dist/commands/show.d.ts +4 -7
  31. package/dist/commands/show.d.ts.map +1 -1
  32. package/dist/commands/show.js +23 -11
  33. package/dist/commands/show.js.map +1 -1
  34. package/dist/commands/validate.d.ts +34 -58
  35. package/dist/commands/validate.d.ts.map +1 -1
  36. package/dist/commands/validate.js +228 -142
  37. package/dist/commands/validate.js.map +1 -1
  38. package/dist/commands/workflow/index.d.ts +1 -5
  39. package/dist/commands/workflow/index.d.ts.map +1 -1
  40. package/dist/commands/workflow/index.js +1 -5
  41. package/dist/commands/workflow/index.js.map +1 -1
  42. package/dist/commands/workflow/instructions.d.ts +14 -24
  43. package/dist/commands/workflow/instructions.d.ts.map +1 -1
  44. package/dist/commands/workflow/instructions.js +230 -132
  45. package/dist/commands/workflow/instructions.js.map +1 -1
  46. package/dist/commands/workflow/new-change.d.ts +2 -5
  47. package/dist/commands/workflow/new-change.d.ts.map +1 -1
  48. package/dist/commands/workflow/new-change.js +75 -34
  49. package/dist/commands/workflow/new-change.js.map +1 -1
  50. package/dist/commands/workflow/schemas.d.ts +1 -5
  51. package/dist/commands/workflow/schemas.d.ts.map +1 -1
  52. package/dist/commands/workflow/schemas.js +6 -17
  53. package/dist/commands/workflow/schemas.js.map +1 -1
  54. package/dist/commands/workflow/shared.d.ts +37 -42
  55. package/dist/commands/workflow/shared.d.ts.map +1 -1
  56. package/dist/commands/workflow/shared.js +23 -54
  57. package/dist/commands/workflow/shared.js.map +1 -1
  58. package/dist/commands/workflow/status.d.ts +7 -17
  59. package/dist/commands/workflow/status.d.ts.map +1 -1
  60. package/dist/commands/workflow/status.js +57 -72
  61. package/dist/commands/workflow/status.js.map +1 -1
  62. package/dist/commands/workflow/templates.d.ts +8 -8
  63. package/dist/commands/workflow/templates.d.ts.map +1 -1
  64. package/dist/commands/workflow/templates.js +32 -46
  65. package/dist/commands/workflow/templates.js.map +1 -1
  66. package/dist/core/archive.d.ts +33 -31
  67. package/dist/core/archive.d.ts.map +1 -1
  68. package/dist/core/archive.js +322 -284
  69. package/dist/core/archive.js.map +1 -1
  70. package/dist/core/artifact-graph/graph.d.ts +25 -42
  71. package/dist/core/artifact-graph/graph.d.ts.map +1 -1
  72. package/dist/core/artifact-graph/graph.js +45 -63
  73. package/dist/core/artifact-graph/graph.js.map +1 -1
  74. package/dist/core/artifact-graph/index.d.ts +1 -1
  75. package/dist/core/artifact-graph/index.d.ts.map +1 -1
  76. package/dist/core/artifact-graph/index.js +1 -1
  77. package/dist/core/artifact-graph/index.js.map +1 -1
  78. package/dist/core/artifact-graph/instruction-loader.d.ts +54 -120
  79. package/dist/core/artifact-graph/instruction-loader.d.ts.map +1 -1
  80. package/dist/core/artifact-graph/instruction-loader.js +129 -111
  81. package/dist/core/artifact-graph/instruction-loader.js.map +1 -1
  82. package/dist/core/artifact-graph/outputs.d.ts +9 -23
  83. package/dist/core/artifact-graph/outputs.d.ts.map +1 -1
  84. package/dist/core/artifact-graph/outputs.js +45 -38
  85. package/dist/core/artifact-graph/outputs.js.map +1 -1
  86. package/dist/core/artifact-graph/resolver.d.ts +36 -81
  87. package/dist/core/artifact-graph/resolver.d.ts.map +1 -1
  88. package/dist/core/artifact-graph/resolver.js +60 -101
  89. package/dist/core/artifact-graph/resolver.js.map +1 -1
  90. package/dist/core/artifact-graph/schema.d.ts +0 -6
  91. package/dist/core/artifact-graph/schema.d.ts.map +1 -1
  92. package/dist/core/artifact-graph/schema.js +7 -32
  93. package/dist/core/artifact-graph/schema.js.map +1 -1
  94. package/dist/core/artifact-graph/state.d.ts +1 -8
  95. package/dist/core/artifact-graph/state.d.ts.map +1 -1
  96. package/dist/core/artifact-graph/state.js +2 -17
  97. package/dist/core/artifact-graph/state.js.map +1 -1
  98. package/dist/core/artifact-graph/stub-detection.d.ts +6 -14
  99. package/dist/core/artifact-graph/stub-detection.d.ts.map +1 -1
  100. package/dist/core/artifact-graph/stub-detection.js +13 -16
  101. package/dist/core/artifact-graph/stub-detection.js.map +1 -1
  102. package/dist/core/artifact-graph/types.d.ts +4 -0
  103. package/dist/core/artifact-graph/types.d.ts.map +1 -1
  104. package/dist/core/artifact-graph/types.js +46 -11
  105. package/dist/core/artifact-graph/types.js.map +1 -1
  106. package/dist/core/available-tools.d.ts +3 -12
  107. package/dist/core/available-tools.d.ts.map +1 -1
  108. package/dist/core/available-tools.js +4 -13
  109. package/dist/core/available-tools.js.map +1 -1
  110. package/dist/core/change-metadata/schema.d.ts +1 -1
  111. package/dist/core/change-metadata/schema.d.ts.map +1 -1
  112. package/dist/core/change-metadata/schema.js +10 -7
  113. package/dist/core/change-metadata/schema.js.map +1 -1
  114. package/dist/core/change-presenter.d.ts +16 -27
  115. package/dist/core/change-presenter.d.ts.map +1 -1
  116. package/dist/core/change-presenter.js +53 -53
  117. package/dist/core/change-presenter.js.map +1 -1
  118. package/dist/core/change-status-policy.d.ts +4 -8
  119. package/dist/core/change-status-policy.d.ts.map +1 -1
  120. package/dist/core/change-status-policy.js +9 -17
  121. package/dist/core/change-status-policy.js.map +1 -1
  122. package/dist/core/codex-metrics.d.ts +25 -45
  123. package/dist/core/codex-metrics.d.ts.map +1 -1
  124. package/dist/core/codex-metrics.js +44 -88
  125. package/dist/core/codex-metrics.js.map +1 -1
  126. package/dist/core/codex-residue.d.ts +14 -15
  127. package/dist/core/codex-residue.d.ts.map +1 -1
  128. package/dist/core/codex-residue.js +18 -22
  129. package/dist/core/codex-residue.js.map +1 -1
  130. package/dist/core/command-generation/adapters/claude.d.ts +2 -9
  131. package/dist/core/command-generation/adapters/claude.d.ts.map +1 -1
  132. package/dist/core/command-generation/adapters/claude.js +2 -12
  133. package/dist/core/command-generation/adapters/claude.js.map +1 -1
  134. package/dist/core/command-generation/adapters/index.d.ts +1 -9
  135. package/dist/core/command-generation/adapters/index.d.ts.map +1 -1
  136. package/dist/core/command-generation/adapters/index.js +1 -9
  137. package/dist/core/command-generation/adapters/index.js.map +1 -1
  138. package/dist/core/command-generation/generator.d.ts +0 -17
  139. package/dist/core/command-generation/generator.d.ts.map +1 -1
  140. package/dist/core/command-generation/generator.js +0 -17
  141. package/dist/core/command-generation/generator.js.map +1 -1
  142. package/dist/core/command-generation/index.d.ts +2 -5
  143. package/dist/core/command-generation/index.d.ts.map +1 -1
  144. package/dist/core/command-generation/index.js +0 -9
  145. package/dist/core/command-generation/index.js.map +1 -1
  146. package/dist/core/command-generation/types.d.ts +10 -36
  147. package/dist/core/command-generation/types.d.ts.map +1 -1
  148. package/dist/core/command-generation/types.js +0 -6
  149. package/dist/core/command-generation/types.js.map +1 -1
  150. package/dist/core/command-generation/yaml.d.ts +3 -18
  151. package/dist/core/command-generation/yaml.d.ts.map +1 -1
  152. package/dist/core/command-generation/yaml.js +5 -23
  153. package/dist/core/command-generation/yaml.js.map +1 -1
  154. package/dist/core/config-prompts.d.ts +2 -4
  155. package/dist/core/config-prompts.d.ts.map +1 -1
  156. package/dist/core/config-prompts.js +2 -7
  157. package/dist/core/config-prompts.js.map +1 -1
  158. package/dist/core/config-schema.d.ts +7 -41
  159. package/dist/core/config-schema.d.ts.map +1 -1
  160. package/dist/core/config-schema.js +35 -74
  161. package/dist/core/config-schema.js.map +1 -1
  162. package/dist/core/config.d.ts +25 -49
  163. package/dist/core/config.d.ts.map +1 -1
  164. package/dist/core/config.js +22 -45
  165. package/dist/core/config.js.map +1 -1
  166. package/dist/core/dashboard-activity.d.ts +7 -9
  167. package/dist/core/dashboard-activity.d.ts.map +1 -1
  168. package/dist/core/dashboard-activity.js +34 -25
  169. package/dist/core/dashboard-activity.js.map +1 -1
  170. package/dist/core/dashboard-data.d.ts +35 -22
  171. package/dist/core/dashboard-data.d.ts.map +1 -1
  172. package/dist/core/dashboard-data.js +57 -72
  173. package/dist/core/dashboard-data.js.map +1 -1
  174. package/dist/core/global-config.d.ts +24 -53
  175. package/dist/core/global-config.d.ts.map +1 -1
  176. package/dist/core/global-config.js +38 -67
  177. package/dist/core/global-config.js.map +1 -1
  178. package/dist/core/init.d.ts +12 -28
  179. package/dist/core/init.d.ts.map +1 -1
  180. package/dist/core/init.js +93 -169
  181. package/dist/core/init.js.map +1 -1
  182. package/dist/core/list.d.ts.map +1 -1
  183. package/dist/core/list.js +95 -41
  184. package/dist/core/list.js.map +1 -1
  185. package/dist/core/local-server.d.ts +41 -83
  186. package/dist/core/local-server.d.ts.map +1 -1
  187. package/dist/core/local-server.js +53 -98
  188. package/dist/core/local-server.js.map +1 -1
  189. package/dist/core/markdown-render.d.ts +15 -23
  190. package/dist/core/markdown-render.d.ts.map +1 -1
  191. package/dist/core/markdown-render.js +25 -34
  192. package/dist/core/markdown-render.js.map +1 -1
  193. package/dist/core/migrate.d.ts +19 -16
  194. package/dist/core/migrate.d.ts.map +1 -1
  195. package/dist/core/migrate.js +162 -136
  196. package/dist/core/migrate.js.map +1 -1
  197. package/dist/core/parsers/change-parser.d.ts +7 -10
  198. package/dist/core/parsers/change-parser.d.ts.map +1 -1
  199. package/dist/core/parsers/change-parser.js +48 -56
  200. package/dist/core/parsers/change-parser.js.map +1 -1
  201. package/dist/core/parsers/markdown-parser.d.ts +8 -9
  202. package/dist/core/parsers/markdown-parser.d.ts.map +1 -1
  203. package/dist/core/parsers/markdown-parser.js +23 -30
  204. package/dist/core/parsers/markdown-parser.js.map +1 -1
  205. package/dist/core/parsers/requirement-blocks.d.ts +43 -15
  206. package/dist/core/parsers/requirement-blocks.d.ts.map +1 -1
  207. package/dist/core/parsers/requirement-blocks.js +142 -70
  208. package/dist/core/parsers/requirement-blocks.js.map +1 -1
  209. package/dist/core/parsers/requirement-text.d.ts +73 -79
  210. package/dist/core/parsers/requirement-text.d.ts.map +1 -1
  211. package/dist/core/parsers/requirement-text.js +137 -79
  212. package/dist/core/parsers/requirement-text.js.map +1 -1
  213. package/dist/core/parsers/spec-structure.d.ts.map +1 -1
  214. package/dist/core/parsers/spec-structure.js +10 -6
  215. package/dist/core/parsers/spec-structure.js.map +1 -1
  216. package/dist/core/profiles.d.ts +3 -10
  217. package/dist/core/profiles.d.ts.map +1 -1
  218. package/dist/core/profiles.js +5 -12
  219. package/dist/core/profiles.js.map +1 -1
  220. package/dist/core/project-config.d.ts +43 -44
  221. package/dist/core/project-config.d.ts.map +1 -1
  222. package/dist/core/project-config.js +107 -82
  223. package/dist/core/project-config.js.map +1 -1
  224. package/dist/core/project-layout.d.ts +9 -17
  225. package/dist/core/project-layout.d.ts.map +1 -1
  226. package/dist/core/project-layout.js +16 -26
  227. package/dist/core/project-layout.js.map +1 -1
  228. package/dist/core/root-selection.d.ts +8 -14
  229. package/dist/core/root-selection.d.ts.map +1 -1
  230. package/dist/core/root-selection.js +3 -6
  231. package/dist/core/root-selection.js.map +1 -1
  232. package/dist/core/rules.d.ts.map +1 -1
  233. package/dist/core/rules.js +2 -3
  234. package/dist/core/rules.js.map +1 -1
  235. package/dist/core/schema-names.d.ts +16 -0
  236. package/dist/core/schema-names.d.ts.map +1 -0
  237. package/dist/core/schema-names.js +16 -0
  238. package/dist/core/schema-names.js.map +1 -0
  239. package/dist/core/schemas/base.schema.d.ts.map +1 -1
  240. package/dist/core/schemas/base.schema.js +6 -12
  241. package/dist/core/schemas/base.schema.js.map +1 -1
  242. package/dist/core/schemas/change.schema.d.ts +8 -0
  243. package/dist/core/schemas/change.schema.d.ts.map +1 -1
  244. package/dist/core/schemas/change.schema.js +41 -10
  245. package/dist/core/schemas/change.schema.js.map +1 -1
  246. package/dist/core/shared/index.d.ts +2 -7
  247. package/dist/core/shared/index.d.ts.map +1 -1
  248. package/dist/core/shared/index.js +2 -7
  249. package/dist/core/shared/index.js.map +1 -1
  250. package/dist/core/shared/rules-generation.d.ts +5 -15
  251. package/dist/core/shared/rules-generation.d.ts.map +1 -1
  252. package/dist/core/shared/rules-generation.js +33 -37
  253. package/dist/core/shared/rules-generation.js.map +1 -1
  254. package/dist/core/shared/skill-generation.d.ts +28 -43
  255. package/dist/core/shared/skill-generation.d.ts.map +1 -1
  256. package/dist/core/shared/skill-generation.js +82 -51
  257. package/dist/core/shared/skill-generation.js.map +1 -1
  258. package/dist/core/shared/tool-detection.d.ts +35 -76
  259. package/dist/core/shared/tool-detection.d.ts.map +1 -1
  260. package/dist/core/shared/tool-detection.js +73 -93
  261. package/dist/core/shared/tool-detection.js.map +1 -1
  262. package/dist/core/skill-metrics.d.ts +36 -63
  263. package/dist/core/skill-metrics.d.ts.map +1 -1
  264. package/dist/core/skill-metrics.js +34 -73
  265. package/dist/core/skill-metrics.js.map +1 -1
  266. package/dist/core/spec-presenter.d.ts.map +1 -1
  267. package/dist/core/spec-presenter.js +5 -10
  268. package/dist/core/spec-presenter.js.map +1 -1
  269. package/dist/core/specs-apply.d.ts +16 -31
  270. package/dist/core/specs-apply.d.ts.map +1 -1
  271. package/dist/core/specs-apply.js +146 -195
  272. package/dist/core/specs-apply.js.map +1 -1
  273. package/dist/core/templates/fragments/interview.d.ts +2 -6
  274. package/dist/core/templates/fragments/interview.d.ts.map +1 -1
  275. package/dist/core/templates/fragments/interview.js +2 -6
  276. package/dist/core/templates/fragments/interview.js.map +1 -1
  277. package/dist/core/templates/fragments/next-step.d.ts +4 -8
  278. package/dist/core/templates/fragments/next-step.d.ts.map +1 -1
  279. package/dist/core/templates/fragments/next-step.js +4 -8
  280. package/dist/core/templates/fragments/next-step.js.map +1 -1
  281. package/dist/core/templates/fragments/validate.d.ts +13 -0
  282. package/dist/core/templates/fragments/validate.d.ts.map +1 -0
  283. package/dist/core/templates/fragments/validate.js +13 -0
  284. package/dist/core/templates/fragments/validate.js.map +1 -0
  285. package/dist/core/templates/fragments/verify.d.ts +9 -12
  286. package/dist/core/templates/fragments/verify.d.ts.map +1 -1
  287. package/dist/core/templates/fragments/verify.js +9 -12
  288. package/dist/core/templates/fragments/verify.js.map +1 -1
  289. package/dist/core/templates/index.d.ts +0 -6
  290. package/dist/core/templates/index.d.ts.map +1 -1
  291. package/dist/core/templates/index.js +0 -7
  292. package/dist/core/templates/index.js.map +1 -1
  293. package/dist/core/templates/skill-templates.d.ts +1 -5
  294. package/dist/core/templates/skill-templates.d.ts.map +1 -1
  295. package/dist/core/templates/skill-templates.js +0 -5
  296. package/dist/core/templates/skill-templates.js.map +1 -1
  297. package/dist/core/templates/types.d.ts +3 -7
  298. package/dist/core/templates/types.d.ts.map +1 -1
  299. package/dist/core/templates/types.js +0 -3
  300. package/dist/core/templates/types.js.map +1 -1
  301. package/dist/core/templates/workflows/apply.d.ts +3 -9
  302. package/dist/core/templates/workflows/apply.d.ts.map +1 -1
  303. package/dist/core/templates/workflows/apply.js +11 -13
  304. package/dist/core/templates/workflows/apply.js.map +1 -1
  305. package/dist/core/templates/workflows/archive.d.ts +0 -6
  306. package/dist/core/templates/workflows/archive.d.ts.map +1 -1
  307. package/dist/core/templates/workflows/archive.js +16 -5
  308. package/dist/core/templates/workflows/archive.js.map +1 -1
  309. package/dist/core/templates/workflows/decision.js +3 -3
  310. package/dist/core/templates/workflows/decision.js.map +1 -1
  311. package/dist/core/templates/workflows/explore.js +1 -1
  312. package/dist/core/templates/workflows/grill.d.ts.map +1 -1
  313. package/dist/core/templates/workflows/grill.js +0 -2
  314. package/dist/core/templates/workflows/grill.js.map +1 -1
  315. package/dist/core/templates/workflows/issue.d.ts +0 -6
  316. package/dist/core/templates/workflows/issue.d.ts.map +1 -1
  317. package/dist/core/templates/workflows/issue.js +3 -2
  318. package/dist/core/templates/workflows/issue.js.map +1 -1
  319. package/dist/core/templates/workflows/propose.d.ts +0 -6
  320. package/dist/core/templates/workflows/propose.d.ts.map +1 -1
  321. package/dist/core/templates/workflows/propose.js +2 -2
  322. package/dist/core/templates/workflows/propose.js.map +1 -1
  323. package/dist/core/templates/workflows/sync.d.ts +2 -8
  324. package/dist/core/templates/workflows/sync.d.ts.map +1 -1
  325. package/dist/core/templates/workflows/sync.js +4 -3
  326. package/dist/core/templates/workflows/sync.js.map +1 -1
  327. package/dist/core/templates/workflows/update.d.ts +0 -6
  328. package/dist/core/templates/workflows/update.d.ts.map +1 -1
  329. package/dist/core/templates/workflows/update.js +9 -2
  330. package/dist/core/templates/workflows/update.js.map +1 -1
  331. package/dist/core/update.d.ts +10 -36
  332. package/dist/core/update.d.ts.map +1 -1
  333. package/dist/core/update.js +59 -126
  334. package/dist/core/update.js.map +1 -1
  335. package/dist/core/user-state-migration.d.ts +13 -15
  336. package/dist/core/user-state-migration.d.ts.map +1 -1
  337. package/dist/core/user-state-migration.js +30 -23
  338. package/dist/core/user-state-migration.js.map +1 -1
  339. package/dist/core/validation/constants.d.ts +4 -10
  340. package/dist/core/validation/constants.d.ts.map +1 -1
  341. package/dist/core/validation/constants.js +25 -25
  342. package/dist/core/validation/constants.js.map +1 -1
  343. package/dist/core/validation/prose-length.d.ts +15 -0
  344. package/dist/core/validation/prose-length.d.ts.map +1 -0
  345. package/dist/core/validation/prose-length.js +29 -0
  346. package/dist/core/validation/prose-length.js.map +1 -0
  347. package/dist/core/validation/purpose-placeholder.d.ts +9 -16
  348. package/dist/core/validation/purpose-placeholder.d.ts.map +1 -1
  349. package/dist/core/validation/purpose-placeholder.js +30 -44
  350. package/dist/core/validation/purpose-placeholder.js.map +1 -1
  351. package/dist/core/validation/section-validator.d.ts +4 -4
  352. package/dist/core/validation/section-validator.d.ts.map +1 -1
  353. package/dist/core/validation/section-validator.js +30 -12
  354. package/dist/core/validation/section-validator.js.map +1 -1
  355. package/dist/core/validation/task-numbering.d.ts +6 -3
  356. package/dist/core/validation/task-numbering.d.ts.map +1 -1
  357. package/dist/core/validation/task-numbering.js +23 -11
  358. package/dist/core/validation/task-numbering.js.map +1 -1
  359. package/dist/core/validation/types.d.ts +18 -0
  360. package/dist/core/validation/types.d.ts.map +1 -1
  361. package/dist/core/validation/types.js +12 -1
  362. package/dist/core/validation/types.js.map +1 -1
  363. package/dist/core/validation/validator.d.ts +42 -68
  364. package/dist/core/validation/validator.d.ts.map +1 -1
  365. package/dist/core/validation/validator.js +474 -285
  366. package/dist/core/validation/validator.js.map +1 -1
  367. package/dist/prompts/searchable-multi-select.d.ts +3 -8
  368. package/dist/prompts/searchable-multi-select.d.ts.map +1 -1
  369. package/dist/prompts/searchable-multi-select.js +16 -39
  370. package/dist/prompts/searchable-multi-select.js.map +1 -1
  371. package/dist/utils/change-metadata.d.ts +11 -50
  372. package/dist/utils/change-metadata.d.ts.map +1 -1
  373. package/dist/utils/change-metadata.js +48 -67
  374. package/dist/utils/change-metadata.js.map +1 -1
  375. package/dist/utils/change-utils.d.ts +24 -76
  376. package/dist/utils/change-utils.d.ts.map +1 -1
  377. package/dist/utils/change-utils.js +95 -142
  378. package/dist/utils/change-utils.js.map +1 -1
  379. package/dist/utils/file-lock.d.ts +39 -0
  380. package/dist/utils/file-lock.d.ts.map +1 -0
  381. package/dist/utils/file-lock.js +149 -0
  382. package/dist/utils/file-lock.js.map +1 -0
  383. package/dist/utils/file-system.d.ts +12 -32
  384. package/dist/utils/file-system.d.ts.map +1 -1
  385. package/dist/utils/file-system.js +41 -45
  386. package/dist/utils/file-system.js.map +1 -1
  387. package/dist/utils/frontmatter.d.ts +7 -11
  388. package/dist/utils/frontmatter.d.ts.map +1 -1
  389. package/dist/utils/frontmatter.js +11 -11
  390. package/dist/utils/frontmatter.js.map +1 -1
  391. package/dist/utils/interactive.d.ts +4 -9
  392. package/dist/utils/interactive.d.ts.map +1 -1
  393. package/dist/utils/interactive.js +2 -4
  394. package/dist/utils/interactive.js.map +1 -1
  395. package/dist/utils/item-discovery.d.ts +10 -20
  396. package/dist/utils/item-discovery.d.ts.map +1 -1
  397. package/dist/utils/item-discovery.js +31 -55
  398. package/dist/utils/item-discovery.js.map +1 -1
  399. package/dist/utils/link.d.ts +9 -18
  400. package/dist/utils/link.d.ts.map +1 -1
  401. package/dist/utils/link.js +9 -18
  402. package/dist/utils/link.js.map +1 -1
  403. package/dist/utils/requirement-diff.d.ts +13 -23
  404. package/dist/utils/requirement-diff.d.ts.map +1 -1
  405. package/dist/utils/requirement-diff.js +13 -23
  406. package/dist/utils/requirement-diff.js.map +1 -1
  407. package/dist/utils/spec-files.d.ts +7 -20
  408. package/dist/utils/spec-files.d.ts.map +1 -1
  409. package/dist/utils/spec-files.js +26 -51
  410. package/dist/utils/spec-files.js.map +1 -1
  411. package/dist/utils/task-progress.d.ts +11 -9
  412. package/dist/utils/task-progress.d.ts.map +1 -1
  413. package/dist/utils/task-progress.js +53 -32
  414. package/dist/utils/task-progress.js.map +1 -1
  415. package/dist/utils/timestamp.d.ts +5 -8
  416. package/dist/utils/timestamp.d.ts.map +1 -1
  417. package/dist/utils/timestamp.js +5 -8
  418. package/dist/utils/timestamp.js.map +1 -1
  419. package/package.json +2 -3
  420. package/schemas/issue/schema.yaml +8 -1
  421. package/schemas/issue/templates/spec.md +20 -3
  422. package/schemas/sdd/schema.yaml +20 -1
  423. package/schemas/sdd/templates/spec.md +20 -3
@@ -1,22 +1,11 @@
1
- /**
2
- * Spec Application Logic
3
- *
4
- * Extracted from ArchiveCommand to enable standalone spec application.
5
- * Applies delta specs from a change to main specs without archiving.
6
- */
1
+ /** Applies a change's delta specs to the main specs, independently of archiving. */
7
2
  import { promises as fs } from 'fs';
8
3
  import path from 'path';
9
- import { extractRequirementsSection, parseDeltaSpec, normalizeRequirementName, foldRequirementName, } from './parsers/requirement-blocks.js';
4
+ import { extractRequirementsSection, parseDeltaSpec, normalizeRequirementName, foldRequirementName, REQUIREMENT_HEADER_REGEX, } from './parsers/requirement-blocks.js';
10
5
  import { findMainSpecStructureIssues } from './parsers/spec-structure.js';
11
6
  import { isAbsentDirectoryError, isMissingPathError } from '../utils/file-system.js';
12
- import { buildCodeFenceMask, findMissingScenarios } from './parsers/requirement-text.js';
7
+ import { buildCodeFenceMask, findMissingScenarios, normalizeDocument } from './parsers/requirement-text.js';
13
8
  import { PURPOSE_PLACEHOLDER_PREFIX, PURPOSE_PLACEHOLDER_SUFFIX, } from './validation/constants.js';
14
- // -----------------------------------------------------------------------------
15
- // Public API
16
- // -----------------------------------------------------------------------------
17
- /**
18
- * Find all delta spec files that need to be applied from a change.
19
- */
20
9
  export async function findSpecUpdates(changeDir, mainSpecsDir) {
21
10
  const updates = [];
22
11
  const changeSpecsDir = path.join(changeDir, 'specs');
@@ -25,9 +14,8 @@ export async function findSpecUpdates(changeDir, mainSpecsDir) {
25
14
  entries = await fs.readdir(changeSpecsDir, { withFileTypes: true });
26
15
  }
27
16
  catch (error) {
28
- // An empty result reads as "this change has no deltas", which callers act
29
- // on: archive merges nothing and reports success. Only a directory that is
30
- // genuinely not there may say that.
17
+ // An empty result means "no deltas", and archive merges nothing and reports
18
+ // success on it. Only a genuinely absent directory may say that.
31
19
  if (isAbsentDirectoryError(error))
32
20
  return updates;
33
21
  throw error;
@@ -41,16 +29,14 @@ export async function findSpecUpdates(changeDir, mainSpecsDir) {
41
29
  await fs.access(specFile);
42
30
  }
43
31
  catch (error) {
44
- // A capability folder with no spec.md is not a delta. A spec.md that
45
- // exists and cannot be reached is one, and skipping it drops that
46
- // capability's requirements without a word.
32
+ // A capability folder with no spec.md is not a delta; an unreachable
33
+ // spec.md is one, and skipping it drops its requirements silently.
47
34
  if (isMissingPathError(error))
48
35
  continue;
49
36
  throw error;
50
37
  }
51
- // Whether the capability is new. Same rule again: an unreachable target is
52
- // not an absent one, and calling it absent sends the merge down the
53
- // new-capability path — which rebuilds the spec from a skeleton.
38
+ // Same rule: calling an unreachable target absent sends the merge down the
39
+ // new-capability path, which rebuilds the spec from a skeleton.
54
40
  let exists = true;
55
41
  try {
56
42
  await fs.access(targetFile);
@@ -69,9 +55,21 @@ export async function findSpecUpdates(changeDir, mainSpecsDir) {
69
55
  return updates;
70
56
  }
71
57
  /**
72
- * Build an updated spec by applying delta operations.
73
- * Returns the rebuilt content, counts of operations, and any non-fatal notices.
58
+ * The capability directory's real name on disk, or `fallback` when absent.
59
+ * Differs only on a case-insensitive filesystem, where `specs/RegexCap` and
60
+ * `specs/regexcap` are one directory — this normalises a message, it does not
61
+ * merge anything.
74
62
  */
63
+ async function resolveOnDiskSpecName(targetPath, fallback) {
64
+ const parent = path.dirname(path.dirname(targetPath));
65
+ try {
66
+ const entries = await fs.readdir(parent);
67
+ return entries.find((entry) => entry.toLowerCase() === fallback.toLowerCase()) ?? fallback;
68
+ }
69
+ catch {
70
+ return fallback;
71
+ }
72
+ }
75
73
  export async function buildUpdatedSpec(update, changeName, options = {}) {
76
74
  const notices = [];
77
75
  const notice = (code, message) => {
@@ -79,36 +77,31 @@ export async function buildUpdatedSpec(update, changeName, options = {}) {
79
77
  if (!options.silent)
80
78
  console.log(`Warning: ${message}`);
81
79
  };
82
- // Read change spec content (delta-format expected)
83
80
  const changeContent = await fs.readFile(update.source, 'utf-8');
84
- // Parse deltas from the change spec file
85
81
  const plan = parseDeltaSpec(changeContent);
82
+ // The capability as the delta spelled it — messages about the delta's own
83
+ // content echo the author's spelling so they can find it in their file.
86
84
  const specName = path.basename(path.dirname(update.target));
87
- // Pre-validate duplicates within sections
88
- const addedNames = new Set();
89
- for (const add of plan.added) {
90
- const name = normalizeRequirementName(add.name);
91
- if (addedNames.has(name)) {
92
- throw new Error(`${specName} validation failed - duplicate requirement in ADDED for header "### Requirement: ${add.name}"`);
93
- }
94
- addedNames.add(name);
95
- }
96
- const modifiedNames = new Set();
97
- for (const mod of plan.modified) {
98
- const name = normalizeRequirementName(mod.name);
99
- if (modifiedNames.has(name)) {
100
- throw new Error(`${specName} validation failed - duplicate requirement in MODIFIED for header "### Requirement: ${mod.name}"`);
101
- }
102
- modifiedNames.add(name);
103
- }
104
- const removedNamesSet = new Set();
105
- for (const rem of plan.removed) {
106
- const name = normalizeRequirementName(rem);
107
- if (removedNamesSet.has(name)) {
108
- throw new Error(`${specName} validation failed - duplicate requirement in REMOVED for header "### Requirement: ${rem}"`);
85
+ // The capability as it exists on disk, which differs on a case-insensitive
86
+ // filesystem: a delta under `specs/RegexCap/` resolves to the existing
87
+ // `tospec/specs/regexcap/`.
88
+ const onDiskSpecName = await resolveOnDiskSpecName(update.target, specName);
89
+ const collectUnique = (names, section) => {
90
+ const seen = new Set();
91
+ for (const original of names) {
92
+ const name = normalizeRequirementName(original);
93
+ if (seen.has(name)) {
94
+ throw new Error(`${specName} validation failed - duplicate requirement in ${section} for header "### Requirement: ${original}"`);
95
+ }
96
+ seen.add(name);
109
97
  }
110
- removedNamesSet.add(name);
111
- }
98
+ return seen;
99
+ };
100
+ const addedNames = collectUnique(plan.added.map((add) => add.name), 'ADDED');
101
+ const modifiedNames = collectUnique(plan.modified.map((mod) => mod.name), 'MODIFIED');
102
+ const removedNamesSet = collectUnique(plan.removed, 'REMOVED');
103
+ // Checked per entry rather than via collectUnique: a pair that duplicates
104
+ // both sides must report the FROM/TO the original interleaving would.
112
105
  const renamedFromSet = new Set();
113
106
  const renamedToSet = new Set();
114
107
  for (const { from, to } of plan.renamed) {
@@ -123,7 +116,6 @@ export async function buildUpdatedSpec(update, changeName, options = {}) {
123
116
  renamedFromSet.add(fromNorm);
124
117
  renamedToSet.add(toNorm);
125
118
  }
126
- // Pre-validate cross-section conflicts
127
119
  const conflicts = [];
128
120
  for (const n of modifiedNames) {
129
121
  if (removedNamesSet.has(n))
@@ -135,15 +127,12 @@ export async function buildUpdatedSpec(update, changeName, options = {}) {
135
127
  if (removedNamesSet.has(n))
136
128
  conflicts.push({ name: n, a: 'ADDED', b: 'REMOVED' });
137
129
  }
138
- // Renamed interplay: MODIFIED must reference the NEW header, not FROM
139
130
  for (const { from, to } of plan.renamed) {
140
131
  const fromNorm = normalizeRequirementName(from);
141
132
  const toNorm = normalizeRequirementName(to);
142
- // A REMOVED naming the FROM side contradicts the rename. Once a missing
143
- // REMOVED target is treated as a no-op (early-sync), this conflict must be
144
- // rejected explicitly instead of failing incidentally at apply time.
145
- // Compared folded, so a case/whitespace variant cannot slip past into a
146
- // warned no-op.
133
+ // A REMOVED naming the FROM side contradicts the rename. Since a missing
134
+ // REMOVED target is a no-op (early-sync), this must be rejected explicitly.
135
+ // Folded, so a case or whitespace variant cannot slip past as a no-op.
147
136
  const removedFoldMatch = [...removedNamesSet].find((r) => foldRequirementName(r) === foldRequirementName(fromNorm));
148
137
  if (removedFoldMatch !== undefined) {
149
138
  throw new Error(`${specName} validation failed - requirement present in multiple sections (RENAMED and REMOVED) for header "### Requirement: ${from}"` +
@@ -152,7 +141,6 @@ export async function buildUpdatedSpec(update, changeName, options = {}) {
152
141
  if (modifiedNames.has(fromNorm)) {
153
142
  throw new Error(`${specName} validation failed - when a rename exists, MODIFIED must reference the NEW header "### Requirement: ${to}"`);
154
143
  }
155
- // Detect ADDED colliding with a RENAMED TO
156
144
  if (addedNames.has(toNorm)) {
157
145
  throw new Error(`${specName} validation failed - RENAMED TO header collides with ADDED for "### Requirement: ${to}"`);
158
146
  }
@@ -166,33 +154,25 @@ export async function buildUpdatedSpec(update, changeName, options = {}) {
166
154
  throw new Error(`Delta parsing found no operations for ${path.basename(path.dirname(update.source))}. ` +
167
155
  `Provide ADDED/MODIFIED/REMOVED/RENAMED sections in change spec.`);
168
156
  }
169
- // Load or create base target content
170
157
  let targetContent;
171
158
  let isNewSpec = false;
172
159
  try {
173
160
  targetContent = await fs.readFile(update.target, 'utf-8');
174
161
  }
175
162
  catch (error) {
176
- // Only "the file is not there" means a new capability. Every other failure —
177
- // a permission denial, a device error, a directory where the file belongs —
178
- // says the spec may well exist with content this path is about to discard.
179
- // For an addition-only delta that is silent data loss: the new-capability
180
- // branch below accepts it and rebuilds the spec from a skeleton.
163
+ // Only "not there" means a new capability. Any other failure says the spec
164
+ // may exist with content the skeleton rebuild below would discard.
181
165
  if (!isMissingPathError(error))
182
166
  throw error;
183
- // Target spec does not exist; MODIFIED and RENAMED are not allowed for new specs
184
- // REMOVED will be ignored with a warning since there's nothing to remove
185
167
  if (plan.modified.length > 0 || plan.renamed.length > 0) {
186
168
  throw new Error(`${specName}: target spec does not exist; only ADDED requirements are allowed for new specs. MODIFIED and RENAMED operations require an existing spec.`);
187
169
  }
188
- // Warn about REMOVED requirements being ignored for new specs
189
170
  if (plan.removed.length > 0) {
190
171
  notice('archive_removed_ignored_new_spec', `${specName} - ${plan.removed.length} REMOVED requirement(s) ignored for new spec (nothing to remove).`);
191
172
  }
192
173
  isNewSpec = true;
193
- // Carry the delta's authored Purpose into the new main spec instead of
194
- // overwriting it with the placeholder. Only plain prose is carried; a
195
- // Purpose that could restructure the spec falls back to the placeholder.
174
+ // Only plain prose is carried; a Purpose that could restructure the spec
175
+ // falls back to the placeholder.
196
176
  const deltaPurpose = extractDeltaPurpose(changeContent);
197
177
  if (deltaPurpose && isPurposeCarrySafe(deltaPurpose)) {
198
178
  targetContent = buildSpecSkeleton(specName, changeName, deltaPurpose);
@@ -202,35 +182,27 @@ export async function buildUpdatedSpec(update, changeName, options = {}) {
202
182
  notice('archive_delta_purpose_ignored', `${specName} - delta Purpose ignored (it would leave the new spec unreadable); wrote the placeholder Purpose instead.`);
203
183
  }
204
184
  else {
205
- // The schema calls `## Purpose` mandatory for a new capability, but
206
- // nothing enforces it: validation does not check, and this placeholder
207
- // let the omission reach the merged spec as the literal word "TBD" with
208
- // nobody told. Saying so here is the only moment the change that owns
185
+ // The schema calls `## Purpose` mandatory for a new capability but
186
+ // validation does not check it, so the placeholder reaches the merged
187
+ // spec as a literal "TBD". This is the last moment the change that owns
209
188
  // the answer is still in hand.
210
189
  notice('archive_purpose_placeholder', `${specName} - delta has no "## Purpose"; wrote a TBD placeholder into the new main spec. Edit tospec/specs/${specName}/spec.md to describe why this capability exists.`);
211
190
  }
212
191
  targetContent = buildSpecSkeleton(specName, changeName);
213
192
  }
214
193
  }
215
- // An existing capability's main spec owns its Purpose; a delta's is read only
216
- // on the new-spec branch above, so on this path it was dropped without a word.
217
- // The schema instruction tells authors not to write one here — which is
218
- // exactly why someone who did needs to hear that it went nowhere, rather than
219
- // assume the main spec now says what they wrote.
194
+ // An existing capability's main spec owns its Purpose, so a delta's is
195
+ // dropped on this path — an author who wrote one must hear it went nowhere.
220
196
  if (!isNewSpec) {
221
197
  const strayPurpose = extractDeltaPurpose(changeContent);
222
198
  if (strayPurpose) {
223
- notice('archive_delta_purpose_ignored', `${specName} - delta "## Purpose" ignored; ${specName} already has a main spec and owns its Purpose. Edit tospec/specs/${specName}/spec.md directly if it needs changing.`);
199
+ notice('archive_delta_purpose_ignored', `${specName} - delta "## Purpose" ignored; ${onDiskSpecName} already has a main spec and owns its Purpose. Edit tospec/specs/${onDiskSpecName}/spec.md directly if it needs changing.`);
224
200
  }
225
201
  }
226
- // Record the target's ending convention here, on the raw bytes, because
227
- // everything downstream destroys it: extractRequirementsSection normalizes the
228
- // document to LF before parsing, normalizeBlockRaw does it again per block,
229
- // and the recompose joins with '\n'. Normalizing before parsing is correct —
230
- // every structural regex below assumes LF — so the fix is to carry the
231
- // convention forward, not to stop normalizing. A whole-file flag rather than
232
- // per-line endings: a merged spec contains lines that had no original, so
233
- // per-line fidelity is undefined for them.
202
+ // Recorded on the raw bytes because everything downstream normalizes to LF
203
+ // (every structural regex below assumes it), so the convention has to be
204
+ // carried forward. A whole-file flag, not per-line: a merged spec contains
205
+ // lines that had no original.
234
206
  const crlf = targetContent.includes('\r\n');
235
207
  const structureIssues = findMainSpecStructureIssues(targetContent);
236
208
  if (structureIssues.length > 0) {
@@ -239,27 +211,22 @@ export async function buildUpdatedSpec(update, changeName, options = {}) {
239
211
  .join('\n');
240
212
  throw new Error(`${specName}: target spec is structurally invalid and cannot be updated until fixed:\n${details}`);
241
213
  }
242
- // Extract requirements section and build name->block map
243
214
  const parts = extractRequirementsSection(targetContent);
244
215
  const nameToBlock = new Map();
245
216
  for (const block of parts.bodyBlocks) {
246
217
  nameToBlock.set(normalizeRequirementName(block.name), block);
247
218
  }
248
- // The lookup key for each original block, by position. RENAMED changes a
249
- // block's key, so `bodyBlocks` alone can no longer find it at recompose time;
250
- // this list carries the current key in the original slot. `bodyBlocks` stays
251
- // untouched because the salvage pass below reads the ORIGINAL raw content.
219
+ // The current key for each original block, by position: RENAMED changes a
220
+ // block's key, so `bodyBlocks` alone cannot find it at recompose time.
221
+ // `bodyBlocks` stays untouched because the salvage pass reads the original raw.
252
222
  const orderedKeys = parts.bodyBlocks.map((block) => normalizeRequirementName(block.name));
253
- // Apply operations in order: RENAMED → REMOVED → MODIFIED → ADDED
254
- // RENAMED
223
+ // Order matters: RENAMED → REMOVED → MODIFIED → ADDED.
255
224
  let renamedApplied = 0;
256
225
  for (const r of plan.renamed) {
257
226
  const from = normalizeRequirementName(r.from);
258
227
  const to = normalizeRequirementName(r.to);
259
228
  if (!nameToBlock.has(from)) {
260
- // Source gone but target present means the rename was already synced to
261
- // the baseline (early-sync pattern) - re-applying it is a no-op, not a
262
- // failure. Only a missing source AND target is a genuine error.
229
+ // Source gone but target present: the rename was already synced.
263
230
  if (nameToBlock.has(to)) {
264
231
  continue;
265
232
  }
@@ -280,72 +247,88 @@ export async function buildUpdatedSpec(update, changeName, options = {}) {
280
247
  };
281
248
  nameToBlock.delete(from);
282
249
  nameToBlock.set(to, renamedBlock);
283
- // delete+set moves the entry to the Map's insertion-order tail, so a rename
284
- // would otherwise push the requirement to the end of the rebuilt spec.
285
- // Re-key it in place instead; a chain A->B->C updates the same slot twice.
250
+ // delete+set moves the entry to the Map's insertion-order tail, pushing the
251
+ // requirement to the end of the rebuilt spec. Re-key in place.
286
252
  const orderIndex = orderedKeys.indexOf(from);
287
253
  if (orderIndex >= 0) {
288
254
  orderedKeys[orderIndex] = to;
289
255
  }
290
256
  renamedApplied++;
291
257
  }
292
- // REMOVED
293
258
  let removedApplied = 0;
294
259
  // Keys this run actually removed. A renamed requirement also disappears from
295
- // its old key, so "absent from nameToBlock" alone cannot tell the two apart —
296
- // and salvaging a renamed block's tail would duplicate it.
260
+ // its old key, so "absent from nameToBlock" cannot tell the two apart, and
261
+ // salvaging a renamed block's tail would duplicate it.
297
262
  const removedKeys = new Set();
298
263
  for (const name of plan.removed) {
299
264
  const key = normalizeRequirementName(name);
300
265
  if (!nameToBlock.has(key)) {
301
- // Requirement gone from the baseline means the removal was already synced
302
- // (early-sync pattern) - re-applying it is a no-op, not a failure. One
303
- // signal separates that from a mistyped header: a requirement that differs
304
- // only in case or interior whitespace still being present. That is a typo,
305
- // and stays a hard abort. For new specs the skip was already warned above.
266
+ // Already gone means the removal was already synced. A near miss — one
267
+ // still present differing only in case or interior whitespace — is a typo
268
+ // instead, and stays a hard abort.
306
269
  if (!isNewSpec) {
307
270
  const nearMiss = [...nameToBlock.keys()].find((k) => foldRequirementName(k) === foldRequirementName(key));
308
271
  if (nearMiss !== undefined) {
309
272
  throw new Error(`${specName} REMOVED failed for header "### Requirement: ${name}" - not found, but "### Requirement: ${nameToBlock.get(nearMiss).name}" exists; fix the header to match it exactly`);
310
273
  }
311
- notice('archive_removed_requirement_absent', `${specName} - REMOVED requirement "${name}" is not in the current spec; treating it as already removed. Nothing was deleted — if you meant to remove a requirement, check the header spelling against tospec/specs/${specName}/spec.md.`);
274
+ notice('archive_removed_requirement_absent', `${specName} - REMOVED requirement "${name}" is not in the current spec; treating it as already removed. Nothing was deleted — if you meant to remove a requirement, check the header spelling against tospec/specs/${onDiskSpecName}/spec.md.`);
312
275
  }
313
- // Skip removal (nothing to remove)
314
276
  continue;
315
277
  }
316
278
  nameToBlock.delete(key);
317
279
  removedKeys.add(key);
318
280
  removedApplied++;
319
281
  }
320
- // MODIFIED
282
+ // Declarations are consumed as a MODIFIED omission matches them. A declaration
283
+ // still here afterwards removed nothing: the section grants no removal of its
284
+ // own, so without a matching omission the scenario stays in the spec while
285
+ // the change says it went. The validator reports the same leftover as an
286
+ // ERROR; re-checking here keeps `--no-validate` from archiving what validate
287
+ // refuses.
288
+ const unmatchedDeclarations = new Set(plan.removedScenarios);
321
289
  for (const mod of plan.modified) {
322
290
  const key = normalizeRequirementName(mod.name);
323
291
  const currentBlock = nameToBlock.get(key);
324
292
  if (!currentBlock) {
325
293
  throw new Error(`${specName} MODIFIED failed for header "### Requirement: ${mod.name}" - not found`);
326
294
  }
327
- // Replace block with provided raw (ensure header line matches key)
328
- const modHeaderMatch = mod.raw.split('\n')[0].match(/^###\s*Requirement:\s*(.+)\s*$/i);
295
+ const modHeaderMatch = mod.raw.split('\n')[0].match(REQUIREMENT_HEADER_REGEX);
329
296
  if (!modHeaderMatch || normalizeRequirementName(modHeaderMatch[1]) !== key) {
330
297
  throw new Error(`${specName} MODIFIED failed for header "### Requirement: ${mod.name}" - header mismatch in content`);
331
298
  }
332
- // Guard against a stale MODIFIED block silently dropping scenarios that a
333
- // previous archive already added to the current spec.
334
- const missingScenarios = findMissingScenarios(currentBlock.raw, mod.raw);
299
+ // Guards against a stale MODIFIED block dropping scenarios a previous
300
+ // archive added. `## REMOVED Scenarios` exempts them, read from the same
301
+ // declarations the validator uses so the two cannot disagree.
302
+ const declaredRemovals = new Set(plan.removedScenarios.filter((entry) => entry.requirement === key).map((entry) => entry.scenario));
303
+ const missingScenarios = findMissingScenarios(currentBlock.raw, mod.raw).filter((name) => {
304
+ if (!declaredRemovals.has(name))
305
+ return true;
306
+ for (const entry of unmatchedDeclarations) {
307
+ if (entry.requirement === key && entry.scenario === name)
308
+ unmatchedDeclarations.delete(entry);
309
+ }
310
+ return false;
311
+ });
335
312
  if (missingScenarios.length > 0) {
336
313
  throw new Error(`${specName} MODIFIED failed for header "### Requirement: ${mod.name}" - current spec contains scenario(s) not present in the modified block: ${missingScenarios.map(name => `"${name}"`).join(', ')}. Refresh the change spec before archiving to avoid dropping scenarios.`);
337
314
  }
338
- nameToBlock.set(key, mod);
315
+ // A plain `### Notes` after the requirement is absorbed into its block (see
316
+ // the recompose pass below), and replacing the block wholesale deleted it
317
+ // with the old body. Same salvage as REMOVED, appended after the new body
318
+ // so the section keeps its place.
319
+ const absorbed = salvageAbsorbedContent(currentBlock.raw);
320
+ nameToBlock.set(key, absorbed ? { ...mod, raw: `${mod.raw.trimEnd()}\n\n${absorbed}` } : mod);
321
+ }
322
+ for (const entry of unmatchedDeclarations) {
323
+ throw new Error(`${specName} REMOVED Scenarios declares "${entry.scenario}" under requirement "${entry.requirement}", but no MODIFIED block for that requirement drops that scenario - the declaration removes nothing on its own. Restate the requirement under "## MODIFIED Requirements" without that scenario, or check both names against the current spec.`);
339
324
  }
340
- // ADDED
341
325
  let addedApplied = 0;
342
326
  for (const add of plan.added) {
343
327
  const key = normalizeRequirementName(add.name);
344
328
  const existing = nameToBlock.get(key);
345
329
  if (existing) {
346
- // Identical content means the requirement was already synced to the
347
- // baseline (early-sync pattern) - re-applying it is a no-op, not a
348
- // conflict. Only differing content is a genuine collision.
330
+ // Identical content means it was already synced; only differing content
331
+ // is a genuine collision.
349
332
  if (normalizeBlockRaw(existing.raw) === normalizeBlockRaw(add.raw)) {
350
333
  continue;
351
334
  }
@@ -354,13 +337,10 @@ export async function buildUpdatedSpec(update, changeName, options = {}) {
354
337
  nameToBlock.set(key, add);
355
338
  addedApplied++;
356
339
  }
357
- // Duplicates within resulting map are implicitly prevented by key uniqueness.
358
- // Recompose requirements section preserving original ordering where possible.
359
- // A block's `raw` runs to the next header the parser RECOGNISES, so a heading
360
- // it does not — a plain `### Notes` — is absorbed into the requirement above
361
- // it. Removing that requirement used to delete the absorbed content with it,
362
- // silently: nothing counted it, so nothing warned, and the spec left behind
363
- // still validated. Salvage it back into place instead.
340
+ // Recompose, preserving original ordering. A block's `raw` runs to the next
341
+ // header the parser recognises, so one it does not — a plain `### Notes` — is
342
+ // absorbed into the requirement above, and removing that requirement would
343
+ // delete the absorbed content silently. Salvage it back into place instead.
364
344
  const pieces = [];
365
345
  const seen = new Set();
366
346
  let salvagedCount = 0;
@@ -384,7 +364,7 @@ export async function buildUpdatedSpec(update, changeName, options = {}) {
384
364
  }
385
365
  }
386
366
  }
387
- // Append any newly added that were not in original order
367
+ // Whatever was added rather than replacing an original slot.
388
368
  for (const [key, block] of nameToBlock.entries()) {
389
369
  if (!seen.has(key)) {
390
370
  pieces.push(block.raw);
@@ -392,8 +372,7 @@ export async function buildUpdatedSpec(update, changeName, options = {}) {
392
372
  }
393
373
  }
394
374
  // Every requirement is gone but absorbed content survived. Retiring would
395
- // delete that content along with the file, which is the silent loss this
396
- // salvage exists to prevent; writing it would produce a spec with no
375
+ // delete that content with the file; writing it would produce a spec with no
397
376
  // requirements, which cannot validate. Neither is safe to choose silently.
398
377
  if (keptRequirements === 0 && removedApplied > 0 && salvagedCount > 0) {
399
378
  throw new Error(`${specName}: removing every requirement would leave content that is not part of any requirement (e.g. a "### Notes" section). Move that content out of ${specName}/spec.md first, then archive again.`);
@@ -403,26 +382,19 @@ export async function buildUpdatedSpec(update, changeName, options = {}) {
403
382
  .concat(pieces)
404
383
  .join('\n\n')
405
384
  .trimEnd();
406
- // The blank lines around `## Requirements` belong to no slice: `before` and
407
- // `after` carry at most one boundary newline by construction, `reqBody` and
408
- // every block `raw` are trimEnd()ed. Joining with a bare '\n' therefore glued
409
- // the heading to the Purpose paragraph above it and the first requirement
410
- // below, so every archive rewrote a well-formatted spec into that shape.
411
- // Separate non-empty slices with one blank line, and end the file with
412
- // exactly one newline instead of inheriting whatever `after` happened to hold.
385
+ // Every slice is trimmed, so the blank lines around `## Requirements` belong
386
+ // to no slice: joining with a bare '\n' glues the heading to the Purpose above
387
+ // and the first requirement below.
413
388
  const rebuilt = collapseBlankRunsOutsideFences([parts.before.trimEnd(), parts.headerLine, reqBody, parts.after.trim()]
414
389
  .filter((s) => s !== '')
415
390
  .join('\n\n')).trimEnd() + '\n';
416
391
  return {
417
392
  rebuilt,
418
393
  crlf,
419
- // The capability has no requirements left and this run is what emptied it.
420
- // Both halves matter. "No requirements left" alone would delete a spec on a
421
- // re-run of an already-synced REMOVED, taking with it any requirement added
422
- // since; "removed something" alone would delete a spec that still has
423
- // requirements. An emptied spec cannot be written either way — it fails
424
- // validation ("Spec must have at least one requirement") — so the caller
425
- // retires the capability instead of writing an unusable file.
394
+ // Both halves matter: "no requirements left" alone would delete a spec on a
395
+ // re-run of an already-synced REMOVED; "removed something" alone would
396
+ // delete a spec that still has requirements. An emptied spec cannot be
397
+ // written (it fails "at least one requirement"), so the caller retires it.
426
398
  retired: keptRequirements === 0 && removedApplied > 0,
427
399
  notices,
428
400
  counts: {
@@ -434,18 +406,13 @@ export async function buildUpdatedSpec(update, changeName, options = {}) {
434
406
  };
435
407
  }
436
408
  function normalizeBlockRaw(raw) {
437
- return raw.replace(/\r\n?/g, '\n').trim();
409
+ return normalizeDocument(raw).trim();
438
410
  }
439
411
  /**
440
- * Collapse runs of blank lines to a single blank line, skipping fenced code.
441
- *
442
- * The collapse exists because the four slices joined above each carry their own
443
- * boundary newlines, so the seams between them accumulate blank lines. Applied
444
- * to the whole document as `replace(/\n{3,}/g, '\n\n')` it could not tell a seam
445
- * from a blank line an author put inside a code sample deliberately, so every
446
- * archive quietly edited the examples in a spec. The seams are all outside any
447
- * fence by construction, so masking fenced lines loses nothing the collapse was
448
- * written for.
412
+ * The slices joined above each carry boundary newlines, so their seams
413
+ * accumulate blank lines. A whole-document `replace(/\n{3,}/g, '\n\n')` cannot
414
+ * tell a seam from a deliberate blank line inside a code sample. Seams are all
415
+ * outside fences by construction, so masking loses nothing.
449
416
  */
450
417
  function collapseBlankRunsOutsideFences(content) {
451
418
  const lines = content.split('\n');
@@ -455,9 +422,8 @@ function collapseBlankRunsOutsideFences(content) {
455
422
  for (let i = 0; i < lines.length; i++) {
456
423
  if (!mask[i] && lines[i].trim() === '') {
457
424
  const last = kept.length - 1;
458
- // Drop this blank only if the previous kept line is a blank that was also
459
- // outside a fence — a blank line inside a fence is content, and it must
460
- // not license dropping the one that follows it.
425
+ // The previous blank must also be outside a fence: a blank inside one is
426
+ // content, and must not license dropping the line that follows it.
461
427
  if (last >= 0 && !keptMask[last] && kept[last].trim() === '')
462
428
  continue;
463
429
  }
@@ -466,16 +432,11 @@ function collapseBlankRunsOutsideFences(content) {
466
432
  }
467
433
  return kept.join('\n');
468
434
  }
469
- /**
470
- * Write an updated spec to disk.
471
- */
472
435
  export async function writeUpdatedSpec(update, rebuilt, counts, options = {}) {
473
- // Create target directory if needed
474
436
  const targetDir = path.dirname(update.target);
475
437
  await fs.mkdir(targetDir, { recursive: true });
476
- // `rebuilt` is pure LF by construction; restore the convention the target had
477
- // before the merge so archiving a CRLF spec does not produce a whole-file diff
478
- // on Windows. A brand new spec has no prior convention and stays LF.
438
+ // `rebuilt` is pure LF by construction; restore the target's pre-merge
439
+ // convention so archiving a CRLF spec is not a whole-file diff on Windows.
479
440
  const content = options.crlf ? rebuilt.replace(/\r?\n/g, '\r\n') : rebuilt;
480
441
  await fs.writeFile(update.target, content);
481
442
  if (options.silent)
@@ -493,19 +454,15 @@ export async function writeUpdatedSpec(update, rebuilt, counts, options = {}) {
493
454
  }
494
455
  /**
495
456
  * The tail of a requirement block that is not part of the requirement: content
496
- * from the first `#`/`##`/`###` heading after the block's own header line.
497
- *
498
- * `####` is excluded deliberately — a requirement's `#### Scenario:` headings
499
- * are its own and go with it when it is removed.
457
+ * from the first `#`/`##`/`###` heading after its own header line. `####` is
458
+ * excluded — a requirement's `#### Scenario:` headings go with it.
500
459
  *
501
- * The alternative was to widen every heading pattern in the parsers so these
502
- * headings end the block properly. That was rejected: it reclassifies content,
503
- * so commented-out or indented examples start parsing as real requirements and
504
- * a spec that was valid becomes invalid. Salvaging on removal changes what a
505
- * removal deletes without changing what anything parses.
460
+ * Widening the parsers' heading patterns instead was rejected: it reclassifies
461
+ * content, so commented-out or indented examples begin parsing as requirements.
462
+ * Salvaging changes only what a removal deletes, not what anything parses.
506
463
  */
507
464
  function salvageAbsorbedContent(requirementRaw) {
508
- const lines = requirementRaw.replace(/\r\n?/g, '\n').split('\n');
465
+ const lines = normalizeDocument(requirementRaw).split('\n');
509
466
  const mask = buildCodeFenceMask(lines);
510
467
  for (let i = 1; i < lines.length; i++) {
511
468
  if (mask[i])
@@ -517,12 +474,9 @@ function salvageAbsorbedContent(requirementRaw) {
517
474
  return '';
518
475
  }
519
476
  /**
520
- * Retire a capability the change emptied: delete its `spec.md`, then remove any
521
- * directory the deletion left empty.
522
- *
523
- * The pruning walks up from the spec file and stops at `specsRoot`, which is
524
- * never removed even if it ends up empty — a project with no capabilities still
525
- * has a specs directory. It also stops at the first non-empty directory, so a
477
+ * Delete an emptied capability's `spec.md` and prune the directories that
478
+ * leaves empty. Pruning stops at `specsRoot` (a project with no capabilities
479
+ * still has a specs directory) and at the first non-empty directory, so a
526
480
  * sibling capability is never touched.
527
481
  */
528
482
  export async function retireSpec(update, specsRoot, options = {}) {
@@ -547,9 +501,8 @@ export async function retireSpec(update, specsRoot, options = {}) {
547
501
  console.log(`Retiring ${options.displayPath ?? `tospec/specs/${specName}/spec.md`}: no requirements left`);
548
502
  }
549
503
  /**
550
- * Build a skeleton spec for new capabilities. Carries the delta's authored
551
- * Purpose when one is supplied; otherwise writes the TBD placeholder — composed
552
- * from the constants validation recognises it by, so the two cannot drift.
504
+ * Skeleton spec for a new capability. The TBD placeholder is composed from the
505
+ * constants validation recognises it by, so the two cannot drift.
553
506
  */
554
507
  export function buildSpecSkeleton(specFolderName, changeName, purpose) {
555
508
  const titleBase = specFolderName;
@@ -563,8 +516,10 @@ export function buildSpecSkeleton(specFolderName, changeName, purpose) {
563
516
  * is none. Fence-masked so a `## Purpose` inside a code example is not mistaken
564
517
  * for the real section.
565
518
  */
566
- function extractDeltaPurpose(content) {
567
- const lines = content.replace(/\r\n?/g, '\n').split('\n');
519
+ export function extractDeltaPurpose(content) {
520
+ // normalizeDocument, not a hand-rolled newline collapse: it also strips the
521
+ // BOM, without which `<BOM>## Purpose` misses the `^##` anchor.
522
+ const lines = normalizeDocument(content).split('\n');
568
523
  const mask = buildCodeFenceMask(lines);
569
524
  let start = -1;
570
525
  for (let i = 0; i < lines.length; i++) {
@@ -589,13 +544,9 @@ function extractDeltaPurpose(content) {
589
544
  return lines.slice(start, end).join('\n').trim();
590
545
  }
591
546
  /**
592
- * A carried Purpose must not restructure the new spec. Headings, HTML comments,
593
- * and code fences each have ways to corrupt or hide the rest of the spec, so a
594
- * Purpose containing any of them falls back to the placeholder rather than risk
595
- * it.
596
- * Limitation: prose-only heuristic; a Purpose that genuinely needs a fence or
597
- * heading falls back to TBD. Loosen only once those corruption classes are
598
- * handled the way archive's own validator reads the spec.
547
+ * A carried Purpose must not restructure the new spec: headings, HTML comments
548
+ * and code fences can each corrupt or hide the rest of it, so any of them falls
549
+ * back to the placeholder.
599
550
  */
600
551
  function isPurposeCarrySafe(body) {
601
552
  if (!body)