@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,9 +1,11 @@
1
1
  import path from 'path';
2
2
  import { promises as fs } from 'fs';
3
3
  import { Validator } from '../core/validation/validator.js';
4
+ import { issueSymbol } from '../core/validation/types.js';
4
5
  import { validateSections } from '../core/validation/section-validator.js';
5
6
  import { resolveSchema, getSchemaDir } from '../core/artifact-graph/resolver.js';
6
7
  import { resolveArtifactOutputs } from '../core/artifact-graph/outputs.js';
8
+ import { isStubArtifactFile } from '../core/artifact-graph/stub-detection.js';
7
9
  import { METADATA_FILENAME, readSkipSpecsMarker, resolveSchemaForChange, } from '../utils/change-metadata.js';
8
10
  import { FileSystemUtils } from '../utils/file-system.js';
9
11
  import { findSpecFiles } from '../utils/spec-files.js';
@@ -15,44 +17,52 @@ import { getAvailableChanges } from './workflow/shared.js';
15
17
  import { nearestMatches } from '../utils/match.js';
16
18
  import { resolveTaskFiles } from '../utils/task-progress.js';
17
19
  import { findTaskNumberingIssues, } from '../core/validation/task-numbering.js';
20
+ /**
21
+ * The one null-shape every `validate --json` failure carries, so a caller
22
+ * reading `result.summary.totals` never gets `undefined` from one failure path
23
+ * and `null` from another.
24
+ */
25
+ /**
26
+ * The `--json` null-shape, `root` included: a root-resolution failure has none,
27
+ * and the sites that do resolve one spread their own over it.
28
+ */
29
+ export const VALIDATE_FAILURE_PAYLOAD = { items: null, summary: null, root: null };
18
30
  export class ValidateCommand {
19
31
  async execute(itemName, options = {}) {
20
- // A bulk run is the one shape that can succeed vacuously: with an implicit
21
- // root it inspects a directory that is not a project, finds nothing, and
22
- // reports "0 items, 0 failed" with exit 0 — a diagnostic command answering
23
- // "everything is fine" about a place it never checked. Refuse the implicit
24
- // root there. Named-item validation keeps it: that path fails loudly on its
25
- // own when the item cannot be found, so it cannot pass vacuously.
32
+ // A bulk run is the one shape that can succeed vacuously: on an implicit
33
+ // root that is not a project it would report "0 items, 0 failed" with exit 0
34
+ // about a place it never checked. Named-item validation keeps the implicit
35
+ // root; it fails loudly instead.
26
36
  const isBulk = !!(options.all || options.changes || options.specs);
27
37
  const root = await resolveRootForCommand(options, {
28
38
  json: options.json,
29
- ...(isBulk
30
- ? { allowImplicitRoot: false, failurePayload: { items: null, summary: null } }
31
- : {}),
39
+ failurePayload: VALIDATE_FAILURE_PAYLOAD,
40
+ ...(isBulk ? { allowImplicitRoot: false } : {}),
32
41
  });
33
42
  if (!root) {
34
43
  return;
35
44
  }
36
45
  const interactive = isInteractive(options);
37
- // Handle bulk flags first
38
46
  if (isBulk) {
47
+ // Stderr, as `show` and `list` do for a flag their mode ignores: silently
48
+ // validating everything reads as a verdict on the one item named.
49
+ if (itemName) {
50
+ console.error(`Warning: Ignoring item '${itemName}'; --all/--changes/--specs validate every item.`);
51
+ }
39
52
  await this.runBulkValidation(root, {
40
53
  changes: !!options.all || !!options.changes,
41
54
  specs: !!options.all || !!options.specs,
42
55
  }, { strict: !!options.strict, json: !!options.json, concurrency: options.concurrency, noInteractive: resolveNoInteractive(options) });
43
56
  return;
44
57
  }
45
- // No item and no flags
46
58
  if (!itemName) {
47
59
  if (interactive) {
48
60
  await this.runInteractiveSelector(root, { strict: !!options.strict, json: !!options.json, concurrency: options.concurrency });
49
61
  return;
50
62
  }
51
- this.printNonInteractiveHint(root);
52
- process.exitCode = 1;
63
+ this.reportNothingToValidate(root, !!options.json);
53
64
  return;
54
65
  }
55
- // Direct item validation with type detection or override
56
66
  const typeOverride = this.normalizeType(options.type);
57
67
  await this.validateDirectItem(root, itemName, { typeOverride, strict: !!options.strict, json: !!options.json });
58
68
  }
@@ -65,11 +75,9 @@ export class ValidateCommand {
65
75
  return undefined;
66
76
  }
67
77
  /**
68
- * Resolve change IDs by directory existence within the resolved root — the
69
- * same rule `tospec status`/`instructions` use (`getAvailableChanges`) —
70
- * rather than requiring `proposal.md`. This lets `validate` resolve a
71
- * scaffolded or still-authoring change that the sibling commands already
72
- * resolve (#1182). Sorted to preserve the prior `getActiveChangeIds` ordering.
78
+ * Change IDs by directory existence — the same rule `status` and
79
+ * `instructions` use — rather than by `proposal.md`, so a scaffolded change
80
+ * resolves here too.
73
81
  */
74
82
  async listChangeIds(root) {
75
83
  const ids = await getAvailableChanges(root.path, root.changesDir);
@@ -92,7 +100,6 @@ export class ValidateCommand {
92
100
  return this.runBulkValidation(root, { changes: true, specs: false }, opts);
93
101
  if (choice === 'specs')
94
102
  return this.runBulkValidation(root, { changes: false, specs: true }, opts);
95
- // one
96
103
  const [changes, specs] = await Promise.all([this.listChangeIds(root), getSpecIds(root.path)]);
97
104
  const items = [];
98
105
  items.push(...changes.map(id => ({ name: `change/${id}`, value: { type: 'change', id } })));
@@ -105,26 +112,70 @@ export class ValidateCommand {
105
112
  const picked = await select({ message: 'Pick an item', choices: items });
106
113
  await this.validateByType(root, picked.type, picked.id, opts);
107
114
  }
108
- printNonInteractiveHint(root) {
115
+ /**
116
+ * Nothing named and no terminal to ask in. `no_item_specified` is the code
117
+ * `show` uses for the same situation.
118
+ */
119
+ reportNothingToValidate(root, json) {
120
+ const fix = [
121
+ 'tospec validate --all',
122
+ 'tospec validate --changes',
123
+ 'tospec validate --specs',
124
+ 'tospec validate <item-name>',
125
+ ];
126
+ if (json) {
127
+ emitFailureStatus({ ...VALIDATE_FAILURE_PAYLOAD, root: toRootOutput(root) }, {
128
+ severity: 'error',
129
+ code: 'no_item_specified',
130
+ message: 'Nothing to validate.',
131
+ fix: fix.join('\n'),
132
+ });
133
+ return;
134
+ }
109
135
  console.error('Nothing to validate. Try one of:');
110
- console.error(` ${'tospec validate --all'}`);
111
- console.error(` ${'tospec validate --changes'}`);
112
- console.error(` ${'tospec validate --specs'}`);
113
- console.error(` ${'tospec validate <item-name>'}`);
136
+ for (const line of fix)
137
+ console.error(` ${line}`);
114
138
  console.error('Or run in an interactive terminal.');
139
+ process.exitCode = 1;
115
140
  }
116
141
  async validateDirectItem(root, itemName, opts) {
117
142
  const [changes, specs] = await Promise.all([this.listChangeIds(root), getSpecIds(root.path)]);
118
143
  const isChange = changes.includes(itemName);
119
144
  const isSpec = specs.includes(itemName);
120
- const type = opts.typeOverride ?? (isChange ? 'change' : isSpec ? 'spec' : undefined);
145
+ let type = opts.typeOverride;
121
146
  if (!type) {
122
- const suggestions = nearestMatches(itemName, [...changes, ...specs]);
147
+ if (isChange)
148
+ type = 'change';
149
+ else if (isSpec)
150
+ type = 'spec';
151
+ }
152
+ // An explicit --type names a kind the item is not: say that, rather than
153
+ // letting the read fail with the ENOENT of a path the caller never typed.
154
+ if (opts.typeOverride && !(opts.typeOverride === 'change' ? isChange : isSpec)) {
155
+ const location = opts.typeOverride === 'change'
156
+ ? path.join(root.changesDir, itemName)
157
+ : path.join(root.specsDir, itemName, 'spec.md');
158
+ const otherKind = opts.typeOverride === 'change' ? (isSpec ? 'spec' : undefined) : isChange ? 'change' : undefined;
159
+ const message = `${opts.typeOverride === 'change' ? 'Change' : 'Spec'} '${itemName}' not found at ${location}.` +
160
+ (otherKind ? ` A ${otherKind} with that name exists: pass --type ${otherKind}.` : '');
161
+ if (opts.json) {
162
+ emitFailureStatus({ ...VALIDATE_FAILURE_PAYLOAD, root: toRootOutput(root) }, { severity: 'error', code: 'unknown_item', message });
163
+ }
164
+ else {
165
+ console.error(message);
166
+ process.exitCode = 1;
167
+ }
168
+ return;
169
+ }
170
+ if (!type) {
171
+ // Deduplicated: a name that is both a change and a spec would otherwise
172
+ // be suggested twice.
173
+ const suggestions = nearestMatches(itemName, [...new Set([...changes, ...specs])]);
123
174
  const message = suggestions.length
124
175
  ? `Unknown item '${itemName}'. Did you mean: ${suggestions.join(', ')}?`
125
176
  : `Unknown item '${itemName}'.`;
126
177
  if (opts.json) {
127
- emitFailureStatus({ items: null, root: toRootOutput(root) }, { severity: 'error', code: 'unknown_item', message });
178
+ emitFailureStatus({ ...VALIDATE_FAILURE_PAYLOAD, root: toRootOutput(root) }, { severity: 'error', code: 'unknown_item', message });
128
179
  }
129
180
  else {
130
181
  console.error(message);
@@ -134,7 +185,7 @@ export class ValidateCommand {
134
185
  }
135
186
  if (!opts.typeOverride && isChange && isSpec) {
136
187
  if (opts.json) {
137
- emitFailureStatus({ items: null, root: toRootOutput(root) }, {
188
+ emitFailureStatus({ ...VALIDATE_FAILURE_PAYLOAD, root: toRootOutput(root) }, {
138
189
  severity: 'error',
139
190
  code: 'ambiguous_item',
140
191
  message: `Ambiguous item '${itemName}' matches both a change and a spec.`,
@@ -156,8 +207,7 @@ export class ValidateCommand {
156
207
  const start = Date.now();
157
208
  const report = await validateChangeArtifacts(root.path, changeDir, { strict: opts.strict });
158
209
  const durationMs = Date.now() - start;
159
- this.printReport('change', id, report, durationMs, opts.json, root);
160
- // Non-zero exit if invalid (keeps enriched output test semantics)
210
+ this.printReport('change', id, report, durationMs, opts.json, root, opts.strict);
161
211
  process.exitCode = report.valid ? 0 : 1;
162
212
  return;
163
213
  }
@@ -165,13 +215,22 @@ export class ValidateCommand {
165
215
  const start = Date.now();
166
216
  const report = await validator.validateSpec(file);
167
217
  const durationMs = Date.now() - start;
168
- this.printReport('spec', id, report, durationMs, opts.json, root);
218
+ this.printReport('spec', id, report, durationMs, opts.json, root, opts.strict);
169
219
  process.exitCode = report.valid ? 0 : 1;
170
220
  }
171
- printReport(type, id, report, durationMs, json, root) {
221
+ printReport(type, id, report, durationMs, json, root, strict = false) {
172
222
  if (json) {
173
223
  emitSuccess({
174
- items: [{ id, type, valid: report.valid, issues: report.issues, durationMs }],
224
+ items: [
225
+ {
226
+ id,
227
+ type,
228
+ valid: report.valid,
229
+ issues: report.issues,
230
+ ...(report.notes?.length ? { notes: report.notes } : {}),
231
+ durationMs,
232
+ },
233
+ ],
175
234
  summary: { totals: { items: 1, passed: report.valid ? 1 : 0, failed: report.valid ? 0 : 1 }, byType: { [type]: { items: 1, passed: report.valid ? 1 : 0, failed: report.valid ? 0 : 1 } } },
176
235
  }, toRootOutput(root));
177
236
  return;
@@ -182,51 +241,65 @@ export class ValidateCommand {
182
241
  else {
183
242
  console.error(`${type === 'change' ? 'Change' : 'Specification'} '${id}' has issues`);
184
243
  }
185
- // Outside the verdict branch on purpose. Three checks are deliberately
186
- // WARNINGs so the run keeps passing while the author still learns about them
187
- // (missing normative keyword, placeholder Purpose, ambiguous task numbering).
188
- // Printing only on failure made every one of them invisible outside --json,
189
- // which implements the downgrade as a deletion.
190
- //
191
- // Routed with the verdict, though: on stderr they make a passing run look
192
- // failed to anything that reads the stream as an error channel — a hook, a
193
- // CI step, an agent — which is the same deletion by a different route, this
194
- // time with a false alarm attached. Exit 0 leaves stderr empty.
244
+ // Printed whatever the verdict: several checks are WARNINGs so a run keeps
245
+ // passing while the author still learns about them. Routed *with* the
246
+ // verdict, so a passing run leaves stderr empty for hooks and CI.
195
247
  const writeIssue = report.valid ? console.log : console.error;
196
248
  for (const issue of report.issues) {
197
249
  writeIssue(formatIssueLine(issue));
198
250
  }
199
- // Still failure-only: a passing item has no next step to take.
251
+ // Once each: a note explains the rule, which is the same however many lines
252
+ // tripped it.
253
+ for (const note of report.notes ?? []) {
254
+ writeIssue(`Note: ${note}`);
255
+ }
256
+ // Failure-only: a passing item has no next step to take.
200
257
  if (!report.valid) {
201
- this.printNextSteps(type, id, root, report.issues);
258
+ this.printNextSteps(type, id, root, report.issues, strict);
202
259
  }
203
260
  }
204
261
  /**
205
- * Derives hints from what actually failed rather than the item type alone —
206
- * every failing change used to get the same three delta hints, so a change
207
- * whose only problem was e.g. a missing design.md section was told to check
208
- * its delta headers instead.
262
+ * Hints derived from what actually failed, not from the item type, so a
263
+ * change whose only problem is a missing design.md section is not told to
264
+ * check delta headers.
209
265
  *
210
- * These three hints are about delta *content* (headers, scenarios, parsing),
211
- * so they apply to an issue reported against a delta file itself: the delta
212
- * validator reports those under `<capability>/spec.md`, relative to the
213
- * change's specs/ dir. Three neighbouring shapes deliberately do not qualify,
214
- * and the leading slash is what excludes the first two:
215
- * - a misnamed file in a capability folder (`auth/extra.md`) and a delta at
216
- * the specs/ root (`spec.md`) are placement errors; each message already
217
- * names the exact path to rename to, so the generic bullets would be
218
- * advice for a different failure;
219
- * - the whole-change `file` sentinel ("No deltas found") already embeds
220
- * these same three tips via `enrichTopLevelError`.
266
+ * The delta hints are about delta *content*, so they match only an issue
267
+ * reported under `<capability>/spec.md`. The leading slash excludes a misnamed
268
+ * file and a delta at the specs/ root — placement errors whose messages
269
+ * already name the path to rename to — and the whole-change `file` sentinel,
270
+ * which `enrichTopLevelError` already covers.
221
271
  */
222
- printNextSteps(type, id, root, issues) {
272
+ printNextSteps(type, id, root, issues, strict) {
223
273
  const bullets = [];
224
274
  if (type === 'change') {
225
- const hasDeltaIssue = issues.some((issue) => issue.path.endsWith('/spec.md'));
226
- if (hasDeltaIssue) {
275
+ // Each delta hint answers one failure. A missing Migration field or a
276
+ // SHALL/MUST warning under --strict is a delta issue too, and used to be
277
+ // told to add a Scenario block it already had.
278
+ const deltaIssues = issues.filter((issue) => issue.path.endsWith('/spec.md'));
279
+ const mentions = (pattern) => deltaIssues.some((issue) => pattern.test(issue.message));
280
+ if (mentions(/delta header|No deltas found|not a delta|## ADDED/i)) {
227
281
  bullets.push('- Ensure change has deltas in specs/: use headers ## ADDED/MODIFIED/REMOVED/RENAMED Requirements');
282
+ }
283
+ if (mentions(/#### Scenario:/)) {
228
284
  bullets.push('- Each requirement MUST include at least one #### Scenario: block');
229
- bullets.push(`- Debug parsed deltas: ${`tospec show ${id} --json --deltas-only`}`);
285
+ }
286
+ if (mentions(/omits scenario/)) {
287
+ bullets.push('- Restate every scenario the current spec still has in the MODIFIED block, or declare the removal under ## REMOVED Scenarios (- Requirement: `<name>` / - Scenario: `<name>` / - Reason: ...)');
288
+ }
289
+ if (mentions(/Migration field/)) {
290
+ bullets.push('- Add a **Migration** field to each REMOVED requirement');
291
+ }
292
+ if (mentions(/SHALL or MUST/)) {
293
+ // The keyword check is English-only, and the way out for a non-English
294
+ // spec is to drop --strict — advice that only makes sense when it is on.
295
+ bullets.push(strict
296
+ ? '- State the obligation with SHALL or MUST in the requirement body (or run without --strict for non-English specs)'
297
+ : '- State the obligation with SHALL or MUST in the requirement body');
298
+ }
299
+ // Only a delta issue that failed the run: a WARNING on an otherwise valid
300
+ // delta (a missing Purpose, say) is not something `show --json` explains.
301
+ if (deltaIssues.some((issue) => issue.level === 'ERROR' || strict)) {
302
+ bullets.push(`- Debug parsed deltas: ${`tospec show ${id} --json`}`);
230
303
  }
231
304
  const hasMissingSection = issues.some((issue) => issue.message.startsWith('Missing required section'));
232
305
  if (hasMissingSection) {
@@ -238,8 +311,7 @@ export class ValidateCommand {
238
311
  bullets.push('- Each requirement MUST include at least one #### Scenario: block');
239
312
  bullets.push('- Re-run with --json to see structured report');
240
313
  }
241
- // No specific hint applies: printing the old generic three bullets would
242
- // be advice for a different failure, so print nothing instead.
314
+ // Nothing applies: generic bullets would be advice for a different failure.
243
315
  if (bullets.length === 0)
244
316
  return;
245
317
  console.error('Next steps:');
@@ -255,7 +327,7 @@ export class ValidateCommand {
255
327
  const concurrency = normalizeConcurrency(opts.concurrency) ?? normalizeConcurrency(process.env.TOSPEC_CONCURRENCY) ?? DEFAULT_CONCURRENCY;
256
328
  const validator = new Validator(opts.strict);
257
329
  // Each entry carries its own id/type so a rejected task can be reported
258
- // without re-deriving them from its queue position.
330
+ // without re-deriving them from its position.
259
331
  const queue = [];
260
332
  for (const id of changeIds) {
261
333
  queue.push({
@@ -266,7 +338,7 @@ export class ValidateCommand {
266
338
  const changeDir = path.join(root.changesDir, id);
267
339
  const report = await validateChangeArtifacts(root.path, changeDir, { strict: opts.strict });
268
340
  const durationMs = Date.now() - start;
269
- return { id, type: 'change', valid: report.valid, issues: report.issues, durationMs };
341
+ return { id, type: 'change', valid: report.valid, issues: report.issues, notes: report.notes, durationMs };
270
342
  },
271
343
  });
272
344
  }
@@ -279,7 +351,7 @@ export class ValidateCommand {
279
351
  const file = path.join(root.specsDir, id, 'spec.md');
280
352
  const report = await validator.validateSpec(file);
281
353
  const durationMs = Date.now() - start;
282
- return { id, type: 'spec', valid: report.valid, issues: report.issues, durationMs };
354
+ return { id, type: 'spec', valid: report.valid, issues: report.issues, notes: report.notes, durationMs };
283
355
  },
284
356
  });
285
357
  }
@@ -338,8 +410,8 @@ export class ValidateCommand {
338
410
  next();
339
411
  });
340
412
  results.sort((a, b) => a.id.localeCompare(b.id));
341
- // Derived from results rather than counted alongside them: two tallies that
342
- // must agree is one more thing to keep in sync.
413
+ // Derived from results rather than counted alongside them, so the two
414
+ // cannot disagree.
343
415
  const passed = results.filter((res) => res.valid).length;
344
416
  const failed = results.length - passed;
345
417
  const summary = {
@@ -349,8 +421,11 @@ export class ValidateCommand {
349
421
  ...(scope.specs ? { spec: summarizeType(results, 'spec') } : {}),
350
422
  },
351
423
  };
424
+ // One copy per run: a sweep of forty specs trips the same rule forty times.
425
+ const runNotes = [...new Set(results.flatMap((res) => res.notes ?? []))];
426
+ const items = results.map(({ notes: _notes, ...res }) => res);
352
427
  if (opts.json) {
353
- emitSuccess({ items: results, summary }, toRootOutput(root));
428
+ emitSuccess({ items, summary, ...(runNotes.length ? { notes: runNotes } : {}) }, toRootOutput(root));
354
429
  }
355
430
  else {
356
431
  for (const res of results) {
@@ -358,31 +433,35 @@ export class ValidateCommand {
358
433
  console.log(`✓ ${res.type}/${res.id}`);
359
434
  else
360
435
  console.error(`✗ ${res.type}/${res.id}`);
361
- // Indented under the item that produced them: a bulk run printing one
362
- // tick per line leaves a finding on a *passing* item with no subject,
363
- // and the `Details:` hint below only ever names a failure. Each item's
364
- // findings follow its own verdict onto the same stream, so a run where
365
- // everything passes writes nothing to stderr.
436
+ // Routed with the item's own verdict, so a run where everything passes
437
+ // writes nothing to stderr.
366
438
  const writeIssue = res.valid ? console.log : console.error;
367
439
  for (const issue of res.issues) {
368
440
  writeIssue(formatIssueLine(issue, ' '));
369
441
  }
370
442
  }
443
+ for (const note of runNotes) {
444
+ console.log(`Note: ${note}`);
445
+ }
371
446
  console.log(`Totals: ${summary.totals.passed} passed, ${summary.totals.failed} failed (${summary.totals.items} items)`);
372
- const firstFailure = results.find((res) => !res.valid);
373
- if (firstFailure) {
374
- console.log(`Details: tospec validate ${firstFailure.id} --type ${firstFailure.type}`);
447
+ // One line per failed item: naming only the first left the rest without
448
+ // a command to rerun.
449
+ const failures = results.filter((res) => !res.valid);
450
+ if (failures.length > 0) {
451
+ console.log('Details:');
452
+ for (const failure of failures) {
453
+ console.log(` tospec validate ${failure.id} --type ${failure.type}`);
454
+ }
375
455
  }
376
456
  }
377
457
  process.exitCode = failed > 0 ? 1 : 0;
378
458
  }
379
459
  }
380
460
  /**
381
- * Validates schema-declared `requiredSections` for ticket/design/task-style
382
- * artifacts that have no Requirement grammar of their own. Only artifacts
383
- * that (a) declare a `validation` block and (b) already have an output file
384
- * on disk are checked — artifacts not yet produced are `status`'s concern,
385
- * not validate's.
461
+ * Validates schema-declared `requiredSections` for artifacts with no Requirement
462
+ * grammar of their own. Only artifacts that declare a `validation` block and
463
+ * already have an output file are checked — one not yet produced is `status`'s
464
+ * concern, not validate's.
386
465
  */
387
466
  export async function validateArtifactSections(projectRoot, changeDir) {
388
467
  const issues = [];
@@ -397,17 +476,27 @@ export async function validateArtifactSections(projectRoot, changeDir) {
397
476
  }
398
477
  const schemaDir = getSchemaDir(schemaName, projectRoot);
399
478
  for (const artifact of schema.artifacts) {
400
- if (!artifact.validation)
401
- continue;
402
479
  for (const file of resolveArtifactOutputs(changeDir, artifact.generates)) {
403
480
  const relPath = FileSystemUtils.toPosixPath(path.relative(changeDir, file));
481
+ // Runs before the section rules because a template satisfies them by
482
+ // construction — it is where every required heading came from. WARNING, so
483
+ // `--strict` is what refuses it.
484
+ if (isStubArtifactFile(file, schemaName, artifact.template, projectRoot)) {
485
+ issues.push({
486
+ level: 'WARNING',
487
+ path: relPath,
488
+ message: `Artifact "${artifact.id}" is still the unfilled template. Run tospec instructions ${artifact.id} --change <name> --json, then replace the file's contents.`,
489
+ });
490
+ continue;
491
+ }
492
+ if (!artifact.validation)
493
+ continue;
404
494
  let content;
405
495
  try {
406
496
  content = await fs.readFile(file, 'utf-8');
407
497
  }
408
498
  catch (error) {
409
- // File vanished between discovery and read (race) — report it as a
410
- // per-file error instead of crashing the whole command.
499
+ // Vanished between discovery and read: a per-file error, not a crash.
411
500
  issues.push({
412
501
  level: 'ERROR',
413
502
  path: relPath,
@@ -416,7 +505,7 @@ export async function validateArtifactSections(projectRoot, changeDir) {
416
505
  continue;
417
506
  }
418
507
  const templateRef = schemaDir
419
- ? FileSystemUtils.toPosixPath(path.join(path.relative(projectRoot, schemaDir), 'templates', artifact.template))
508
+ ? describeTemplatePath(projectRoot, schemaDir, artifact.template)
420
509
  : undefined;
421
510
  issues.push(...validateSections(content, artifact.validation, { path: relPath, templateRef }));
422
511
  }
@@ -426,9 +515,9 @@ export async function validateArtifactSections(projectRoot, changeDir) {
426
515
  const EMPTY_REPORT = { valid: true, issues: [], summary: { errors: 0, warnings: 0, info: 0 } };
427
516
  /**
428
517
  * Ambiguous task numbering, as WARNINGs. Task IDs are how apply refers to work,
429
- * so a duplicate or a mismatched group makes that reference point at the wrong
430
- * item — but the numbering is a convention, not a structural requirement, so it
431
- * reports rather than blocks (`--strict` still refuses).
518
+ * so a duplicate makes that reference point at the wrong item — but the
519
+ * numbering is a convention, not a structural requirement, so it reports rather
520
+ * than blocks (`--strict` still refuses).
432
521
  */
433
522
  async function validateTaskNumbering(projectRoot, changeDir) {
434
523
  const documents = [];
@@ -451,11 +540,8 @@ async function validateTaskNumbering(projectRoot, changeDir) {
451
540
  }));
452
541
  }
453
542
  /**
454
- * The single validation pipeline for a change — used verbatim by both
455
- * `tospec validate` and `tospec archive`, so their rule sets and severities
456
- * can never disagree ("validate green but archive blocked" is structurally
457
- * impossible). Runs delta-spec validation (when the schema requires deltas
458
- * or the change produced any) plus schema-declared section validation.
543
+ * The single validation pipeline for a change, used verbatim by `validate` and
544
+ * `archive`, so "validate green but archive blocked" is structurally impossible.
459
545
  */
460
546
  export async function validateChangeArtifacts(projectRoot, changeDir, options = {}) {
461
547
  const strict = options.strict ?? false;
@@ -463,11 +549,24 @@ export async function validateChangeArtifacts(projectRoot, changeDir, options =
463
549
  const validator = new Validator(strict);
464
550
  const marker = readSkipSpecsMarker(changeDir);
465
551
  const markerIssues = await skipSpecsMarkerIssues(changeDir, marker);
466
- // A declared marker waives the delta requirement — that is its whole job.
467
- // A *contradicted* marker (declared, but delta files present) must not: the
468
- // files are real and reporting only the contradiction would hide anything
469
- // else wrong with them.
470
- const requireDeltas = (!marker.declared || markerIssues.length > 0) &&
552
+ // A schema that cannot be resolved is the one finding: every other check
553
+ // would run against `sdd` by default and report "no delta" about a change
554
+ // whose real problem is the schema name. Unparseable metadata is already
555
+ // reported by the marker, and the schema cannot be read out of it either.
556
+ if (!marker.unreadableReason) {
557
+ const schemaIssue = schemaResolutionIssue(projectRoot, changeDir);
558
+ if (schemaIssue) {
559
+ return mergeSectionIssues(EMPTY_REPORT, [schemaIssue, ...markerIssues], strict);
560
+ }
561
+ }
562
+ // A declared marker waives the delta requirement; a *contradicted* one
563
+ // (declared, yet delta files exist) must not, because those files are real.
564
+ //
565
+ // Unparseable metadata waives it for the opposite reason: whether skip_specs
566
+ // was declared is unknowable, so "add a delta or set skip_specs" dead-ends a
567
+ // user who already set it inside the malformed file.
568
+ const requireDeltas = !marker.unreadableReason &&
569
+ (!marker.declared || markerIssues.length > 0) &&
471
570
  (await deltaSpecsRequired(projectRoot, changeDir));
472
571
  const [deltaReport, sectionIssues, numberingIssues] = await Promise.all([
473
572
  requireDeltas
@@ -481,12 +580,43 @@ export async function validateChangeArtifacts(projectRoot, changeDir, options =
481
580
  return mergeSectionIssues(deltaReport, [...sectionIssues, ...numberingIssues, ...markerIssues], strict);
482
581
  }
483
582
  /**
484
- * Errors in the skip_specs declaration itself: an unusable value, or a change
485
- * claiming it has no specs while carrying delta files. Both are contradictions
486
- * the user has to resolve — silently picking one reading would let a change
487
- * archive under an assumption its own contents deny.
583
+ * The ERROR for a change whose schema cannot be resolved (`.tospec.yaml` names
584
+ * one that does not exist, or the schema file itself is broken), or null.
585
+ * Same message `list` and `status` give, so the three commands agree on what
586
+ * is wrong with the change.
587
+ */
588
+ function schemaResolutionIssue(projectRoot, changeDir) {
589
+ try {
590
+ resolveSchema(resolveSchemaForChange(changeDir, undefined, projectRoot), projectRoot);
591
+ return null;
592
+ }
593
+ catch (error) {
594
+ const reason = (error instanceof Error ? error.message : String(error)).replace(/\.?\s*$/, '.');
595
+ return {
596
+ level: 'ERROR',
597
+ path: METADATA_FILENAME,
598
+ message: `${reason} Fix the schema name in ${METADATA_FILENAME} before anything else in this change can be checked.`,
599
+ };
600
+ }
601
+ }
602
+ /**
603
+ * Errors in the skip_specs declaration itself: unparseable metadata, an unusable
604
+ * value, or a change claiming no specs while carrying delta files. Each is a
605
+ * contradiction the user has to resolve.
606
+ *
607
+ * Most-fundamental first and one at a time: a file that does not parse cannot
608
+ * also be said to hold a wrong value.
488
609
  */
489
610
  async function skipSpecsMarkerIssues(changeDir, marker) {
611
+ if (marker.unreadableReason) {
612
+ return [
613
+ {
614
+ level: 'ERROR',
615
+ path: METADATA_FILENAME,
616
+ message: `Invalid YAML in ${METADATA_FILENAME}: ${marker.unreadableReason} Nothing in this file can be read until it parses — including \`skip_specs\`, so no other check about it means anything yet.`,
617
+ },
618
+ ];
619
+ }
490
620
  if (marker.invalidReason) {
491
621
  return [
492
622
  {
@@ -515,13 +645,9 @@ async function skipSpecsMarkerIssues(changeDir, marker) {
515
645
  }
516
646
  /**
517
647
  * Whether a change's schema requires at least one delta spec — a property of the
518
- * schema, not of the change.
519
- *
520
- * False when the schema declares no `specs` artifact, and when it declares one as
521
- * `optional` and this change produced none. A pure fix with no behavioral change
522
- * (e.g. an issue schema change) is a legitimate, complete change, not a
523
- * validation failure. Falls back to requiring deltas (the stricter, pre-existing
524
- * behavior) when the schema can't be resolved.
648
+ * schema, not of the change. False when the schema declares no `specs` artifact,
649
+ * or declares it `optional` and this change produced none: a pure fix with no
650
+ * behavioral change is a complete change, not a validation failure.
525
651
  */
526
652
  export async function deltaSpecsRequired(projectRoot, changeDir) {
527
653
  let schemaName;
@@ -531,16 +657,14 @@ export async function deltaSpecsRequired(projectRoot, changeDir) {
531
657
  schema = resolveSchema(schemaName, projectRoot);
532
658
  }
533
659
  catch {
534
- // Nothing is known about what this schema can produce, so demand the delta:
535
- // failing loudly beats passing a change nobody examined. Deliberately NOT
536
- // the same answer as the branch below, which is why it stays its own exit.
660
+ // Nothing is known about what this schema produces, so demand the delta:
661
+ // failing loudly beats passing a change nobody examined. The opposite
662
+ // answer from the `!specsArtifact` branch below, deliberately.
537
663
  return true;
538
664
  }
539
665
  const specsArtifact = schema.artifacts.find((a) => a.id === 'specs');
540
- // The schema told us what it produces and deltas were not on the list. Asking
541
- // for one is unsatisfiable — the file it names is not an artifact of this
542
- // schema, so it never appears in `status` and no instruction ever writes it —
543
- // and an unsatisfiable error is a dead end, not a rule.
666
+ // Deltas are not on this schema's list, so asking for one is unsatisfiable:
667
+ // the file never appears in `status` and no instruction ever writes it.
544
668
  if (!specsArtifact)
545
669
  return false;
546
670
  if (!specsArtifact.optional)
@@ -558,15 +682,24 @@ export function mergeSectionIssues(report, extra, strict) {
558
682
  return { valid, issues, summary: { errors, warnings, info } };
559
683
  }
560
684
  /**
561
- * One rendering of a finding for every human surface.
562
- *
563
- * The named-item and bulk paths print the same three fields and differ only by
564
- * indentation. This change exists because two renderings of the same data were
565
- * allowed to disagree; leaving the line format duplicated would reproduce that
566
- * the next time a field is added.
685
+ * Where to find an artifact's template, in whichever form is openable: relative
686
+ * to the project root when the schema lives inside it, absolute otherwise.
687
+ * `path.relative` on a packaged schema produces routes *out* of the project that
688
+ * say nothing about what they are relative to. Absolute also matches what
689
+ * `tospec templates --json` answers.
690
+ */
691
+ function describeTemplatePath(projectRoot, schemaDir, template) {
692
+ const absolute = path.join(schemaDir, 'templates', template);
693
+ const relative = path.relative(projectRoot, absolute);
694
+ const insideProject = relative !== '' && !relative.startsWith('..') && !path.isAbsolute(relative);
695
+ return FileSystemUtils.toPosixPath(insideProject ? relative : absolute);
696
+ }
697
+ /**
698
+ * One rendering of a finding for every human surface. The named-item and bulk
699
+ * paths print the same three fields and differ only by indentation.
567
700
  */
568
701
  export function formatIssueLine(issue, indent = '') {
569
- const prefix = issue.level === 'ERROR' ? '✗' : issue.level === 'WARNING' ? '⚠' : 'ℹ';
702
+ const prefix = issueSymbol(issue.level);
570
703
  return `${indent}${prefix} [${issue.level}] ${issue.path}: ${issue.message}`;
571
704
  }
572
705
  function summarizeType(results, type) {