@seanmars/tospec 0.24.0 → 0.26.0-beta

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 (332) hide show
  1. package/CHANGELOG.md +241 -0
  2. package/README.md +10 -6
  3. package/assets/dashboard/lib/derive.js +11 -5
  4. package/assets/dashboard/lib/store.js +4 -2
  5. package/assets/dashboard/views/change.js +5 -3
  6. package/dist/cli/index.d.ts.map +1 -1
  7. package/dist/cli/index.js +42 -7
  8. package/dist/cli/index.js.map +1 -1
  9. package/dist/commands/config.d.ts.map +1 -1
  10. package/dist/commands/config.js +213 -100
  11. package/dist/commands/config.js.map +1 -1
  12. package/dist/commands/dashboard.d.ts +9 -0
  13. package/dist/commands/dashboard.d.ts.map +1 -1
  14. package/dist/commands/dashboard.js +77 -21
  15. package/dist/commands/dashboard.js.map +1 -1
  16. package/dist/commands/decision.d.ts +38 -1
  17. package/dist/commands/decision.d.ts.map +1 -1
  18. package/dist/commands/decision.js +303 -27
  19. package/dist/commands/decision.js.map +1 -1
  20. package/dist/commands/metrics.d.ts +3 -4
  21. package/dist/commands/metrics.d.ts.map +1 -1
  22. package/dist/commands/metrics.js +3 -5
  23. package/dist/commands/metrics.js.map +1 -1
  24. package/dist/commands/shared-output.d.ts +24 -0
  25. package/dist/commands/shared-output.d.ts.map +1 -1
  26. package/dist/commands/shared-output.js +21 -2
  27. package/dist/commands/shared-output.js.map +1 -1
  28. package/dist/commands/show.d.ts.map +1 -1
  29. package/dist/commands/show.js +12 -9
  30. package/dist/commands/show.js.map +1 -1
  31. package/dist/commands/validate.d.ts +19 -0
  32. package/dist/commands/validate.d.ts.map +1 -1
  33. package/dist/commands/validate.js +243 -64
  34. package/dist/commands/validate.js.map +1 -1
  35. package/dist/commands/workflow/instructions.d.ts +7 -1
  36. package/dist/commands/workflow/instructions.d.ts.map +1 -1
  37. package/dist/commands/workflow/instructions.js +299 -81
  38. package/dist/commands/workflow/instructions.js.map +1 -1
  39. package/dist/commands/workflow/new-change.d.ts.map +1 -1
  40. package/dist/commands/workflow/new-change.js +26 -17
  41. package/dist/commands/workflow/new-change.js.map +1 -1
  42. package/dist/commands/workflow/prompt-escape.d.ts +25 -0
  43. package/dist/commands/workflow/prompt-escape.d.ts.map +1 -0
  44. package/dist/commands/workflow/prompt-escape.js +62 -0
  45. package/dist/commands/workflow/prompt-escape.js.map +1 -0
  46. package/dist/commands/workflow/schemas.d.ts.map +1 -1
  47. package/dist/commands/workflow/schemas.js +34 -16
  48. package/dist/commands/workflow/schemas.js.map +1 -1
  49. package/dist/commands/workflow/shared.d.ts +46 -10
  50. package/dist/commands/workflow/shared.d.ts.map +1 -1
  51. package/dist/commands/workflow/shared.js +66 -30
  52. package/dist/commands/workflow/shared.js.map +1 -1
  53. package/dist/commands/workflow/status.d.ts +5 -2
  54. package/dist/commands/workflow/status.d.ts.map +1 -1
  55. package/dist/commands/workflow/status.js +62 -28
  56. package/dist/commands/workflow/status.js.map +1 -1
  57. package/dist/core/archive.d.ts +38 -0
  58. package/dist/core/archive.d.ts.map +1 -1
  59. package/dist/core/archive.js +492 -75
  60. package/dist/core/archive.js.map +1 -1
  61. package/dist/core/artifact-graph/index.d.ts +2 -2
  62. package/dist/core/artifact-graph/index.d.ts.map +1 -1
  63. package/dist/core/artifact-graph/index.js +2 -2
  64. package/dist/core/artifact-graph/index.js.map +1 -1
  65. package/dist/core/artifact-graph/instruction-loader.d.ts +8 -0
  66. package/dist/core/artifact-graph/instruction-loader.d.ts.map +1 -1
  67. package/dist/core/artifact-graph/instruction-loader.js +58 -36
  68. package/dist/core/artifact-graph/instruction-loader.js.map +1 -1
  69. package/dist/core/artifact-graph/resolver.d.ts +39 -12
  70. package/dist/core/artifact-graph/resolver.d.ts.map +1 -1
  71. package/dist/core/artifact-graph/resolver.js +82 -36
  72. package/dist/core/artifact-graph/resolver.js.map +1 -1
  73. package/dist/core/artifact-graph/schema.d.ts +24 -0
  74. package/dist/core/artifact-graph/schema.d.ts.map +1 -1
  75. package/dist/core/artifact-graph/schema.js +32 -1
  76. package/dist/core/artifact-graph/schema.js.map +1 -1
  77. package/dist/core/artifact-graph/stub-detection.d.ts +10 -2
  78. package/dist/core/artifact-graph/stub-detection.d.ts.map +1 -1
  79. package/dist/core/artifact-graph/stub-detection.js +18 -11
  80. package/dist/core/artifact-graph/stub-detection.js.map +1 -1
  81. package/dist/core/artifact-graph/types.d.ts +2 -2
  82. package/dist/core/artifact-graph/types.d.ts.map +1 -1
  83. package/dist/core/artifact-graph/types.js +10 -1
  84. package/dist/core/artifact-graph/types.js.map +1 -1
  85. package/dist/core/change-metadata/schema.d.ts.map +1 -1
  86. package/dist/core/change-metadata/schema.js +18 -4
  87. package/dist/core/change-metadata/schema.js.map +1 -1
  88. package/dist/core/change-presenter.d.ts.map +1 -1
  89. package/dist/core/change-presenter.js +13 -8
  90. package/dist/core/change-presenter.js.map +1 -1
  91. package/dist/core/change-status-policy.d.ts.map +1 -1
  92. package/dist/core/change-status-policy.js +3 -1
  93. package/dist/core/change-status-policy.js.map +1 -1
  94. package/dist/core/codex-metrics.d.ts +1 -1
  95. package/dist/core/codex-metrics.js +1 -1
  96. package/dist/core/config-schema.d.ts +8 -0
  97. package/dist/core/config-schema.d.ts.map +1 -1
  98. package/dist/core/config-schema.js +10 -0
  99. package/dist/core/config-schema.js.map +1 -1
  100. package/dist/core/dashboard-activity.d.ts +4 -4
  101. package/dist/core/dashboard-activity.js +4 -4
  102. package/dist/core/dashboard-data.d.ts +11 -5
  103. package/dist/core/dashboard-data.d.ts.map +1 -1
  104. package/dist/core/dashboard-data.js +54 -28
  105. package/dist/core/dashboard-data.js.map +1 -1
  106. package/dist/core/dashboard-hub.d.ts +3 -3
  107. package/dist/core/dashboard-hub.d.ts.map +1 -1
  108. package/dist/core/dashboard-hub.js +16 -4
  109. package/dist/core/dashboard-hub.js.map +1 -1
  110. package/dist/core/dashboard-routes.d.ts +2 -1
  111. package/dist/core/dashboard-routes.d.ts.map +1 -1
  112. package/dist/core/dashboard-routes.js +102 -33
  113. package/dist/core/dashboard-routes.js.map +1 -1
  114. package/dist/core/global-config.d.ts +71 -2
  115. package/dist/core/global-config.d.ts.map +1 -1
  116. package/dist/core/global-config.js +149 -4
  117. package/dist/core/global-config.js.map +1 -1
  118. package/dist/core/id.d.ts +9 -0
  119. package/dist/core/id.d.ts.map +1 -1
  120. package/dist/core/id.js +37 -0
  121. package/dist/core/id.js.map +1 -1
  122. package/dist/core/init.d.ts +9 -2
  123. package/dist/core/init.d.ts.map +1 -1
  124. package/dist/core/init.js +116 -33
  125. package/dist/core/init.js.map +1 -1
  126. package/dist/core/list.d.ts.map +1 -1
  127. package/dist/core/list.js +69 -45
  128. package/dist/core/list.js.map +1 -1
  129. package/dist/core/local-server.d.ts +9 -2
  130. package/dist/core/local-server.d.ts.map +1 -1
  131. package/dist/core/local-server.js +24 -4
  132. package/dist/core/local-server.js.map +1 -1
  133. package/dist/core/markdown-render.d.ts.map +1 -1
  134. package/dist/core/markdown-render.js +7 -2
  135. package/dist/core/markdown-render.js.map +1 -1
  136. package/dist/core/migrate.d.ts +24 -2
  137. package/dist/core/migrate.d.ts.map +1 -1
  138. package/dist/core/migrate.js +335 -59
  139. package/dist/core/migrate.js.map +1 -1
  140. package/dist/core/parsers/change-parser.d.ts +20 -1
  141. package/dist/core/parsers/change-parser.d.ts.map +1 -1
  142. package/dist/core/parsers/change-parser.js +76 -73
  143. package/dist/core/parsers/change-parser.js.map +1 -1
  144. package/dist/core/parsers/markdown-parser.d.ts +24 -0
  145. package/dist/core/parsers/markdown-parser.d.ts.map +1 -1
  146. package/dist/core/parsers/markdown-parser.js +65 -10
  147. package/dist/core/parsers/markdown-parser.js.map +1 -1
  148. package/dist/core/parsers/requirement-blocks.d.ts +127 -6
  149. package/dist/core/parsers/requirement-blocks.d.ts.map +1 -1
  150. package/dist/core/parsers/requirement-blocks.js +349 -56
  151. package/dist/core/parsers/requirement-blocks.js.map +1 -1
  152. package/dist/core/parsers/requirement-text.d.ts +25 -1
  153. package/dist/core/parsers/requirement-text.d.ts.map +1 -1
  154. package/dist/core/parsers/requirement-text.js +62 -6
  155. package/dist/core/parsers/requirement-text.js.map +1 -1
  156. package/dist/core/parsers/spec-structure.d.ts.map +1 -1
  157. package/dist/core/parsers/spec-structure.js +36 -24
  158. package/dist/core/parsers/spec-structure.js.map +1 -1
  159. package/dist/core/project-config.d.ts +9 -2
  160. package/dist/core/project-config.d.ts.map +1 -1
  161. package/dist/core/project-config.js +30 -7
  162. package/dist/core/project-config.js.map +1 -1
  163. package/dist/core/project-layout.d.ts +26 -0
  164. package/dist/core/project-layout.d.ts.map +1 -1
  165. package/dist/core/project-layout.js +51 -1
  166. package/dist/core/project-layout.js.map +1 -1
  167. package/dist/core/schema-names.d.ts +17 -0
  168. package/dist/core/schema-names.d.ts.map +1 -1
  169. package/dist/core/schema-names.js +24 -0
  170. package/dist/core/schema-names.js.map +1 -1
  171. package/dist/core/shared/index.d.ts +1 -1
  172. package/dist/core/shared/index.d.ts.map +1 -1
  173. package/dist/core/shared/index.js +1 -1
  174. package/dist/core/shared/index.js.map +1 -1
  175. package/dist/core/shared/rules-generation.d.ts +12 -0
  176. package/dist/core/shared/rules-generation.d.ts.map +1 -1
  177. package/dist/core/shared/rules-generation.js +26 -2
  178. package/dist/core/shared/rules-generation.js.map +1 -1
  179. package/dist/core/shared/skill-generation.d.ts +7 -0
  180. package/dist/core/shared/skill-generation.d.ts.map +1 -1
  181. package/dist/core/shared/skill-generation.js +19 -0
  182. package/dist/core/shared/skill-generation.js.map +1 -1
  183. package/dist/core/shared/tool-detection.d.ts +10 -1
  184. package/dist/core/shared/tool-detection.d.ts.map +1 -1
  185. package/dist/core/shared/tool-detection.js +37 -6
  186. package/dist/core/shared/tool-detection.js.map +1 -1
  187. package/dist/core/spec-presenter.d.ts.map +1 -1
  188. package/dist/core/spec-presenter.js +23 -1
  189. package/dist/core/spec-presenter.js.map +1 -1
  190. package/dist/core/specs-apply.d.ts +98 -1
  191. package/dist/core/specs-apply.d.ts.map +1 -1
  192. package/dist/core/specs-apply.js +426 -63
  193. package/dist/core/specs-apply.js.map +1 -1
  194. package/dist/core/templates/fragments/change-diff.d.ts +37 -0
  195. package/dist/core/templates/fragments/change-diff.d.ts.map +1 -0
  196. package/dist/core/templates/fragments/change-diff.js +37 -0
  197. package/dist/core/templates/fragments/change-diff.js.map +1 -0
  198. package/dist/core/templates/fragments/sync-fail.d.ts +8 -0
  199. package/dist/core/templates/fragments/sync-fail.d.ts.map +1 -0
  200. package/dist/core/templates/fragments/sync-fail.js +8 -0
  201. package/dist/core/templates/fragments/sync-fail.js.map +1 -0
  202. package/dist/core/templates/fragments/validate.d.ts +14 -1
  203. package/dist/core/templates/fragments/validate.d.ts.map +1 -1
  204. package/dist/core/templates/fragments/validate.js +14 -1
  205. package/dist/core/templates/fragments/validate.js.map +1 -1
  206. package/dist/core/templates/fragments/verify.d.ts +2 -2
  207. package/dist/core/templates/fragments/verify.d.ts.map +1 -1
  208. package/dist/core/templates/fragments/verify.js +8 -5
  209. package/dist/core/templates/fragments/verify.js.map +1 -1
  210. package/dist/core/templates/workflows/apply.d.ts +1 -1
  211. package/dist/core/templates/workflows/apply.d.ts.map +1 -1
  212. package/dist/core/templates/workflows/apply.js +29 -10
  213. package/dist/core/templates/workflows/apply.js.map +1 -1
  214. package/dist/core/templates/workflows/archive.d.ts.map +1 -1
  215. package/dist/core/templates/workflows/archive.js +27 -9
  216. package/dist/core/templates/workflows/archive.js.map +1 -1
  217. package/dist/core/templates/workflows/decision.d.ts.map +1 -1
  218. package/dist/core/templates/workflows/decision.js +10 -4
  219. package/dist/core/templates/workflows/decision.js.map +1 -1
  220. package/dist/core/templates/workflows/explore.d.ts +2 -2
  221. package/dist/core/templates/workflows/explore.d.ts.map +1 -1
  222. package/dist/core/templates/workflows/explore.js +7 -3
  223. package/dist/core/templates/workflows/explore.js.map +1 -1
  224. package/dist/core/templates/workflows/grill.d.ts +2 -2
  225. package/dist/core/templates/workflows/grill.d.ts.map +1 -1
  226. package/dist/core/templates/workflows/grill.js +5 -4
  227. package/dist/core/templates/workflows/grill.js.map +1 -1
  228. package/dist/core/templates/workflows/issue.d.ts.map +1 -1
  229. package/dist/core/templates/workflows/issue.js +9 -3
  230. package/dist/core/templates/workflows/issue.js.map +1 -1
  231. package/dist/core/templates/workflows/propose.d.ts.map +1 -1
  232. package/dist/core/templates/workflows/propose.js +4 -1
  233. package/dist/core/templates/workflows/propose.js.map +1 -1
  234. package/dist/core/templates/workflows/sync.d.ts.map +1 -1
  235. package/dist/core/templates/workflows/sync.js +25 -11
  236. package/dist/core/templates/workflows/sync.js.map +1 -1
  237. package/dist/core/templates/workflows/update.d.ts.map +1 -1
  238. package/dist/core/templates/workflows/update.js +17 -10
  239. package/dist/core/templates/workflows/update.js.map +1 -1
  240. package/dist/core/update.d.ts +5 -0
  241. package/dist/core/update.d.ts.map +1 -1
  242. package/dist/core/update.js +66 -29
  243. package/dist/core/update.js.map +1 -1
  244. package/dist/core/validation/constants.d.ts +7 -1
  245. package/dist/core/validation/constants.d.ts.map +1 -1
  246. package/dist/core/validation/constants.js +16 -2
  247. package/dist/core/validation/constants.js.map +1 -1
  248. package/dist/core/validation/purpose-placeholder.d.ts.map +1 -1
  249. package/dist/core/validation/purpose-placeholder.js +35 -11
  250. package/dist/core/validation/purpose-placeholder.js.map +1 -1
  251. package/dist/core/validation/section-validator.d.ts +7 -0
  252. package/dist/core/validation/section-validator.d.ts.map +1 -1
  253. package/dist/core/validation/section-validator.js +22 -2
  254. package/dist/core/validation/section-validator.js.map +1 -1
  255. package/dist/core/validation/sister-changes.d.ts +93 -0
  256. package/dist/core/validation/sister-changes.d.ts.map +1 -0
  257. package/dist/core/validation/sister-changes.js +180 -0
  258. package/dist/core/validation/sister-changes.js.map +1 -0
  259. package/dist/core/validation/task-markers.d.ts +9 -0
  260. package/dist/core/validation/task-markers.d.ts.map +1 -0
  261. package/dist/core/validation/task-markers.js +71 -0
  262. package/dist/core/validation/task-markers.js.map +1 -0
  263. package/dist/core/validation/types.d.ts +58 -0
  264. package/dist/core/validation/types.d.ts.map +1 -1
  265. package/dist/core/validation/types.js +22 -0
  266. package/dist/core/validation/types.js.map +1 -1
  267. package/dist/core/validation/validator.d.ts +79 -2
  268. package/dist/core/validation/validator.d.ts.map +1 -1
  269. package/dist/core/validation/validator.js +565 -84
  270. package/dist/core/validation/validator.js.map +1 -1
  271. package/dist/utils/change-metadata.d.ts +62 -5
  272. package/dist/utils/change-metadata.d.ts.map +1 -1
  273. package/dist/utils/change-metadata.js +193 -31
  274. package/dist/utils/change-metadata.js.map +1 -1
  275. package/dist/utils/change-utils.d.ts +21 -0
  276. package/dist/utils/change-utils.d.ts.map +1 -1
  277. package/dist/utils/change-utils.js +168 -53
  278. package/dist/utils/change-utils.js.map +1 -1
  279. package/dist/utils/command-line.d.ts +11 -0
  280. package/dist/utils/command-line.d.ts.map +1 -0
  281. package/dist/utils/command-line.js +43 -0
  282. package/dist/utils/command-line.js.map +1 -0
  283. package/dist/utils/display-width.d.ts +10 -0
  284. package/dist/utils/display-width.d.ts.map +1 -0
  285. package/dist/utils/display-width.js +15 -0
  286. package/dist/utils/display-width.js.map +1 -0
  287. package/dist/utils/file-system.d.ts +16 -0
  288. package/dist/utils/file-system.d.ts.map +1 -1
  289. package/dist/utils/file-system.js +49 -2
  290. package/dist/utils/file-system.js.map +1 -1
  291. package/dist/utils/frontmatter.d.ts.map +1 -1
  292. package/dist/utils/frontmatter.js +11 -5
  293. package/dist/utils/frontmatter.js.map +1 -1
  294. package/dist/utils/interactive.d.ts.map +1 -1
  295. package/dist/utils/interactive.js +3 -1
  296. package/dist/utils/interactive.js.map +1 -1
  297. package/dist/utils/item-discovery.d.ts +3 -2
  298. package/dist/utils/item-discovery.d.ts.map +1 -1
  299. package/dist/utils/item-discovery.js +34 -16
  300. package/dist/utils/item-discovery.js.map +1 -1
  301. package/dist/utils/link.d.ts +0 -1
  302. package/dist/utils/link.d.ts.map +1 -1
  303. package/dist/utils/link.js +0 -1
  304. package/dist/utils/link.js.map +1 -1
  305. package/dist/utils/match.d.ts.map +1 -1
  306. package/dist/utils/match.js +10 -1
  307. package/dist/utils/match.js.map +1 -1
  308. package/dist/utils/option-flags.d.ts +9 -0
  309. package/dist/utils/option-flags.d.ts.map +1 -0
  310. package/dist/utils/option-flags.js +12 -0
  311. package/dist/utils/option-flags.js.map +1 -0
  312. package/dist/utils/sentence.d.ts +9 -0
  313. package/dist/utils/sentence.d.ts.map +1 -0
  314. package/dist/utils/sentence.js +12 -0
  315. package/dist/utils/sentence.js.map +1 -0
  316. package/dist/utils/spec-files.d.ts +84 -1
  317. package/dist/utils/spec-files.d.ts.map +1 -1
  318. package/dist/utils/spec-files.js +192 -3
  319. package/dist/utils/spec-files.js.map +1 -1
  320. package/dist/utils/task-progress.d.ts.map +1 -1
  321. package/dist/utils/task-progress.js +32 -9
  322. package/dist/utils/task-progress.js.map +1 -1
  323. package/dist/utils/timestamp.d.ts +27 -0
  324. package/dist/utils/timestamp.d.ts.map +1 -1
  325. package/dist/utils/timestamp.js +37 -0
  326. package/dist/utils/timestamp.js.map +1 -1
  327. package/package.json +4 -1
  328. package/schemas/decision/templates/decision.md +2 -2
  329. package/schemas/issue/schema.yaml +12 -6
  330. package/schemas/issue/templates/task.md +1 -1
  331. package/schemas/sdd/schema.yaml +21 -10
  332. package/schemas/sdd/templates/proposal.md +1 -1
@@ -1,22 +1,24 @@
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
+ import { issueSymbol, ARCHIVE_REFUSAL_CODE } from '../core/validation/types.js';
5
5
  import { validateSections } from '../core/validation/section-validator.js';
6
6
  import { resolveSchema, getSchemaDir } from '../core/artifact-graph/resolver.js';
7
7
  import { resolveArtifactOutputs } from '../core/artifact-graph/outputs.js';
8
- import { isStubArtifactFile } from '../core/artifact-graph/stub-detection.js';
9
- import { METADATA_FILENAME, readSkipSpecsMarker, resolveSchemaForChange, } from '../utils/change-metadata.js';
8
+ import { detectStubArtifact } from '../core/artifact-graph/stub-detection.js';
9
+ import { ChangeMetadataError, METADATA_FILENAME, UnknownChangeSchemaError, readSkipSpecsMarker, resolveSchemaForChange, findChangeSchemaFault, findUnknownMetadataKeys, readChangeMetadata, suggestMetadataKey, } from '../utils/change-metadata.js';
10
+ import { findDecisionReferenceProblems } from './decision.js';
10
11
  import { FileSystemUtils } from '../utils/file-system.js';
11
- import { findSpecFiles } from '../utils/spec-files.js';
12
+ import { findRootDeltaFiles, findSkipSpecsContradictions } from '../utils/spec-files.js';
12
13
  import { resolveRootForCommand, toRootOutput, } from '../core/root-selection.js';
13
14
  import { isInteractive, resolveNoInteractive } from '../utils/interactive.js';
14
- import { emitSuccess, emitFailureStatus } from './shared-output.js';
15
+ import { emitSuccess, emitFailureStatus, indentContinuation } from './shared-output.js';
15
16
  import { getSpecIds } from '../utils/item-discovery.js';
16
17
  import { getAvailableChanges } from './workflow/shared.js';
17
18
  import { nearestMatches } from '../utils/match.js';
18
19
  import { resolveTaskFiles } from '../utils/task-progress.js';
19
20
  import { findTaskNumberingIssues, } from '../core/validation/task-numbering.js';
21
+ import { findTaskMarkerIssues } from '../core/validation/task-markers.js';
20
22
  /**
21
23
  * The one null-shape every `validate --json` failure carries, so a caller
22
24
  * reading `result.summary.totals` never gets `undefined` from one failure path
@@ -105,7 +107,7 @@ export class ValidateCommand {
105
107
  items.push(...changes.map(id => ({ name: `change/${id}`, value: { type: 'change', id } })));
106
108
  items.push(...specs.map(id => ({ name: `spec/${id}`, value: { type: 'spec', id } })));
107
109
  if (items.length === 0) {
108
- console.error('No items found to validate.');
110
+ console.error('Error: No items found to validate.');
109
111
  process.exitCode = 1;
110
112
  return;
111
113
  }
@@ -132,10 +134,12 @@ export class ValidateCommand {
132
134
  });
133
135
  return;
134
136
  }
135
- console.error('Nothing to validate. Try one of:');
137
+ // Every line after `Error:` is indented, as a multi-line fix is: a bare
138
+ // line would read as a second message.
139
+ console.error('Error: Nothing to validate. Try one of:');
136
140
  for (const line of fix)
137
141
  console.error(` ${line}`);
138
- console.error('Or run in an interactive terminal.');
142
+ console.error(' Or run in an interactive terminal.');
139
143
  process.exitCode = 1;
140
144
  }
141
145
  async validateDirectItem(root, itemName, opts) {
@@ -162,7 +166,7 @@ export class ValidateCommand {
162
166
  emitFailureStatus({ ...VALIDATE_FAILURE_PAYLOAD, root: toRootOutput(root) }, { severity: 'error', code: 'unknown_item', message });
163
167
  }
164
168
  else {
165
- console.error(message);
169
+ console.error(`Error: ${message}`);
166
170
  process.exitCode = 1;
167
171
  }
168
172
  return;
@@ -178,7 +182,7 @@ export class ValidateCommand {
178
182
  emitFailureStatus({ ...VALIDATE_FAILURE_PAYLOAD, root: toRootOutput(root) }, { severity: 'error', code: 'unknown_item', message });
179
183
  }
180
184
  else {
181
- console.error(message);
185
+ console.error(`Error: ${message}`);
182
186
  process.exitCode = 1;
183
187
  }
184
188
  return;
@@ -193,8 +197,8 @@ export class ValidateCommand {
193
197
  });
194
198
  return;
195
199
  }
196
- console.error(`Ambiguous item '${itemName}' matches both a change and a spec.`);
197
- console.error('Pass --type change|spec, or use: tospec validate --changes / tospec validate --specs');
200
+ console.error(`Error: Ambiguous item '${itemName}' matches both a change and a spec.`);
201
+ console.error('Fix: Pass --type change|spec, or use: tospec validate --changes / tospec validate --specs');
198
202
  process.exitCode = 1;
199
203
  return;
200
204
  }
@@ -220,6 +224,8 @@ export class ValidateCommand {
220
224
  }
221
225
  printReport(type, id, report, durationMs, json, root, strict = false) {
222
226
  if (json) {
227
+ const archiveRefusals = countArchiveRefusals(report.issues);
228
+ const counts = { items: 1, passed: report.valid ? 1 : 0, failed: report.valid ? 0 : 1, archiveRefusals };
223
229
  emitSuccess({
224
230
  items: [
225
231
  {
@@ -229,9 +235,10 @@ export class ValidateCommand {
229
235
  issues: report.issues,
230
236
  ...(report.notes?.length ? { notes: report.notes } : {}),
231
237
  durationMs,
238
+ archiveRefusals,
232
239
  },
233
240
  ],
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 } } },
241
+ summary: { totals: counts, byType: { [type]: { ...counts } } },
235
242
  }, toRootOutput(root));
236
243
  return;
237
244
  }
@@ -272,6 +279,19 @@ export class ValidateCommand {
272
279
  printNextSteps(type, id, root, issues, strict) {
273
280
  const bullets = [];
274
281
  if (type === 'change') {
282
+ // The metadata and the whole-change "No deltas found" ERROR sit outside
283
+ // `<capability>/spec.md`, so the delta hints below never matched them,
284
+ // and a change failing on those too was told only to add a section.
285
+ // Their messages carry the fix; the bullet says it is still owed. It does
286
+ // not say "Fix .tospec.yaml": a schema issue filed there may be fixed in
287
+ // tospec/config.yaml or the schema's own file.
288
+ const fails = (issue) => issue.level === 'ERROR' || strict;
289
+ if (issues.some((issue) => issue.path === METADATA_FILENAME && fails(issue))) {
290
+ bullets.push(`- Resolve the ${METADATA_FILENAME} issue as its message above says`);
291
+ }
292
+ if (issues.some((issue) => issue.path === 'file' && /No deltas found/.test(issue.message) && fails(issue))) {
293
+ bullets.push('- Write the delta specs under specs/<capability>/spec.md, or set `skip_specs: true` in .tospec.yaml only when the user confirms this change needs no delta spec');
294
+ }
275
295
  // Each delta hint answers one failure. A missing Migration field or a
276
296
  // SHALL/MUST warning under --strict is a delta issue too, and used to be
277
297
  // told to add a Scenario block it already had.
@@ -357,10 +377,10 @@ export class ValidateCommand {
357
377
  }
358
378
  if (queue.length === 0) {
359
379
  const summary = {
360
- totals: { items: 0, passed: 0, failed: 0 },
380
+ totals: { items: 0, passed: 0, failed: 0, archiveRefusals: 0 },
361
381
  byType: {
362
- ...(scope.changes ? { change: { items: 0, passed: 0, failed: 0 } } : {}),
363
- ...(scope.specs ? { spec: { items: 0, passed: 0, failed: 0 } } : {}),
382
+ ...(scope.changes ? { change: { items: 0, passed: 0, failed: 0, archiveRefusals: 0 } } : {}),
383
+ ...(scope.specs ? { spec: { items: 0, passed: 0, failed: 0, archiveRefusals: 0 } } : {}),
364
384
  },
365
385
  };
366
386
  if (opts.json) {
@@ -415,7 +435,7 @@ export class ValidateCommand {
415
435
  const passed = results.filter((res) => res.valid).length;
416
436
  const failed = results.length - passed;
417
437
  const summary = {
418
- totals: { items: results.length, passed, failed },
438
+ totals: { items: results.length, passed, failed, archiveRefusals: sumArchiveRefusals(results) },
419
439
  byType: {
420
440
  ...(scope.changes ? { change: summarizeType(results, 'change') } : {}),
421
441
  ...(scope.specs ? { spec: summarizeType(results, 'spec') } : {}),
@@ -423,7 +443,10 @@ export class ValidateCommand {
423
443
  };
424
444
  // One copy per run: a sweep of forty specs trips the same rule forty times.
425
445
  const runNotes = [...new Set(results.flatMap((res) => res.notes ?? []))];
426
- const items = results.map(({ notes: _notes, ...res }) => res);
446
+ const items = results.map(({ notes: _notes, ...res }) => ({
447
+ ...res,
448
+ archiveRefusals: countArchiveRefusals(res.issues),
449
+ }));
427
450
  if (opts.json) {
428
451
  emitSuccess({ items, summary, ...(runNotes.length ? { notes: runNotes } : {}) }, toRootOutput(root));
429
452
  }
@@ -475,17 +498,24 @@ export async function validateArtifactSections(projectRoot, changeDir) {
475
498
  return issues;
476
499
  }
477
500
  const schemaDir = getSchemaDir(schemaName, projectRoot);
501
+ const changeName = path.basename(changeDir);
478
502
  for (const artifact of schema.artifacts) {
479
503
  for (const file of resolveArtifactOutputs(changeDir, artifact.generates)) {
480
504
  const relPath = FileSystemUtils.toPosixPath(path.relative(changeDir, file));
481
505
  // Runs before the section rules because a template satisfies them by
482
506
  // construction — it is where every required heading came from. WARNING, so
483
507
  // `--strict` is what refuses it.
484
- if (isStubArtifactFile(file, schemaName, artifact.template, projectRoot)) {
508
+ // The change's own name, not a `<name>` placeholder: the message is the
509
+ // fix, and a placeholder makes the reader rebuild the command by hand.
510
+ const stub = detectStubArtifact(file, schemaName, artifact.template, projectRoot);
511
+ if (stub !== null) {
512
+ const instructions = `tospec instructions ${artifact.id} --change "${changeName}" --json`;
485
513
  issues.push({
486
514
  level: 'WARNING',
487
515
  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.`,
516
+ message: stub === 'empty'
517
+ ? `Artifact "${artifact.id}" is empty. Run ${instructions}, then write the file's contents.`
518
+ : `Artifact "${artifact.id}" is still the unfilled template. Run ${instructions}, then replace the file's contents.`,
489
519
  });
490
520
  continue;
491
521
  }
@@ -514,12 +544,13 @@ export async function validateArtifactSections(projectRoot, changeDir) {
514
544
  }
515
545
  const EMPTY_REPORT = { valid: true, issues: [], summary: { errors: 0, warnings: 0, info: 0 } };
516
546
  /**
517
- * Ambiguous task numbering, as WARNINGs. Task IDs are how apply refers to work,
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).
547
+ * Ambiguous task numbering and task lines the counter cannot see, as WARNINGs.
548
+ * Task IDs are how apply refers to work, so a duplicate makes that reference
549
+ * point at the wrong item; a `[~]` box drops out of the count, so the change
550
+ * reads done. Both are conventions, not structural requirements, so they report
551
+ * rather than block (`--strict` still refuses).
521
552
  */
522
- async function validateTaskNumbering(projectRoot, changeDir) {
553
+ async function validateTaskFiles(projectRoot, changeDir) {
523
554
  const documents = [];
524
555
  for (const file of resolveTaskFiles(changeDir, projectRoot)) {
525
556
  try {
@@ -532,10 +563,10 @@ async function validateTaskNumbering(projectRoot, changeDir) {
532
563
  continue; // no tasks file yet — the artifact-status check owns that
533
564
  }
534
565
  }
535
- return findTaskNumberingIssues(documents).map((issue) => ({
566
+ return [...findTaskNumberingIssues(documents), ...findTaskMarkerIssues(documents)].map((issue) => ({
536
567
  level: 'WARNING',
537
568
  path: issue.path,
538
- line: issue.line,
569
+ ...(issue.line !== undefined ? { line: issue.line } : {}),
539
570
  message: issue.message,
540
571
  }));
541
572
  }
@@ -549,6 +580,13 @@ export async function validateChangeArtifacts(projectRoot, changeDir, options =
549
580
  const validator = new Validator(strict);
550
581
  const marker = readSkipSpecsMarker(changeDir);
551
582
  const markerIssues = await skipSpecsMarkerIssues(changeDir, marker);
583
+ // Kept apart from markerIssues: those void the skip_specs waiver below, and
584
+ // a misspelled sibling key or a dangling ADR link does not contradict
585
+ // skip_specs. Skipped for a file that does not parse: the marker already
586
+ // reports it, and it has no keys to name.
587
+ const metadataFieldIssues = marker.unreadableReason
588
+ ? []
589
+ : [...unknownMetadataKeyIssues(changeDir), ...decisionReferenceIssues(projectRoot, changeDir)];
552
590
  // A schema that cannot be resolved is the one finding: every other check
553
591
  // would run against `sdd` by default and report "no delta" about a change
554
592
  // whose real problem is the schema name. Unparseable metadata is already
@@ -556,28 +594,65 @@ export async function validateChangeArtifacts(projectRoot, changeDir, options =
556
594
  if (!marker.unreadableReason) {
557
595
  const schemaIssue = schemaResolutionIssue(projectRoot, changeDir);
558
596
  if (schemaIssue) {
559
- return mergeSectionIssues(EMPTY_REPORT, [schemaIssue, ...markerIssues], strict);
597
+ return mergeSectionIssues(EMPTY_REPORT, [schemaIssue, ...markerIssues, ...metadataFieldIssues], strict);
560
598
  }
561
599
  }
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) &&
570
- (await deltaSpecsRequired(projectRoot, changeDir));
571
- const [deltaReport, sectionIssues, numberingIssues] = await Promise.all([
572
- requireDeltas
600
+ const requireDeltas = await requiresDeltas(projectRoot, changeDir, marker, markerIssues);
601
+ // The delta pass also runs for a schema that demands no delta, whenever the
602
+ // change has a specs/ directory: archive merges every
603
+ // specs/<capability>/spec.md whatever the schema says. Gated on requireDeltas
604
+ // alone, such a schema merged deltas nobody had checked, and a nested or
605
+ // dot-folder spec.md passed here while the archive backstop refused it and
606
+ // sent the user back to validate. requireDeltas still decides whether a
607
+ // delta must exist.
608
+ const hasSpecsDir = await fs
609
+ .stat(path.join(changeDir, 'specs'))
610
+ .then((stat) => stat.isDirectory(), () => false);
611
+ const [deltaReport, sectionIssues, taskFileIssues, rootDeltaIssues] = await Promise.all([
612
+ requireDeltas || hasSpecsDir
573
613
  ? validator.validateChangeDeltaSpecs(changeDir, path.join(projectRoot, 'tospec', 'specs'), {
574
614
  archivePreflight,
615
+ requireDelta: requireDeltas,
616
+ sisterChanges: options.sisterChanges,
575
617
  })
576
618
  : Promise.resolve(EMPTY_REPORT),
577
619
  validateArtifactSections(projectRoot, changeDir),
578
- validateTaskNumbering(projectRoot, changeDir),
620
+ validateTaskFiles(projectRoot, changeDir),
621
+ rootDeltaFileIssues(changeDir),
579
622
  ]);
580
- return mergeSectionIssues(deltaReport, [...sectionIssues, ...numberingIssues, ...markerIssues], strict);
623
+ return mergeSectionIssues(deltaReport, [...sectionIssues, ...taskFileIssues, ...rootDeltaIssues, ...markerIssues, ...metadataFieldIssues], strict);
624
+ }
625
+ /**
626
+ * WARNING per delta written at `specs/<capability>.md`, which the merge never
627
+ * reads. Outside the `requireDeltas` gate on purpose: archive warns about the
628
+ * same list whatever the schema or skip_specs says, and validate running it
629
+ * only when a delta was demanded passed an issue-schema or skip_specs change
630
+ * that archive then flagged. WARNING, not ERROR, because a root notes file that
631
+ * quotes a delta header is legitimate; `--strict` still refuses it.
632
+ *
633
+ * `specs/spec.md` is left to the delta pass, which runs whatever the schema
634
+ * demands.
635
+ */
636
+ async function rootDeltaFileIssues(changeDir) {
637
+ return (await findRootDeltaFiles(path.join(changeDir, 'specs'))).map((file) => {
638
+ const name = path.basename(file);
639
+ const capability = name.replace(/\.md$/i, '');
640
+ return {
641
+ level: 'WARNING',
642
+ path: name,
643
+ message: `specs/${name} holds delta sections, but a file at the specs/ root is ignored when the change is archived. Move it to specs/${capability}/spec.md.`,
644
+ };
645
+ });
646
+ }
647
+ /**
648
+ * `message` with trailing blanks dropped and ending in exactly one period.
649
+ * `trimEnd` strips exactly the `\s` set; the unanchored `/\.?\s*$/` it replaces
650
+ * retried from every position of an inner blank run, and a schema name holding
651
+ * 80k blanks took 2.8 s.
652
+ */
653
+ export function endWithPeriod(message) {
654
+ const trimmed = message.trimEnd();
655
+ return trimmed.endsWith('.') ? trimmed : `${trimmed}.`;
581
656
  }
582
657
  /**
583
658
  * The ERROR for a change whose schema cannot be resolved (`.tospec.yaml` names
@@ -586,18 +661,31 @@ export async function validateChangeArtifacts(projectRoot, changeDir, options =
586
661
  * is wrong with the change.
587
662
  */
588
663
  function schemaResolutionIssue(projectRoot, changeDir) {
589
- try {
590
- resolveSchema(resolveSchemaForChange(changeDir, undefined, projectRoot), projectRoot);
664
+ const fault = findChangeSchemaFault(changeDir, projectRoot);
665
+ if (!fault)
591
666
  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
- }
667
+ const { error } = fault;
668
+ const reason = endWithPeriod(error instanceof Error ? error.message : String(error));
669
+ // A bad field other than `schema` fails the same read, and its message
670
+ // already names the fix; telling the user to fix the schema name sent them
671
+ // to a line that was fine.
672
+ // A schema.yaml that does not read or parse is the same: the name found it,
673
+ // and the reason already names the file at fault.
674
+ // A name that came from tospec/config.yaml is not in .tospec.yaml at all, so
675
+ // the fix names the config field, in the words `new change` uses for it.
676
+ const fieldAtFault = error instanceof ChangeMetadataError && !(error instanceof UnknownChangeSchemaError);
677
+ const fix = fault.at === 'schema-file'
678
+ ? 'Fix that schema file before anything else in this change can be checked.'
679
+ : fault.at === 'config'
680
+ ? `Schema '${fault.schemaName}' comes from tospec/config.yaml; fix its 'schema:' field, or set 'schema:' in ${METADATA_FILENAME}, before anything else in this change can be checked.`
681
+ : fieldAtFault
682
+ ? `Nothing else in this change can be checked until ${METADATA_FILENAME} reads.`
683
+ : `Fix the schema name in ${METADATA_FILENAME} before anything else in this change can be checked.`;
684
+ return {
685
+ level: 'ERROR',
686
+ path: METADATA_FILENAME,
687
+ message: `${reason} ${fix}`,
688
+ };
601
689
  }
602
690
  /**
603
691
  * Errors in the skip_specs declaration itself: unparseable metadata, an unusable
@@ -607,7 +695,7 @@ function schemaResolutionIssue(projectRoot, changeDir) {
607
695
  * Most-fundamental first and one at a time: a file that does not parse cannot
608
696
  * also be said to hold a wrong value.
609
697
  */
610
- async function skipSpecsMarkerIssues(changeDir, marker) {
698
+ async function skipSpecsMarkerIssues(changeDir, marker, contradictingSpecs) {
611
699
  if (marker.unreadableReason) {
612
700
  return [
613
701
  {
@@ -622,27 +710,106 @@ async function skipSpecsMarkerIssues(changeDir, marker) {
622
710
  {
623
711
  level: 'ERROR',
624
712
  path: METADATA_FILENAME,
625
- message: `Invalid ${METADATA_FILENAME}: ${marker.invalidReason} Remove the key, or set it to true only when the change genuinely has no behavioral effect.`,
713
+ message: `Invalid ${METADATA_FILENAME}: ${marker.invalidReason} Remove the key, or set it to true only when the user confirms this change needs no delta spec.`,
626
714
  },
627
715
  ];
628
716
  }
629
- if (!marker.declared)
630
- return [];
631
- const specsDir = path.join(changeDir, 'specs');
632
- const specFiles = await findSpecFiles(specsDir);
717
+ // The same list `instructions apply` warns about. rootDeltaFileIssues covers
718
+ // the root `specs/<cap>.md` files it leaves out.
719
+ const specFiles = contradictingSpecs ?? (await findSkipSpecsContradictions(changeDir, marker));
633
720
  if (specFiles.length === 0)
634
721
  return [];
635
- const listed = specFiles
636
- .map((file) => FileSystemUtils.toPosixPath(path.relative(specsDir, file)))
637
- .join(', ');
638
722
  return [
639
723
  {
640
724
  level: 'ERROR',
641
725
  path: METADATA_FILENAME,
642
- message: `skip_specs: true declares this change has no delta specs, but ${specFiles.length} spec file(s) exist: ${listed}. Drop skip_specs, or delete the spec files — never invent a requirement just to satisfy validation.`,
726
+ message: `skip_specs: true declares this change has no delta specs, but ${specFiles.length} spec file(s) exist: ${specFiles.join(', ')}. Drop skip_specs, or delete the spec files only when the user confirms this change needs no delta spec — never invent a requirement just to satisfy validation.`,
643
727
  },
644
728
  ];
645
729
  }
730
+ /**
731
+ * WARNING per top-level `.tospec.yaml` key the schema does not know. Not an
732
+ * ERROR: an unknown key breaks nothing on its own, but what it was meant to
733
+ * carry is dropped, and `--strict` still refuses it.
734
+ */
735
+ function unknownMetadataKeyIssues(changeDir) {
736
+ return findUnknownMetadataKeys(changeDir).map((key) => {
737
+ const suggestion = suggestMetadataKey(key);
738
+ return {
739
+ level: 'WARNING',
740
+ path: METADATA_FILENAME,
741
+ message: `Unknown key "${printableKey(key)}" in ${METADATA_FILENAME} is ignored` +
742
+ (suggestion ? ` — did you mean "${suggestion}"?` : '.') +
743
+ ' Keep data a tool stores for itself under an `x-` prefixed key (e.g. `x-mytool`), which tospec leaves alone.',
744
+ };
745
+ });
746
+ }
747
+ /**
748
+ * WARNING per `decisions:` value that is not a decision record filename or
749
+ * names no file under `tospec/decisions/` — the rules `new change --decisions`
750
+ * refuses by, applied to a value added by hand, which is how an ADR recorded
751
+ * before the change gets linked. Without it, archive turned such a value into
752
+ * a dead Related Decisions link.
753
+ *
754
+ * WARNING, as for an unknown key, not ERROR: archive's own use of the field is
755
+ * best-effort and never blocks, and a dangling link loses a reference without
756
+ * breaking anything the merge writes. `--strict` still refuses it.
757
+ */
758
+ function decisionReferenceIssues(projectRoot, changeDir) {
759
+ let decisions;
760
+ try {
761
+ decisions = readChangeMetadata(changeDir, projectRoot)?.decisions;
762
+ }
763
+ catch {
764
+ return []; // schemaResolutionIssue reports a file that does not read
765
+ }
766
+ if (!decisions || decisions.length === 0)
767
+ return [];
768
+ const { notRecords, missing } = findDecisionReferenceProblems(projectRoot, decisions);
769
+ return [
770
+ ...notRecords.map((value) => ({
771
+ level: 'WARNING',
772
+ path: METADATA_FILENAME,
773
+ message: `decisions: "${printableKey(value)}" is not a decision record filename. List the bare <yyyyMMdd_HHmmss>-<topic>.md name under tospec/decisions/, without a directory part; \`tospec decision list\` shows them.`,
774
+ })),
775
+ ...missing.map((value) => ({
776
+ level: 'WARNING',
777
+ path: METADATA_FILENAME,
778
+ message: `decisions: "${printableKey(value)}" names no file in tospec/decisions/, so archive would link to nothing. Fix the name, or remove the entry.`,
779
+ })),
780
+ ];
781
+ }
782
+ /**
783
+ * A key as it can be shown in a message: the file is user-controlled, so a
784
+ * control character or escape sequence in a key must not reach the terminal.
785
+ */
786
+ function printableKey(key) {
787
+ const cleaned = key.replace(/[\u0000-\u001f\u007f-\u009f]/g, '?');
788
+ return cleaned.length > 80 ? `${cleaned.slice(0, 80)}…` : cleaned;
789
+ }
790
+ /**
791
+ * Whether this change must carry a delta spec, as validate decides it. The
792
+ * archive backstop asks too: it picks the files it refuses by the rule
793
+ * validate's ERRORs follow, and a `specs/spec.md` without a delta header is
794
+ * one of them only where a delta is required.
795
+ */
796
+ export async function changeRequiresDeltas(projectRoot, changeDir, known) {
797
+ // `known` is for a caller that already read both, as `instructions apply`
798
+ // does: optional rather than a second export, so the decision keeps one body.
799
+ const marker = known?.marker ?? readSkipSpecsMarker(changeDir);
800
+ return requiresDeltas(projectRoot, changeDir, marker, await skipSpecsMarkerIssues(changeDir, marker, known?.contradictingSpecs));
801
+ }
802
+ async function requiresDeltas(projectRoot, changeDir, marker, markerIssues) {
803
+ // A declared marker waives the delta requirement; a *contradicted* one
804
+ // (declared, yet delta files exist) must not, because those files are real.
805
+ //
806
+ // Unparseable metadata waives it for the opposite reason: whether skip_specs
807
+ // was declared is unknowable, so "add a delta or set skip_specs" dead-ends a
808
+ // user who already set it inside the malformed file.
809
+ return (!marker.unreadableReason &&
810
+ (!marker.declared || markerIssues.length > 0) &&
811
+ (await deltaSpecsRequired(projectRoot, changeDir)));
812
+ }
646
813
  /**
647
814
  * Whether a change's schema requires at least one delta spec — a property of the
648
815
  * schema, not of the change. False when the schema declares no `specs` artifact,
@@ -702,14 +869,26 @@ function describeTemplatePath(projectRoot, schemaDir, template) {
702
869
  */
703
870
  export function formatIssueLine(issue, indent = '') {
704
871
  const prefix = issueSymbol(issue.level);
705
- return `${indent}${prefix} [${issue.level}] ${issue.path}: ${issue.message}`;
872
+ // A message's later lines (a code frame, a scenario example) sit under its
873
+ // text: at the margin they read as the next finding.
874
+ return `${indent}${prefix} [${issue.level}] ${issue.path}: ${indentContinuation(issue.message, `${indent} `)}`;
706
875
  }
707
876
  function summarizeType(results, type) {
708
877
  const filtered = results.filter(r => r.type === type);
709
878
  const items = filtered.length;
710
879
  const passed = filtered.filter(r => r.valid).length;
711
880
  const failed = items - passed;
712
- return { items, passed, failed };
881
+ return { items, passed, failed, archiveRefusals: sumArchiveRefusals(filtered) };
882
+ }
883
+ /**
884
+ * Counted beside `passed` / `failed` because those cannot carry it: a refusal
885
+ * is a WARNING, so the item still passes and the exit code stays 0.
886
+ */
887
+ function countArchiveRefusals(issues) {
888
+ return issues.filter((issue) => issue.code === ARCHIVE_REFUSAL_CODE).length;
889
+ }
890
+ function sumArchiveRefusals(results) {
891
+ return results.reduce((sum, res) => sum + countArchiveRefusals(res.issues), 0);
713
892
  }
714
893
  function normalizeConcurrency(value) {
715
894
  if (!value)