@seanmars/tospec 0.19.0-beta.0 → 0.19.0-beta.13

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 (431) hide show
  1. package/CHANGELOG.md +578 -0
  2. package/README.md +71 -77
  3. package/assets/dashboard/app.js +14 -3
  4. package/assets/dashboard/style.css +7 -0
  5. package/assets/metrics/app.js +22 -0
  6. package/assets/metrics/style.css +8 -0
  7. package/assets/rules/tospec/decision.md +3 -0
  8. package/assets/rules/tospec/single-source-of-truth.md +19 -0
  9. package/dist/cli/index.d.ts.map +1 -1
  10. package/dist/cli/index.js +266 -82
  11. package/dist/cli/index.js.map +1 -1
  12. package/dist/commands/config.d.ts +9 -17
  13. package/dist/commands/config.d.ts.map +1 -1
  14. package/dist/commands/config.js +374 -95
  15. package/dist/commands/config.js.map +1 -1
  16. package/dist/commands/dashboard.d.ts +64 -81
  17. package/dist/commands/dashboard.d.ts.map +1 -1
  18. package/dist/commands/dashboard.js +350 -218
  19. package/dist/commands/dashboard.js.map +1 -1
  20. package/dist/commands/decision.d.ts +41 -19
  21. package/dist/commands/decision.d.ts.map +1 -1
  22. package/dist/commands/decision.js +400 -70
  23. package/dist/commands/decision.js.map +1 -1
  24. package/dist/commands/metrics.d.ts +34 -48
  25. package/dist/commands/metrics.d.ts.map +1 -1
  26. package/dist/commands/metrics.js +74 -84
  27. package/dist/commands/metrics.js.map +1 -1
  28. package/dist/commands/shared-output.d.ts +24 -10
  29. package/dist/commands/shared-output.d.ts.map +1 -1
  30. package/dist/commands/shared-output.js +62 -11
  31. package/dist/commands/shared-output.js.map +1 -1
  32. package/dist/commands/show.d.ts +7 -0
  33. package/dist/commands/show.d.ts.map +1 -1
  34. package/dist/commands/show.js +39 -8
  35. package/dist/commands/show.js.map +1 -1
  36. package/dist/commands/validate.d.ts +47 -30
  37. package/dist/commands/validate.d.ts.map +1 -1
  38. package/dist/commands/validate.js +282 -108
  39. package/dist/commands/validate.js.map +1 -1
  40. package/dist/commands/workflow/index.d.ts +6 -10
  41. package/dist/commands/workflow/index.d.ts.map +1 -1
  42. package/dist/commands/workflow/index.js +6 -10
  43. package/dist/commands/workflow/index.js.map +1 -1
  44. package/dist/commands/workflow/instructions.d.ts +21 -8
  45. package/dist/commands/workflow/instructions.d.ts.map +1 -1
  46. package/dist/commands/workflow/instructions.js +254 -95
  47. package/dist/commands/workflow/instructions.js.map +1 -1
  48. package/dist/commands/workflow/new-change.d.ts +4 -5
  49. package/dist/commands/workflow/new-change.d.ts.map +1 -1
  50. package/dist/commands/workflow/new-change.js +90 -25
  51. package/dist/commands/workflow/new-change.js.map +1 -1
  52. package/dist/commands/workflow/schemas.d.ts +3 -5
  53. package/dist/commands/workflow/schemas.d.ts.map +1 -1
  54. package/dist/commands/workflow/schemas.js +37 -11
  55. package/dist/commands/workflow/schemas.js.map +1 -1
  56. package/dist/commands/workflow/shared.d.ts +48 -21
  57. package/dist/commands/workflow/shared.d.ts.map +1 -1
  58. package/dist/commands/workflow/shared.js +36 -33
  59. package/dist/commands/workflow/shared.js.map +1 -1
  60. package/dist/commands/workflow/status.d.ts +10 -6
  61. package/dist/commands/workflow/status.d.ts.map +1 -1
  62. package/dist/commands/workflow/status.js +84 -42
  63. package/dist/commands/workflow/status.js.map +1 -1
  64. package/dist/commands/workflow/templates.d.ts +10 -3
  65. package/dist/commands/workflow/templates.d.ts.map +1 -1
  66. package/dist/commands/workflow/templates.js +39 -40
  67. package/dist/commands/workflow/templates.js.map +1 -1
  68. package/dist/core/archive.d.ts +26 -21
  69. package/dist/core/archive.d.ts.map +1 -1
  70. package/dist/core/archive.js +415 -201
  71. package/dist/core/archive.js.map +1 -1
  72. package/dist/core/artifact-graph/graph.d.ts +29 -36
  73. package/dist/core/artifact-graph/graph.d.ts.map +1 -1
  74. package/dist/core/artifact-graph/graph.js +50 -58
  75. package/dist/core/artifact-graph/graph.js.map +1 -1
  76. package/dist/core/artifact-graph/index.d.ts +2 -2
  77. package/dist/core/artifact-graph/index.d.ts.map +1 -1
  78. package/dist/core/artifact-graph/index.js +2 -2
  79. package/dist/core/artifact-graph/index.js.map +1 -1
  80. package/dist/core/artifact-graph/instruction-loader.d.ts +105 -100
  81. package/dist/core/artifact-graph/instruction-loader.d.ts.map +1 -1
  82. package/dist/core/artifact-graph/instruction-loader.js +176 -110
  83. package/dist/core/artifact-graph/instruction-loader.js.map +1 -1
  84. package/dist/core/artifact-graph/outputs.d.ts +13 -6
  85. package/dist/core/artifact-graph/outputs.d.ts.map +1 -1
  86. package/dist/core/artifact-graph/outputs.js +146 -8
  87. package/dist/core/artifact-graph/outputs.js.map +1 -1
  88. package/dist/core/artifact-graph/resolver.d.ts +44 -63
  89. package/dist/core/artifact-graph/resolver.d.ts.map +1 -1
  90. package/dist/core/artifact-graph/resolver.js +85 -86
  91. package/dist/core/artifact-graph/resolver.js.map +1 -1
  92. package/dist/core/artifact-graph/schema.d.ts +0 -6
  93. package/dist/core/artifact-graph/schema.d.ts.map +1 -1
  94. package/dist/core/artifact-graph/schema.js +7 -32
  95. package/dist/core/artifact-graph/schema.js.map +1 -1
  96. package/dist/core/artifact-graph/state.d.ts +1 -8
  97. package/dist/core/artifact-graph/state.d.ts.map +1 -1
  98. package/dist/core/artifact-graph/state.js +2 -17
  99. package/dist/core/artifact-graph/state.js.map +1 -1
  100. package/dist/core/artifact-graph/stub-detection.d.ts +12 -0
  101. package/dist/core/artifact-graph/stub-detection.d.ts.map +1 -0
  102. package/dist/core/artifact-graph/stub-detection.js +39 -0
  103. package/dist/core/artifact-graph/stub-detection.js.map +1 -0
  104. package/dist/core/artifact-graph/types.d.ts +4 -0
  105. package/dist/core/artifact-graph/types.d.ts.map +1 -1
  106. package/dist/core/artifact-graph/types.js +30 -10
  107. package/dist/core/artifact-graph/types.js.map +1 -1
  108. package/dist/core/available-tools.d.ts +3 -12
  109. package/dist/core/available-tools.d.ts.map +1 -1
  110. package/dist/core/available-tools.js +4 -13
  111. package/dist/core/available-tools.js.map +1 -1
  112. package/dist/core/change-metadata/schema.d.ts +1 -1
  113. package/dist/core/change-metadata/schema.d.ts.map +1 -1
  114. package/dist/core/change-metadata/schema.js +10 -7
  115. package/dist/core/change-metadata/schema.js.map +1 -1
  116. package/dist/core/change-presenter.d.ts +23 -18
  117. package/dist/core/change-presenter.d.ts.map +1 -1
  118. package/dist/core/change-presenter.js +102 -43
  119. package/dist/core/change-presenter.js.map +1 -1
  120. package/dist/core/change-status-policy.d.ts +8 -1
  121. package/dist/core/change-status-policy.d.ts.map +1 -1
  122. package/dist/core/change-status-policy.js +25 -1
  123. package/dist/core/change-status-policy.js.map +1 -1
  124. package/dist/core/codex-metrics.d.ts +25 -45
  125. package/dist/core/codex-metrics.d.ts.map +1 -1
  126. package/dist/core/codex-metrics.js +44 -88
  127. package/dist/core/codex-metrics.js.map +1 -1
  128. package/dist/core/codex-residue.d.ts +22 -0
  129. package/dist/core/codex-residue.d.ts.map +1 -0
  130. package/dist/core/codex-residue.js +61 -0
  131. package/dist/core/codex-residue.js.map +1 -0
  132. package/dist/core/command-generation/adapters/claude.d.ts +2 -9
  133. package/dist/core/command-generation/adapters/claude.d.ts.map +1 -1
  134. package/dist/core/command-generation/adapters/claude.js +2 -12
  135. package/dist/core/command-generation/adapters/claude.js.map +1 -1
  136. package/dist/core/command-generation/adapters/index.d.ts +1 -9
  137. package/dist/core/command-generation/adapters/index.d.ts.map +1 -1
  138. package/dist/core/command-generation/adapters/index.js +1 -9
  139. package/dist/core/command-generation/adapters/index.js.map +1 -1
  140. package/dist/core/command-generation/generator.d.ts +0 -17
  141. package/dist/core/command-generation/generator.d.ts.map +1 -1
  142. package/dist/core/command-generation/generator.js +0 -17
  143. package/dist/core/command-generation/generator.js.map +1 -1
  144. package/dist/core/command-generation/index.d.ts +2 -5
  145. package/dist/core/command-generation/index.d.ts.map +1 -1
  146. package/dist/core/command-generation/index.js +0 -9
  147. package/dist/core/command-generation/index.js.map +1 -1
  148. package/dist/core/command-generation/types.d.ts +10 -36
  149. package/dist/core/command-generation/types.d.ts.map +1 -1
  150. package/dist/core/command-generation/types.js +0 -6
  151. package/dist/core/command-generation/types.js.map +1 -1
  152. package/dist/core/command-generation/yaml.d.ts +3 -18
  153. package/dist/core/command-generation/yaml.d.ts.map +1 -1
  154. package/dist/core/command-generation/yaml.js +5 -23
  155. package/dist/core/command-generation/yaml.js.map +1 -1
  156. package/dist/core/config-prompts.d.ts +2 -4
  157. package/dist/core/config-prompts.d.ts.map +1 -1
  158. package/dist/core/config-prompts.js +2 -7
  159. package/dist/core/config-prompts.js.map +1 -1
  160. package/dist/core/config-schema.d.ts +8 -53
  161. package/dist/core/config-schema.d.ts.map +1 -1
  162. package/dist/core/config-schema.js +49 -62
  163. package/dist/core/config-schema.js.map +1 -1
  164. package/dist/core/config.d.ts +56 -0
  165. package/dist/core/config.d.ts.map +1 -1
  166. package/dist/core/config.js +73 -2
  167. package/dist/core/config.js.map +1 -1
  168. package/dist/core/converters/json-converter.d.ts.map +1 -1
  169. package/dist/core/dashboard-activity.d.ts +7 -9
  170. package/dist/core/dashboard-activity.d.ts.map +1 -1
  171. package/dist/core/dashboard-activity.js +26 -24
  172. package/dist/core/dashboard-activity.js.map +1 -1
  173. package/dist/core/dashboard-data.d.ts +40 -22
  174. package/dist/core/dashboard-data.d.ts.map +1 -1
  175. package/dist/core/dashboard-data.js +84 -68
  176. package/dist/core/dashboard-data.js.map +1 -1
  177. package/dist/core/global-config.d.ts +24 -53
  178. package/dist/core/global-config.d.ts.map +1 -1
  179. package/dist/core/global-config.js +43 -62
  180. package/dist/core/global-config.js.map +1 -1
  181. package/dist/core/init.d.ts +29 -11
  182. package/dist/core/init.d.ts.map +1 -1
  183. package/dist/core/init.js +232 -164
  184. package/dist/core/init.js.map +1 -1
  185. package/dist/core/list.d.ts +1 -1
  186. package/dist/core/list.d.ts.map +1 -1
  187. package/dist/core/list.js +121 -28
  188. package/dist/core/list.js.map +1 -1
  189. package/dist/core/local-server.d.ts +63 -39
  190. package/dist/core/local-server.d.ts.map +1 -1
  191. package/dist/core/local-server.js +99 -53
  192. package/dist/core/local-server.js.map +1 -1
  193. package/dist/core/markdown-render.d.ts +25 -0
  194. package/dist/core/markdown-render.d.ts.map +1 -0
  195. package/dist/core/markdown-render.js +94 -0
  196. package/dist/core/markdown-render.js.map +1 -0
  197. package/dist/core/migrate.d.ts +32 -15
  198. package/dist/core/migrate.d.ts.map +1 -1
  199. package/dist/core/migrate.js +220 -108
  200. package/dist/core/migrate.js.map +1 -1
  201. package/dist/core/parsers/change-parser.d.ts +7 -10
  202. package/dist/core/parsers/change-parser.d.ts.map +1 -1
  203. package/dist/core/parsers/change-parser.js +48 -56
  204. package/dist/core/parsers/change-parser.js.map +1 -1
  205. package/dist/core/parsers/markdown-parser.d.ts +8 -9
  206. package/dist/core/parsers/markdown-parser.d.ts.map +1 -1
  207. package/dist/core/parsers/markdown-parser.js +31 -22
  208. package/dist/core/parsers/markdown-parser.js.map +1 -1
  209. package/dist/core/parsers/requirement-blocks.d.ts +53 -11
  210. package/dist/core/parsers/requirement-blocks.d.ts.map +1 -1
  211. package/dist/core/parsers/requirement-blocks.js +200 -60
  212. package/dist/core/parsers/requirement-blocks.js.map +1 -1
  213. package/dist/core/parsers/requirement-text.d.ts +73 -79
  214. package/dist/core/parsers/requirement-text.d.ts.map +1 -1
  215. package/dist/core/parsers/requirement-text.js +137 -79
  216. package/dist/core/parsers/requirement-text.js.map +1 -1
  217. package/dist/core/parsers/spec-structure.d.ts +1 -1
  218. package/dist/core/parsers/spec-structure.d.ts.map +1 -1
  219. package/dist/core/parsers/spec-structure.js +30 -3
  220. package/dist/core/parsers/spec-structure.js.map +1 -1
  221. package/dist/core/planning-home.js.map +1 -1
  222. package/dist/core/profiles.d.ts +3 -10
  223. package/dist/core/profiles.d.ts.map +1 -1
  224. package/dist/core/profiles.js +5 -12
  225. package/dist/core/profiles.js.map +1 -1
  226. package/dist/core/project-config.d.ts +43 -44
  227. package/dist/core/project-config.d.ts.map +1 -1
  228. package/dist/core/project-config.js +107 -82
  229. package/dist/core/project-config.js.map +1 -1
  230. package/dist/core/project-layout.d.ts +10 -18
  231. package/dist/core/project-layout.d.ts.map +1 -1
  232. package/dist/core/project-layout.js +16 -26
  233. package/dist/core/project-layout.js.map +1 -1
  234. package/dist/core/root-selection.d.ts +11 -7
  235. package/dist/core/root-selection.d.ts.map +1 -1
  236. package/dist/core/root-selection.js +7 -8
  237. package/dist/core/root-selection.js.map +1 -1
  238. package/dist/core/rules.d.ts +10 -0
  239. package/dist/core/rules.d.ts.map +1 -0
  240. package/dist/core/rules.js +43 -0
  241. package/dist/core/rules.js.map +1 -0
  242. package/dist/core/schema-names.d.ts +16 -0
  243. package/dist/core/schema-names.d.ts.map +1 -0
  244. package/dist/core/schema-names.js +16 -0
  245. package/dist/core/schema-names.js.map +1 -0
  246. package/dist/core/schemas/base.schema.d.ts +3 -0
  247. package/dist/core/schemas/base.schema.d.ts.map +1 -1
  248. package/dist/core/schemas/base.schema.js +22 -6
  249. package/dist/core/schemas/base.schema.js.map +1 -1
  250. package/dist/core/schemas/change.schema.d.ts +16 -0
  251. package/dist/core/schemas/change.schema.d.ts.map +1 -1
  252. package/dist/core/schemas/change.schema.js +41 -10
  253. package/dist/core/schemas/change.schema.js.map +1 -1
  254. package/dist/core/schemas/spec.schema.d.ts +2 -0
  255. package/dist/core/schemas/spec.schema.d.ts.map +1 -1
  256. package/dist/core/shared/index.d.ts +3 -8
  257. package/dist/core/shared/index.d.ts.map +1 -1
  258. package/dist/core/shared/index.js +3 -8
  259. package/dist/core/shared/index.js.map +1 -1
  260. package/dist/core/shared/rules-generation.d.ts +27 -8
  261. package/dist/core/shared/rules-generation.d.ts.map +1 -1
  262. package/dist/core/shared/rules-generation.js +151 -16
  263. package/dist/core/shared/rules-generation.js.map +1 -1
  264. package/dist/core/shared/skill-generation.d.ts +38 -53
  265. package/dist/core/shared/skill-generation.d.ts.map +1 -1
  266. package/dist/core/shared/skill-generation.js +82 -51
  267. package/dist/core/shared/skill-generation.js.map +1 -1
  268. package/dist/core/shared/tool-detection.d.ts +40 -62
  269. package/dist/core/shared/tool-detection.d.ts.map +1 -1
  270. package/dist/core/shared/tool-detection.js +88 -80
  271. package/dist/core/shared/tool-detection.js.map +1 -1
  272. package/dist/core/skill-metrics.d.ts +36 -63
  273. package/dist/core/skill-metrics.d.ts.map +1 -1
  274. package/dist/core/skill-metrics.js +34 -73
  275. package/dist/core/skill-metrics.js.map +1 -1
  276. package/dist/core/spec-presenter.d.ts.map +1 -1
  277. package/dist/core/spec-presenter.js +6 -6
  278. package/dist/core/spec-presenter.js.map +1 -1
  279. package/dist/core/specs-apply.d.ts +24 -23
  280. package/dist/core/specs-apply.d.ts.map +1 -1
  281. package/dist/core/specs-apply.js +188 -173
  282. package/dist/core/specs-apply.js.map +1 -1
  283. package/dist/core/templates/fragments/interview.d.ts +2 -6
  284. package/dist/core/templates/fragments/interview.d.ts.map +1 -1
  285. package/dist/core/templates/fragments/interview.js +2 -6
  286. package/dist/core/templates/fragments/interview.js.map +1 -1
  287. package/dist/core/templates/fragments/next-step.d.ts +4 -8
  288. package/dist/core/templates/fragments/next-step.d.ts.map +1 -1
  289. package/dist/core/templates/fragments/next-step.js +4 -8
  290. package/dist/core/templates/fragments/next-step.js.map +1 -1
  291. package/dist/core/templates/fragments/verify.d.ts +9 -12
  292. package/dist/core/templates/fragments/verify.d.ts.map +1 -1
  293. package/dist/core/templates/fragments/verify.js +9 -12
  294. package/dist/core/templates/fragments/verify.js.map +1 -1
  295. package/dist/core/templates/index.d.ts +0 -6
  296. package/dist/core/templates/index.d.ts.map +1 -1
  297. package/dist/core/templates/index.js +0 -7
  298. package/dist/core/templates/index.js.map +1 -1
  299. package/dist/core/templates/skill-templates.d.ts +1 -5
  300. package/dist/core/templates/skill-templates.d.ts.map +1 -1
  301. package/dist/core/templates/skill-templates.js +0 -5
  302. package/dist/core/templates/skill-templates.js.map +1 -1
  303. package/dist/core/templates/types.d.ts +3 -7
  304. package/dist/core/templates/types.d.ts.map +1 -1
  305. package/dist/core/templates/types.js +0 -3
  306. package/dist/core/templates/types.js.map +1 -1
  307. package/dist/core/templates/workflows/apply.d.ts +3 -9
  308. package/dist/core/templates/workflows/apply.d.ts.map +1 -1
  309. package/dist/core/templates/workflows/apply.js +9 -12
  310. package/dist/core/templates/workflows/apply.js.map +1 -1
  311. package/dist/core/templates/workflows/archive.d.ts +0 -6
  312. package/dist/core/templates/workflows/archive.d.ts.map +1 -1
  313. package/dist/core/templates/workflows/archive.js +7 -5
  314. package/dist/core/templates/workflows/archive.js.map +1 -1
  315. package/dist/core/templates/workflows/decision.js +4 -4
  316. package/dist/core/templates/workflows/decision.js.map +1 -1
  317. package/dist/core/templates/workflows/explore.js +1 -1
  318. package/dist/core/templates/workflows/grill.d.ts.map +1 -1
  319. package/dist/core/templates/workflows/grill.js +0 -2
  320. package/dist/core/templates/workflows/grill.js.map +1 -1
  321. package/dist/core/templates/workflows/issue.d.ts +0 -6
  322. package/dist/core/templates/workflows/issue.d.ts.map +1 -1
  323. package/dist/core/templates/workflows/issue.js +3 -0
  324. package/dist/core/templates/workflows/issue.js.map +1 -1
  325. package/dist/core/templates/workflows/propose.d.ts +0 -6
  326. package/dist/core/templates/workflows/propose.d.ts.map +1 -1
  327. package/dist/core/templates/workflows/propose.js +0 -1
  328. package/dist/core/templates/workflows/propose.js.map +1 -1
  329. package/dist/core/templates/workflows/sync.d.ts +2 -8
  330. package/dist/core/templates/workflows/sync.d.ts.map +1 -1
  331. package/dist/core/templates/workflows/sync.js +2 -2
  332. package/dist/core/templates/workflows/sync.js.map +1 -1
  333. package/dist/core/templates/workflows/update.d.ts +0 -6
  334. package/dist/core/templates/workflows/update.d.ts.map +1 -1
  335. package/dist/core/templates/workflows/update.js.map +1 -1
  336. package/dist/core/update.d.ts +26 -21
  337. package/dist/core/update.d.ts.map +1 -1
  338. package/dist/core/update.js +165 -116
  339. package/dist/core/update.js.map +1 -1
  340. package/dist/core/user-state-migration.d.ts +13 -15
  341. package/dist/core/user-state-migration.d.ts.map +1 -1
  342. package/dist/core/user-state-migration.js +16 -20
  343. package/dist/core/user-state-migration.js.map +1 -1
  344. package/dist/core/validation/constants.d.ts +19 -25
  345. package/dist/core/validation/constants.d.ts.map +1 -1
  346. package/dist/core/validation/constants.js +25 -20
  347. package/dist/core/validation/constants.js.map +1 -1
  348. package/dist/core/validation/prose-length.d.ts +15 -0
  349. package/dist/core/validation/prose-length.d.ts.map +1 -0
  350. package/dist/core/validation/prose-length.js +29 -0
  351. package/dist/core/validation/prose-length.js.map +1 -0
  352. package/dist/core/validation/purpose-placeholder.d.ts +9 -16
  353. package/dist/core/validation/purpose-placeholder.d.ts.map +1 -1
  354. package/dist/core/validation/purpose-placeholder.js +30 -44
  355. package/dist/core/validation/purpose-placeholder.js.map +1 -1
  356. package/dist/core/validation/section-validator.d.ts +4 -4
  357. package/dist/core/validation/section-validator.d.ts.map +1 -1
  358. package/dist/core/validation/section-validator.js +43 -7
  359. package/dist/core/validation/section-validator.js.map +1 -1
  360. package/dist/core/validation/task-numbering.d.ts +6 -3
  361. package/dist/core/validation/task-numbering.d.ts.map +1 -1
  362. package/dist/core/validation/task-numbering.js +23 -11
  363. package/dist/core/validation/task-numbering.js.map +1 -1
  364. package/dist/core/validation/types.d.ts +18 -0
  365. package/dist/core/validation/types.d.ts.map +1 -1
  366. package/dist/core/validation/types.js +12 -1
  367. package/dist/core/validation/types.js.map +1 -1
  368. package/dist/core/validation/validator.d.ts +50 -51
  369. package/dist/core/validation/validator.d.ts.map +1 -1
  370. package/dist/core/validation/validator.js +486 -263
  371. package/dist/core/validation/validator.js.map +1 -1
  372. package/dist/prompts/searchable-multi-select.d.ts +3 -8
  373. package/dist/prompts/searchable-multi-select.d.ts.map +1 -1
  374. package/dist/prompts/searchable-multi-select.js +16 -39
  375. package/dist/prompts/searchable-multi-select.js.map +1 -1
  376. package/dist/utils/change-metadata.d.ts +11 -50
  377. package/dist/utils/change-metadata.d.ts.map +1 -1
  378. package/dist/utils/change-metadata.js +48 -67
  379. package/dist/utils/change-metadata.js.map +1 -1
  380. package/dist/utils/change-utils.d.ts +31 -54
  381. package/dist/utils/change-utils.d.ts.map +1 -1
  382. package/dist/utils/change-utils.js +143 -100
  383. package/dist/utils/change-utils.js.map +1 -1
  384. package/dist/utils/file-lock.d.ts +39 -0
  385. package/dist/utils/file-lock.d.ts.map +1 -0
  386. package/dist/utils/file-lock.js +149 -0
  387. package/dist/utils/file-lock.js.map +1 -0
  388. package/dist/utils/file-system.d.ts +12 -32
  389. package/dist/utils/file-system.d.ts.map +1 -1
  390. package/dist/utils/file-system.js +16 -40
  391. package/dist/utils/file-system.js.map +1 -1
  392. package/dist/utils/frontmatter.d.ts +7 -11
  393. package/dist/utils/frontmatter.d.ts.map +1 -1
  394. package/dist/utils/frontmatter.js +11 -11
  395. package/dist/utils/frontmatter.js.map +1 -1
  396. package/dist/utils/interactive.d.ts +4 -9
  397. package/dist/utils/interactive.d.ts.map +1 -1
  398. package/dist/utils/interactive.js +2 -4
  399. package/dist/utils/interactive.js.map +1 -1
  400. package/dist/utils/item-discovery.d.ts +15 -10
  401. package/dist/utils/item-discovery.d.ts.map +1 -1
  402. package/dist/utils/item-discovery.js +42 -47
  403. package/dist/utils/item-discovery.js.map +1 -1
  404. package/dist/utils/link.d.ts +13 -4
  405. package/dist/utils/link.d.ts.map +1 -1
  406. package/dist/utils/link.js +13 -4
  407. package/dist/utils/link.js.map +1 -1
  408. package/dist/utils/match.js.map +1 -1
  409. package/dist/utils/requirement-diff.d.ts +13 -23
  410. package/dist/utils/requirement-diff.d.ts.map +1 -1
  411. package/dist/utils/requirement-diff.js +13 -23
  412. package/dist/utils/requirement-diff.js.map +1 -1
  413. package/dist/utils/spec-files.d.ts +10 -11
  414. package/dist/utils/spec-files.d.ts.map +1 -1
  415. package/dist/utils/spec-files.js +31 -22
  416. package/dist/utils/spec-files.js.map +1 -1
  417. package/dist/utils/task-progress.d.ts +11 -9
  418. package/dist/utils/task-progress.d.ts.map +1 -1
  419. package/dist/utils/task-progress.js +53 -32
  420. package/dist/utils/task-progress.js.map +1 -1
  421. package/dist/utils/timestamp.d.ts +5 -8
  422. package/dist/utils/timestamp.d.ts.map +1 -1
  423. package/dist/utils/timestamp.js +5 -8
  424. package/dist/utils/timestamp.js.map +1 -1
  425. package/package.json +9 -10
  426. package/schemas/decision/templates/decision.md +3 -1
  427. package/schemas/decision/templates/index.md +2 -2
  428. package/schemas/issue/schema.yaml +11 -2
  429. package/schemas/issue/templates/spec.md +37 -3
  430. package/schemas/sdd/schema.yaml +24 -1
  431. package/schemas/sdd/templates/spec.md +37 -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';
@@ -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,246 @@ function ensureIndex(decisionsDir, projectRoot) {
107
128
  }
108
129
  return indexPath;
109
130
  }
110
- // ponytail: append-only, no dedup — re-running `new` for the same topic adds a
111
- // second row. Dedup by topic if the ledger ever grows noisy enough to matter.
112
- // `file` is the sibling filename (index.md lives in the same decisions dir), so
113
- // a relative markdown link resolves straight to the decision doc.
114
- 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) {
115
135
  const fileLink = `[${indexCell(file)}](${file})`;
116
- 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 };
117
348
  }
118
- // -----------------------------------------------------------------------------
119
- // Command implementations (exported for tests)
120
- // -----------------------------------------------------------------------------
121
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
+ });
122
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;
123
368
  if (!topic)
124
369
  throw new Error('Missing required argument <topic>');
125
- const nameValidation = validateChangeName(topic);
370
+ const nameValidation = validateChangeName(topic, 'Topic');
126
371
  if (!nameValidation.valid) {
127
372
  throw new Error(`Invalid topic '${topic}': ${nameValidation.error}`);
128
373
  }
@@ -134,28 +379,43 @@ export async function decisionNewCommand(topic, options) {
134
379
  if (!TIMESTAMP_RE.test(stamp)) {
135
380
  throw new Error(`Invalid --date '${stamp}'. Expected project timestamp format: yyyyMMdd_HHmmss`);
136
381
  }
137
- const root = await resolveRootForCommand(options, {
138
- json: options.json,
139
- failurePayload: { decision: null },
140
- });
141
- if (!root)
142
- return;
382
+ if (!isRealTimestamp(stamp)) {
383
+ throw new Error(`Invalid --date '${stamp}'. That is not a real date and time.`);
384
+ }
143
385
  const decisionsDir = path.join(root.path, DECISIONS_SUBDIR);
144
386
  const fileName = `${stamp}-${topic}.md`;
145
387
  const filePath = path.join(decisionsDir, fileName);
146
- if (fs.existsSync(filePath) && !options.force) {
147
- throw new Error(`Decision already exists: ${filePath} (use --force to overwrite)`);
148
- }
149
388
  const title = options.title ?? topic;
150
389
  const summary = options.summary ?? title;
151
390
  const template = loadTemplate(SCHEMA_NAME, 'decision.md', root.path);
152
391
  const content = renderDecision(template, title, stamp, status);
153
- // CLI produces both files: the dated decision file from its template, and
154
- // 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);
155
398
  fs.mkdirSync(decisionsDir, { recursive: true });
156
- fs.writeFileSync(filePath, content, 'utf-8');
399
+ if (options.force) {
400
+ fs.writeFileSync(filePath, content, 'utf-8');
401
+ }
402
+ else {
403
+ // 'wx' makes the create atomic, closing the check-then-write window a
404
+ // separate existsSync would leave open.
405
+ try {
406
+ fs.writeFileSync(filePath, content, { encoding: 'utf-8', flag: 'wx' });
407
+ }
408
+ catch (error) {
409
+ if (error.code === 'EEXIST') {
410
+ throw new Error(`Decision already exists: ${filePath} (use --force to overwrite)`);
411
+ }
412
+ throw error;
413
+ }
414
+ }
157
415
  const indexPath = ensureIndex(decisionsDir, root.path);
158
- appendIndexRow(indexPath, stampToHuman(stamp), title, summary, fileName);
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);
159
419
  const payload = {
160
420
  decision: { topic, title, date: stamp, status, summary, path: filePath, indexPath },
161
421
  root: toRootOutput(root),
@@ -171,7 +431,7 @@ export async function decisionNewCommand(topic, options) {
171
431
  }
172
432
  catch (error) {
173
433
  if (options.json) {
174
- emitFailure(true, { decision: null }, error, 'decision_error');
434
+ emitFailure(true, failurePayload(), error, 'decision_error');
175
435
  return;
176
436
  }
177
437
  throw error;
@@ -179,14 +439,36 @@ export async function decisionNewCommand(topic, options) {
179
439
  }
180
440
  export async function decisionListCommand(options) {
181
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.
182
445
  const root = await resolveRootForCommand(options, {
183
446
  json: options.json,
184
- failurePayload: { decisions: null },
447
+ allowImplicitRoot: false,
448
+ failurePayload: DECISION_LIST_FAILURE_PAYLOAD,
185
449
  });
186
450
  if (!root)
187
451
  return;
188
452
  const decisionsDir = path.join(root.path, DECISIONS_SUBDIR);
189
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
+ }
190
472
  if (options.status) {
191
473
  entries = entries.filter((e) => e.status === options.status);
192
474
  }
@@ -195,39 +477,85 @@ export async function decisionListCommand(options) {
195
477
  entries.sort((a, b) => a.topic.localeCompare(b.topic));
196
478
  }
197
479
  else {
198
- // date desc (newest first); tie-break by topic for stability
480
+ // Newest first; tie-break by topic for stability.
199
481
  entries.sort((a, b) => b.date.localeCompare(a.date) || a.topic.localeCompare(b.topic));
200
482
  }
201
483
  if (options.json) {
202
- 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));
203
502
  return;
204
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
+ }
205
535
  if (entries.length === 0) {
206
- 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>');
207
539
  return;
208
540
  }
209
- console.log('Date | Status | Title');
210
- 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)}-|------`);
211
547
  for (const e of entries) {
212
- console.log(`${e.date} | ${e.status.padEnd(10)} | ${e.title}`);
548
+ console.log(`${e.date.padEnd(dateWidth)} | ${e.status.padEnd(statusWidth)} | ${e.title}`);
213
549
  }
214
550
  }
215
551
  catch (error) {
216
552
  if (options.json) {
217
- emitFailure(true, { decisions: null }, error, 'decision_error');
553
+ emitFailure(true, DECISION_LIST_FAILURE_PAYLOAD, error, 'decision_error');
218
554
  return;
219
555
  }
220
556
  throw error;
221
557
  }
222
558
  }
223
- // -----------------------------------------------------------------------------
224
- // Registration
225
- // -----------------------------------------------------------------------------
226
- /**
227
- * Register the `decision` command group and its subcommands.
228
- *
229
- * @param program - The Commander program instance
230
- */
231
559
  export function registerDecisionCommand(program) {
232
560
  const decisionCmd = program
233
561
  .command('decision')
@@ -246,8 +574,10 @@ export function registerDecisionCommand(program) {
246
574
  await decisionNewCommand(topic, options);
247
575
  }
248
576
  catch (error) {
249
- console.error(error instanceof Error ? error.message : String(error));
250
- 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');
251
581
  }
252
582
  });
253
583
  decisionCmd
@@ -255,14 +585,14 @@ export function registerDecisionCommand(program) {
255
585
  .description('List decisions as an index table')
256
586
  .addOption(new Option('--status <status>', 'Filter by status').choices(DECISION_STATUSES))
257
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")
258
589
  .option('--json', 'Output as JSON')
259
590
  .action(async (options) => {
260
591
  try {
261
592
  await decisionListCommand(options);
262
593
  }
263
594
  catch (error) {
264
- console.error(error instanceof Error ? error.message : String(error));
265
- process.exitCode = 1;
595
+ emitFailure(false, {}, error, 'decision_error');
266
596
  }
267
597
  });
268
598
  }