@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,20 +1,12 @@
1
1
  /**
2
- * Decision (ADR) Command
2
+ * `tospec decision` — Architecture Decision Records. Unlike the `change`
3
+ * workflow these are permanent: one
4
+ * `tospec/decisions/<yyyyMMdd_HHmmss>-<topic>.md` file each.
3
5
  *
4
- * A dedicated command group for Architecture Decision Records. Unlike the
5
- * `change` workflow, decisions are permanent records: each one is a single
6
- * `tospec/decisions/<yyyyMMdd_HHmmss>-<topic>.md` file.
7
- *
8
- * `decision new` produces both files from the CLI's own templates: it writes the
9
- * dated decision file from `decision.md` (title / status / date pre-filled) and
10
- * appends a row to the persisted ledger `tospec/decisions/index.md`
11
- * (timestamp / title / summary), seeding it from `index.md` on first use. The
12
- * agent then fills the decision's prose sections.
13
- *
14
- * `decision list` derives a live status view from the files on disk.
15
- *
16
- * The `decision` schema (schemas/decision/) supplies both templates and the
17
- * required-sections contract; this command consumes them.
6
+ * `decision new` writes that file from the `decision` schema's template (title /
7
+ * status / date pre-filled) and appends a row to the ledger
8
+ * `tospec/decisions/index.md`, seeded from `index.md` on first use; the agent
9
+ * fills the prose sections. `decision list` derives a live view from disk.
18
10
  */
19
11
  import { Option } from 'commander';
20
12
  import path from 'path';
@@ -24,20 +16,13 @@ import { loadTemplate } from '../core/artifact-graph/index.js';
24
16
  import { MarkdownParser, findSection } from '../core/parsers/markdown-parser.js';
25
17
  import { completeRootStructure, validateChangeName } from '../utils/change-utils.js';
26
18
  import { formatTimestamp } from '../utils/timestamp.js';
27
- import { DEFAULT_SCHEMA } from './workflow/shared.js';
19
+ import { DEFAULT_SCHEMA, DECISION_SCHEMA_NAME } from '../core/schema-names.js';
28
20
  import { emitSuccess, emitFailure } from './shared-output.js';
29
- // -----------------------------------------------------------------------------
30
- // Types
31
- // -----------------------------------------------------------------------------
21
+ import { stripBom } from '../core/parsers/requirement-text.js';
32
22
  const DECISION_STATUSES = ['proposed', 'accepted', 'superseded'];
33
- /**
34
- * Exported so `new change` can refuse it by name. The decision schema backs
35
- * this command's ADR writer, not the change graph: its artifact lands in
36
- * `tospec/decisions/`, so a change created against it produced a directory
37
- * under `tospec/changes/` that `decision list` never saw, with no ticket and
38
- * no `type`.
39
- */
40
- export const DECISION_SCHEMA_NAME = 'decision';
23
+ // Re-exported for callers that reached it here; defined in core/schema-names so
24
+ // workflow/shared.ts no longer has to import this command module.
25
+ export { DECISION_SCHEMA_NAME };
41
26
  /** The `--json` null-shapes; shared by the commander table and the catch sites. */
42
27
  export const DECISION_NEW_FAILURE_PAYLOAD = {
43
28
  decision: null,
@@ -51,18 +36,14 @@ const SCHEMA_NAME = DECISION_SCHEMA_NAME;
51
36
  const DECISIONS_SUBDIR = path.join('tospec', 'decisions');
52
37
  // Aligns with the project-wide CLI timestamp (formatTimestamp): yyyyMMdd_HHmmss.
53
38
  const TIMESTAMP_RE = /^\d{8}_\d{6}$/;
54
- /** The same shape, split into fields for calendar checking and display. */
55
39
  const TIMESTAMP_PARTS_RE = /^(\d{4})(\d{2})(\d{2})_(\d{2})(\d{2})(\d{2})$/;
56
40
  const FILE_RE = /^(\d{8}_\d{6})-(.+)\.md$/;
57
- // -----------------------------------------------------------------------------
58
- // Helpers
59
- // -----------------------------------------------------------------------------
60
41
  /** Title = the H1 heading; falls back to the topic derived from the filename. */
61
42
  function parseTitle(sections, fallback) {
62
43
  const h1 = sections.find((s) => s.level === 1);
63
44
  return h1?.title.trim() || fallback;
64
45
  }
65
- /** Status = the first ASCII-word bullet under the `Status` section (the `Date:` bullet comes after it, so first-match wins). */
46
+ /** Status = the first ASCII-word bullet under `Status` (the `Date:` bullet follows it). */
66
47
  function parseStatus(sections) {
67
48
  const statusSection = findSection(sections, 'Status');
68
49
  if (!statusSection)
@@ -100,33 +81,34 @@ export function readDecisionEntries(decisionsDir) {
100
81
  }
101
82
  /** Fills CLI-owned fields (title / date / status) into the schema template. */
102
83
  function renderDecision(template, title, stamp, status) {
84
+ // Function replacers: a string replacement expands `$&`, `$'` and `` $` ``, so
85
+ // a `--title` containing one wrote pieces of the template into the heading.
103
86
  return template
104
- .replace(/^# .*$/m, `# ${title}`)
105
- .replace('[yyyyMMdd_HHmmss]', stamp)
106
- .replace(/^- proposed$/m, `- ${status}`);
87
+ .replace(/^# .*$/m, () => `# ${title}`)
88
+ .replace('[yyyyMMdd_HHmmss]', () => stamp)
89
+ .replace(/^- proposed$/m, () => `- ${status}`);
107
90
  }
108
- /** `20260722_161825` -> `2026-07-22 16:18:25` for the index row (falls back to the raw stamp). */
109
91
  /**
110
92
  * Whether a shape-valid stamp also names a real instant.
111
93
  *
112
- * The regex alone accepts `20261345_996199` — month 13, day 45, hour 99 — and
113
- * `--date` is the only place a timestamp comes from outside `formatTimestamp`.
114
- * The bad value then became the filename, the ledger's human date
115
- * (`2026-13-45 99:61:99`), and the sort key `decision list --sort date` orders
116
- * by, where it outranks every real record.
117
- *
118
- * Round-tripped rather than range-checked per field, so month lengths and leap
119
- * days come from the calendar instead of a second implementation of it.
94
+ * `TIMESTAMP_RE` alone accepts `20261345_996199`, and that value becomes the
95
+ * filename, the ledger's date and the `--sort date` key. Round-tripped rather
96
+ * than range-checked per field, so month lengths and leap days come from the
97
+ * calendar.
120
98
  */
121
99
  function isRealTimestamp(stamp) {
122
100
  const m = TIMESTAMP_PARTS_RE.exec(stamp);
123
101
  if (!m)
124
102
  return false;
125
103
  const [, y, mo, d, h, mi, sec] = m.map(Number);
126
- const date = new Date(y, mo - 1, d, h, mi, sec);
127
- return (date.getFullYear() === y && date.getMonth() === mo - 1 && date.getDate() === d
128
- && date.getHours() === h && date.getMinutes() === mi && date.getSeconds() === sec);
104
+ // UTC, not local time: the stamp is a calendar label, and a local round-trip
105
+ // rejected the hour a DST transition skips (02:30 on the spring-forward day
106
+ // in a DST zone), making a `--date` valid on one machine and not another.
107
+ const date = new Date(Date.UTC(y, mo - 1, d, h, mi, sec));
108
+ return (date.getUTCFullYear() === y && date.getUTCMonth() === mo - 1 && date.getUTCDate() === d
109
+ && date.getUTCHours() === h && date.getUTCMinutes() === mi && date.getUTCSeconds() === sec);
129
110
  }
111
+ /** `20260722_161825` -> `2026-07-22 16:18:25`, or the raw stamp if unparseable. */
130
112
  function stampToHuman(stamp) {
131
113
  const m = TIMESTAMP_PARTS_RE.exec(stamp);
132
114
  if (!m)
@@ -148,11 +130,9 @@ function ensureIndex(decisionsDir, projectRoot) {
148
130
  }
149
131
  return indexPath;
150
132
  }
151
- // Limitation: no dedup by topic — two decisions on different dates for the same
152
- // topic are two files and so two rows. Dedup by topic if the ledger ever grows
153
- // noisy enough to matter.
154
- // `file` is the sibling filename (index.md lives in the same decisions dir), so
155
- // a relative markdown link resolves straight to the decision doc.
133
+ // `file` is the sibling filename (index.md lives in the decisions dir), so the
134
+ // relative link resolves straight to the decision doc.
135
+ // Limitation: no dedup by topic — two dates for one topic are two rows.
156
136
  function indexRow(timestamp, status, title, summary, file) {
157
137
  const fileLink = `[${indexCell(file)}](${file})`;
158
138
  return `| ${timestamp} | ${indexCell(status)} | ${indexCell(title)} | ${indexCell(summary)} | ${fileLink} |`;
@@ -161,19 +141,12 @@ function indexRow(timestamp, status, title, summary, file) {
161
141
  const INDEX_HEADER = '| timestamp | status | title | summary | file |';
162
142
  const INDEX_SEPARATOR = '| ----- | ----- | ----- | ----- | ----- |';
163
143
  /**
164
- * Brings a pre-status ledger up to the current five-column shape.
165
- *
166
- * Round 4 report 14: `index.md` recorded timestamp/title/summary/file, so a
167
- * superseded decision was indistinguishable from a live one in the file people
168
- * and agents actually read — the decision skill even tells the author to flip an
169
- * older record's status, and nothing about the ledger changed when they did.
144
+ * Brings a pre-status ledger up to the current five-column shape. Without the
145
+ * column a superseded decision is indistinguishable from a live one.
170
146
  *
171
- * Each existing row's status is read from the decision file its link points at,
172
- * rather than left blank: the value is on disk already, and a column of empties
173
- * would be a new way to not know. Rows whose file is gone keep the `unknown`
174
- * that `decision list` reports for the same case. Everything else in the row is
175
- * preserved — this is a column insert, not a rebuild, so hand-written cells
176
- * survive.
147
+ * Each row's status is read from the file its link points at rather than left
148
+ * blank; a missing file keeps the `unknown` `decision list` reports. A column
149
+ * insert, not a rebuild, so hand-written cells survive.
177
150
  */
178
151
  function upgradeIndexColumns(lines, decisionsDir) {
179
152
  return lines.map((line) => {
@@ -192,6 +165,39 @@ function upgradeIndexColumns(lines, decisionsDir) {
192
165
  return `|${cells[0]}| ${status} |${cells[1]}|${cells[2]}|${cells[3]}|`;
193
166
  });
194
167
  }
168
+ /**
169
+ * Re-reads every row's status from the decision file it links to, on every index
170
+ * write. A status changes on a *different* row than the one being written: the
171
+ * decision skill tells the author to flip an older record to `superseded` and
172
+ * forbids hand-editing `index.md`.
173
+ *
174
+ * Only the status cell is touched; hand-edited cells survive, and so does a row
175
+ * whose file is gone — see the guard below.
176
+ */
177
+ function refreshIndexStatuses(lines, decisionsDir) {
178
+ return lines.map((line) => {
179
+ const trimmed = line.trim();
180
+ if (!trimmed.startsWith('|'))
181
+ return line;
182
+ if (/^\|\s*timestamp\s*\|/i.test(trimmed))
183
+ return line;
184
+ if (/^[|\s-]+$/.test(trimmed))
185
+ return line;
186
+ const cells = trimmed.slice(1, -1).split('|');
187
+ if (cells.length !== 5)
188
+ return line;
189
+ const fileMatch = /\]\(([^)]+)\)/.exec(cells[4]);
190
+ if (!fileMatch)
191
+ return line;
192
+ // A row whose file no longer exists is left exactly as it is: rewriting it
193
+ // to `unknown` would overwrite the last surviving record of that decision,
194
+ // and would count as a refresh on a row that never drifted.
195
+ if (!fs.existsSync(path.join(decisionsDir, fileMatch[1])))
196
+ return line;
197
+ const status = statusOfDecisionFile(decisionsDir, fileMatch[1]);
198
+ return `|${cells[0]}| ${status} |${cells[2]}|${cells[3]}|${cells[4]}|`;
199
+ });
200
+ }
195
201
  /** The status recorded in a decision file, or `unknown` when it cannot be read. */
196
202
  function statusOfDecisionFile(decisionsDir, file) {
197
203
  try {
@@ -203,47 +209,164 @@ function statusOfDecisionFile(decisionsDir, file) {
203
209
  }
204
210
  }
205
211
  /**
206
- * Write this decision's row, replacing the existing one for the same file.
207
- *
208
- * One row per file, so `--force` must not append a second — but it must not
209
- * leave the old one either. Skipping the write entirely was the previous
210
- * behaviour, and it let `--force` change a decision's title and status in the
211
- * file while the ledger kept advertising the old ones: `decision list` reads the
212
- * files and index.md is read by people, so the two silently disagreed with no
213
- * way to tell which was current.
212
+ * Writes this decision's row, replacing the existing one for the same file: one
213
+ * row per file, so `--force` must neither append a second nor leave the old one
214
+ * advertising a title and status the file no longer has.
214
215
  *
215
- * Matched on the filename rather than the title, because the title is exactly
216
- * what a rewrite is likely to change.
216
+ * Matched on the filename, not the title — the title is what a rewrite changes.
217
217
  */
218
218
  function writeIndexRow(indexPath, timestamp, status, title, summary, file) {
219
219
  const row = indexRow(timestamp, status, title, summary, file);
220
- let existing = fs.readFileSync(indexPath, 'utf-8');
221
- // Preserve whatever endings the ledger already uses (see the CRLF note in
222
- // CONVENTIONS): split tolerantly, rejoin with what was found.
220
+ // BOM stripped so the rewritten ledger comes out BOM-free and the
221
+ // `| status |` header test below cannot be defeated by one.
222
+ const existing = stripBom(fs.readFileSync(indexPath, 'utf-8'));
223
+ // Preserve whatever endings the ledger already uses.
223
224
  const eol = existing.includes('\r\n') ? '\r\n' : '\n';
224
225
  let lines = existing.split(/\r?\n/);
225
- // A ledger written before the status column gets it here, filled from the
226
- // decision files, rather than growing a row one cell wider than its header.
226
+ // A ledger written before the status column gets it here, rather than growing
227
+ // a row one cell wider than its header.
227
228
  if (!existing.includes('| status |')) {
228
229
  lines = upgradeIndexColumns(lines, path.dirname(indexPath));
229
- existing = lines.join(eol);
230
- fs.writeFileSync(indexPath, existing, 'utf-8');
231
230
  }
231
+ // Every other row's status is brought up to date in the same write. The row
232
+ // being written is excluded by construction — it is replaced with `row` below.
233
+ lines = refreshIndexStatuses(lines, path.dirname(indexPath));
232
234
  const linkTarget = `](${file})`;
233
235
  const index = lines.findIndex((line) => line.startsWith('|') && line.includes(linkTarget));
234
236
  if (index === -1) {
235
- const separator = existing.length > 0 && !existing.endsWith('\n') ? eol : '';
236
- fs.appendFileSync(indexPath, separator + row + eol, 'utf-8');
237
+ const body = lines.join(eol);
238
+ const separator = body.length > 0 && !body.endsWith('\n') ? eol : '';
239
+ fs.writeFileSync(indexPath, body + separator + row + eol, 'utf-8');
237
240
  return;
238
241
  }
239
242
  lines[index] = row;
240
243
  fs.writeFileSync(indexPath, lines.join(eol), 'utf-8');
241
244
  }
242
- // -----------------------------------------------------------------------------
243
- // Command implementations (exported for tests)
244
- // -----------------------------------------------------------------------------
245
+ /** The decision file an index row links to, or null for a non-row line. */
246
+ function indexRowFile(line) {
247
+ const trimmed = line.trim();
248
+ if (!trimmed.startsWith('|'))
249
+ return null;
250
+ return /\]\(([^)]+)\)/.exec(trimmed)?.[1] ?? null;
251
+ }
252
+ /**
253
+ * A data row's timestamp cell, or null for the header, the separator and prose.
254
+ * `yyyy-MM-dd HH:mm:ss` sorts lexicographically in chronological order, so the
255
+ * string is its own comparison key — no date parsing, and a hand-edited cell
256
+ * compares as text rather than throwing.
257
+ */
258
+ function indexRowTimestamp(line) {
259
+ const trimmed = line.trim();
260
+ if (!trimmed.startsWith('|'))
261
+ return null;
262
+ if (/^\|\s*timestamp\s*\|/i.test(trimmed))
263
+ return null;
264
+ if (/^[|\s-]+$/.test(trimmed))
265
+ return null;
266
+ const cells = trimmed.slice(1, -1).split('|');
267
+ if (cells.length !== 5)
268
+ return null;
269
+ return cells[0].trim();
270
+ }
271
+ /**
272
+ * Splices recovered rows into the ledger in timestamp order rather than onto the
273
+ * end: a row recovered by `--reindex` is dated whenever it was actually decided,
274
+ * so appending left index.md disagreeing with the sorted `decision list`.
275
+ *
276
+ * Existing rows are never reordered, only inserted between, so a hand-arranged
277
+ * ledger keeps its arrangement.
278
+ */
279
+ function spliceRowsByTimestamp(body, additions) {
280
+ const result = [...body];
281
+ for (const addition of additions) {
282
+ const at = result.findIndex((line) => {
283
+ const stamp = indexRowTimestamp(line);
284
+ return stamp !== null && stamp > addition.timestamp;
285
+ });
286
+ if (at === -1)
287
+ result.push(addition.row);
288
+ else
289
+ result.splice(at, 0, addition.row);
290
+ }
291
+ return result;
292
+ }
293
+ /**
294
+ * Rows whose linked decision file is gone, read without touching the ledger.
295
+ *
296
+ * Split out of `reindexDecisionIndex` so a plain `decision list` can report the
297
+ * same rot: `list` reads the decision files, so a row naming a deleted one is
298
+ * invisible to it, and the rot was only surfaced by `--reindex` — a flag whose
299
+ * name is about writing, which nobody reaches for to ask a question.
300
+ */
301
+ export function findDanglingIndexRows(decisionsDir) {
302
+ const indexPath = path.join(decisionsDir, 'index.md');
303
+ if (!fs.existsSync(indexPath))
304
+ return [];
305
+ const lines = stripBom(fs.readFileSync(indexPath, 'utf-8')).split(/\r?\n/);
306
+ const linked = new Set(lines.map((line) => indexRowFile(line)).filter((file) => file !== null));
307
+ return [...linked].filter((file) => !fs.existsSync(path.join(decisionsDir, file)));
308
+ }
309
+ /**
310
+ * Brings `index.md` back in line with the decision files without creating one.
311
+ * Three ways a ledger falls behind:
312
+ *
313
+ * 1. A stale status cell. Rewritten from the file it links to.
314
+ * 2. A decision file with no row (how one arrives from another branch).
315
+ * Inserted in timestamp order.
316
+ * 3. A row whose file is gone. Named in the result, never removed — the ledger
317
+ * is a historical record.
318
+ */
319
+ export function reindexDecisionIndex(decisionsDir) {
320
+ const empty = { statusesRefreshed: 0, rowsAdded: [], danglingRows: [] };
321
+ const indexPath = path.join(decisionsDir, 'index.md');
322
+ if (!fs.existsSync(indexPath))
323
+ return empty;
324
+ const existing = stripBom(fs.readFileSync(indexPath, 'utf-8'));
325
+ const eol = existing.includes('\r\n') ? '\r\n' : '\n';
326
+ const lines = existing.split(/\r?\n/);
327
+ const refreshed = refreshIndexStatuses(lines, decisionsDir);
328
+ const statusesRefreshed = refreshed.reduce((count, line, i) => (line === lines[i] ? count : count + 1), 0);
329
+ const linked = new Set(refreshed.map((line) => indexRowFile(line)).filter((file) => file !== null));
330
+ const danglingRows = [...linked].filter((file) => !fs.existsSync(path.join(decisionsDir, file)));
331
+ // Oldest first, so recovered rows land in the order the ledger would have
332
+ // grown had they never gone missing.
333
+ const missing = readDecisionEntries(decisionsDir)
334
+ .filter((entry) => !linked.has(entry.file))
335
+ .sort((a, b) => a.date.localeCompare(b.date));
336
+ const added = missing.map((entry) => ({
337
+ timestamp: stampToHuman(entry.date),
338
+ // Summary left empty: no file carries it, and reusing the title would put
339
+ // text in the ledger nobody wrote.
340
+ row: indexRow(stampToHuman(entry.date), entry.status, entry.title, '', entry.file),
341
+ }));
342
+ if (statusesRefreshed > 0 || added.length > 0) {
343
+ const body = [...refreshed];
344
+ // Trailing blank lines are the file's ending, not rows; splice above them.
345
+ while (body.length > 0 && body[body.length - 1].trim() === '')
346
+ body.pop();
347
+ fs.writeFileSync(indexPath, spliceRowsByTimestamp(body, added).join(eol) + eol, 'utf-8');
348
+ }
349
+ return { statusesRefreshed, rowsAdded: missing.map((entry) => entry.file), danglingRows };
350
+ }
245
351
  export async function decisionNewCommand(topic, options) {
352
+ // Root first, as `new change` does: an argument failure still happened
353
+ // somewhere, and `root: null` could not say where.
354
+ let resolvedRoot = null;
355
+ const failurePayload = () => ({
356
+ decision: null,
357
+ root: resolvedRoot ? toRootOutput(resolvedRoot) : null,
358
+ });
246
359
  try {
360
+ // Same rule as `new change`: a writing command never scaffolds a root in
361
+ // an uninitialised directory; that is `tospec init`'s job.
362
+ resolvedRoot = await resolveRootForCommand(options, {
363
+ json: options.json,
364
+ allowImplicitRoot: false,
365
+ failurePayload: failurePayload(),
366
+ });
367
+ if (!resolvedRoot)
368
+ return;
369
+ const root = resolvedRoot;
247
370
  if (!topic)
248
371
  throw new Error('Missing required argument <topic>');
249
372
  const nameValidation = validateChangeName(topic, 'Topic');
@@ -261,12 +384,6 @@ export async function decisionNewCommand(topic, options) {
261
384
  if (!isRealTimestamp(stamp)) {
262
385
  throw new Error(`Invalid --date '${stamp}'. That is not a real date and time.`);
263
386
  }
264
- const root = await resolveRootForCommand(options, {
265
- json: options.json,
266
- failurePayload: DECISION_NEW_FAILURE_PAYLOAD,
267
- });
268
- if (!root)
269
- return;
270
387
  const decisionsDir = path.join(root.path, DECISIONS_SUBDIR);
271
388
  const fileName = `${stamp}-${topic}.md`;
272
389
  const filePath = path.join(decisionsDir, fileName);
@@ -274,24 +391,19 @@ export async function decisionNewCommand(topic, options) {
274
391
  const summary = options.summary ?? title;
275
392
  const template = loadTemplate(SCHEMA_NAME, 'decision.md', root.path);
276
393
  const content = renderDecision(template, title, stamp, status);
277
- // CLI produces both files: the dated decision file from its template, and
278
- // the ledger (seeded from index.md on first use) with a row for this record.
279
- //
280
- // The root is completed first, not just `decisions/`. This command accepts an
281
- // implicit root, so it can be what creates `tospec/` — and creating only the
282
- // one subdirectory it needs left a half-root that every other command then
283
- // resolved as a project, making `validate --all` and `status --all` report a
284
- // clean sweep over somewhere that had never been one. `new change` has always
285
- // completed the root for this reason; they now share the one implementation.
394
+ // The whole root, not just `decisions/`. The root above is always an
395
+ // explicit `tospec/` (allowImplicitRoot: false), but it may be a partial
396
+ // one — a `tospec/` with only `changes/` — and creating just `decisions/`
397
+ // inside it would leave a half-root that every other command resolves as
398
+ // a project. Directories only; no config.yaml is written here.
286
399
  await completeRootStructure(root.path, DEFAULT_SCHEMA);
287
400
  fs.mkdirSync(decisionsDir, { recursive: true });
288
401
  if (options.force) {
289
402
  fs.writeFileSync(filePath, content, 'utf-8');
290
403
  }
291
404
  else {
292
- // 'wx' makes the create atomic: the filesystem itself enforces the
293
- // guard, closing the check-then-write window a separate existsSync
294
- // check would leave open between two concurrent invocations.
405
+ // 'wx' makes the create atomic, closing the check-then-write window a
406
+ // separate existsSync would leave open.
295
407
  try {
296
408
  fs.writeFileSync(filePath, content, { encoding: 'utf-8', flag: 'wx' });
297
409
  }
@@ -303,9 +415,8 @@ export async function decisionNewCommand(topic, options) {
303
415
  }
304
416
  }
305
417
  const indexPath = ensureIndex(decisionsDir, root.path);
306
- // Written unconditionally: one row per decision file, and a --force that
307
- // rewrote the file has to carry its new title and summary into the ledger
308
- // rather than leave the previous ones standing.
418
+ // Unconditional: a --force that rewrote the file has to carry its new title
419
+ // and summary into the ledger.
309
420
  writeIndexRow(indexPath, stampToHuman(stamp), status, title, summary, fileName);
310
421
  const payload = {
311
422
  decision: { topic, title, date: stamp, status, summary, path: filePath, indexPath },
@@ -322,7 +433,7 @@ export async function decisionNewCommand(topic, options) {
322
433
  }
323
434
  catch (error) {
324
435
  if (options.json) {
325
- emitFailure(true, DECISION_NEW_FAILURE_PAYLOAD, error, 'decision_error');
436
+ emitFailure(true, failurePayload(), error, 'decision_error');
326
437
  return;
327
438
  }
328
439
  throw error;
@@ -330,11 +441,9 @@ export async function decisionNewCommand(topic, options) {
330
441
  }
331
442
  export async function decisionListCommand(options) {
332
443
  try {
333
- // No implicit root, for the reason `list`, `status --all` and `validate --all`
334
- // refuse one: this is a batch query, and on a directory that is not a project
335
- // it answered "no decisions here" with exit 0 — a clean report about somewhere
336
- // never examined. A named lookup cannot fail that way, which is why `show` and
337
- // `validate <item>` still accept an implicit root; a listing can.
444
+ // No implicit root, like `list` / `status --all` / `validate --all`: a batch
445
+ // query outside a project would answer "no decisions here" with exit 0, a
446
+ // clean report about somewhere never examined.
338
447
  const root = await resolveRootForCommand(options, {
339
448
  json: options.json,
340
449
  allowImplicitRoot: false,
@@ -344,6 +453,24 @@ export async function decisionListCommand(options) {
344
453
  return;
345
454
  const decisionsDir = path.join(root.path, DECISIONS_SUBDIR);
346
455
  let entries = readDecisionEntries(decisionsDir);
456
+ // Taken before the filter, so the empty-state message can tell "nothing
457
+ // recorded" from "nothing matching".
458
+ const totalEntries = entries.length;
459
+ // The only write this command can perform, behind an explicit flag so a
460
+ // query never rewrites a tracked file unasked.
461
+ let reindexed = {
462
+ statusesRefreshed: 0,
463
+ rowsAdded: [],
464
+ danglingRows: [],
465
+ };
466
+ if (options.reindex) {
467
+ reindexed = reindexDecisionIndex(decisionsDir);
468
+ }
469
+ else {
470
+ // Read-only: a query reports the rot it can see without rewriting a
471
+ // tracked file.
472
+ reindexed = { ...reindexed, danglingRows: findDanglingIndexRows(decisionsDir) };
473
+ }
347
474
  if (options.status) {
348
475
  entries = entries.filter((e) => e.status === options.status);
349
476
  }
@@ -352,20 +479,69 @@ export async function decisionListCommand(options) {
352
479
  entries.sort((a, b) => a.topic.localeCompare(b.topic));
353
480
  }
354
481
  else {
355
- // date desc (newest first); tie-break by topic for stability
482
+ // Newest first; tie-break by topic for stability.
356
483
  entries.sort((a, b) => b.date.localeCompare(a.date) || a.topic.localeCompare(b.topic));
357
484
  }
358
485
  if (options.json) {
359
- emitSuccess({ decisions: entries }, toRootOutput(root));
486
+ emitSuccess({
487
+ decisions: entries,
488
+ ...(options.reindex
489
+ ? {
490
+ // `reindexedRows` keeps its original meaning (status cells
491
+ // rewritten); the other two drifts get their own fields.
492
+ reindexedRows: reindexed.statusesRefreshed,
493
+ addedRows: reindexed.rowsAdded,
494
+ danglingRows: reindexed.danglingRows,
495
+ }
496
+ : // Without --reindex the write counters would be a constant zero
497
+ // that reads as "checked, nothing to do"; only the drift this
498
+ // query actually looked for is reported, and only when there is
499
+ // some.
500
+ reindexed.danglingRows.length > 0
501
+ ? { danglingRows: reindexed.danglingRows }
502
+ : {}),
503
+ }, toRootOutput(root));
360
504
  return;
361
505
  }
506
+ // Posix separators throughout: `DECISIONS_SUBDIR` is a `path.join`, so on
507
+ // Windows appending `/index.md` printed `tospec\decisions/index.md`.
508
+ const ledger = `${DECISIONS_SUBDIR.split(path.sep).join('/')}/index.md`;
509
+ const hasDangling = reindexed.danglingRows.length > 0;
510
+ if (options.reindex) {
511
+ const done = [];
512
+ if (reindexed.statusesRefreshed > 0) {
513
+ done.push(`refreshed ${reindexed.statusesRefreshed} status cell(s)`);
514
+ }
515
+ if (reindexed.rowsAdded.length > 0) {
516
+ done.push(`added ${reindexed.rowsAdded.length} missing row(s): ${reindexed.rowsAdded.join(', ')}`);
517
+ }
518
+ if (done.length > 0) {
519
+ console.log(`${ledger}: ${done.join('; ')}.`);
520
+ }
521
+ else if (!hasDangling) {
522
+ // The all-clear is withheld while a row points at a file that is gone:
523
+ // saying the ledger agrees with the decision files and then naming rows
524
+ // that do not is two answers to one question.
525
+ console.log(`${ledger} is already in sync with the decision files.`);
526
+ }
527
+ else {
528
+ console.log(`${ledger}: no status cell or row needed rewriting, but it is not clean:`);
529
+ }
530
+ }
531
+ if (hasDangling) {
532
+ console.log(`Warning: ${reindexed.danglingRows.length} row(s) in ${ledger} link to a decision file that no longer exists: ${reindexed.danglingRows.join(', ')}.`);
533
+ console.log(options.reindex
534
+ ? 'Their status reads "unknown". Restore the file or remove the row by hand.'
535
+ : 'Restore the file or remove the row by hand; `tospec decision list --reindex` reports them without deleting any.');
536
+ }
362
537
  if (entries.length === 0) {
363
- console.log('No decisions found. Create one with: tospec decision new <topic>');
538
+ console.log(options.status && totalEntries > 0
539
+ ? `No decisions with status '${options.status}' (${totalEntries} recorded). Drop --status to see them all.`
540
+ : 'No decisions found. Create one with: tospec decision new <topic>');
364
541
  return;
365
542
  }
366
- // Widths derived from the data, not guessed: the header was padded to 12
367
- // and 13 while a `yyyyMMdd_HHmmss` stamp is 15, so no row ever lined up
368
- // with the rule beneath it.
543
+ // Widths derived from the data: a hardcoded width never lined a
544
+ // `yyyyMMdd_HHmmss` stamp up with the rule beneath it.
369
545
  const dateWidth = Math.max('Date'.length, ...entries.map((e) => e.date.length));
370
546
  const statusWidth = Math.max('Status'.length, ...entries.map((e) => e.status.length));
371
547
  console.log(`${'Date'.padEnd(dateWidth)} | ${'Status'.padEnd(statusWidth)} | Title`);
@@ -382,14 +558,6 @@ export async function decisionListCommand(options) {
382
558
  throw error;
383
559
  }
384
560
  }
385
- // -----------------------------------------------------------------------------
386
- // Registration
387
- // -----------------------------------------------------------------------------
388
- /**
389
- * Register the `decision` command group and its subcommands.
390
- *
391
- * @param program - The Commander program instance
392
- */
393
561
  export function registerDecisionCommand(program) {
394
562
  const decisionCmd = program
395
563
  .command('decision')
@@ -408,8 +576,10 @@ export function registerDecisionCommand(program) {
408
576
  await decisionNewCommand(topic, options);
409
577
  }
410
578
  catch (error) {
411
- console.error(error instanceof Error ? error.message : String(error));
412
- process.exitCode = 1;
579
+ // The command already emitted its own JSON envelope; this catch only
580
+ // reaches the human path, which uses the same `Error:` line as every
581
+ // other command.
582
+ emitFailure(false, {}, error, 'decision_error');
413
583
  }
414
584
  });
415
585
  decisionCmd
@@ -417,14 +587,14 @@ export function registerDecisionCommand(program) {
417
587
  .description('List decisions as an index table')
418
588
  .addOption(new Option('--status <status>', 'Filter by status').choices(DECISION_STATUSES))
419
589
  .addOption(new Option('--sort <mode>', 'Sort order').choices(['date', 'name']).default('date'))
590
+ .option('--reindex', "Rewrite decisions/index.md's status column from the decision files")
420
591
  .option('--json', 'Output as JSON')
421
592
  .action(async (options) => {
422
593
  try {
423
594
  await decisionListCommand(options);
424
595
  }
425
596
  catch (error) {
426
- console.error(error instanceof Error ? error.message : String(error));
427
- process.exitCode = 1;
597
+ emitFailure(false, {}, error, 'decision_error');
428
598
  }
429
599
  });
430
600
  }