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