@shrkcrft/cli 0.1.0-alpha.29 → 0.1.0-alpha.31

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 (510) hide show
  1. package/dist/asset-preview/apply-asset-preview.js +1 -1
  2. package/dist/authoring/authoring-kit.d.ts +9 -1
  3. package/dist/authoring/authoring-kit.d.ts.map +1 -1
  4. package/dist/authoring/authoring-kit.js +26 -13
  5. package/dist/bootstrap/unlinked-workspace-dependency.d.ts +11 -0
  6. package/dist/bootstrap/unlinked-workspace-dependency.d.ts.map +1 -0
  7. package/dist/bootstrap/unlinked-workspace-dependency.js +99 -0
  8. package/dist/command-registry.d.ts +100 -1
  9. package/dist/command-registry.d.ts.map +1 -1
  10. package/dist/command-registry.js +130 -3
  11. package/dist/commands/api-diff.command.d.ts.map +1 -1
  12. package/dist/commands/api-diff.command.js +12 -0
  13. package/dist/commands/apply.command.d.ts.map +1 -1
  14. package/dist/commands/apply.command.js +3 -0
  15. package/dist/commands/arch.command.d.ts.map +1 -1
  16. package/dist/commands/arch.command.js +26 -7
  17. package/dist/commands/architecture.command.d.ts.map +1 -1
  18. package/dist/commands/architecture.command.js +8 -2
  19. package/dist/commands/baseline.command.d.ts +76 -0
  20. package/dist/commands/baseline.command.d.ts.map +1 -1
  21. package/dist/commands/baseline.command.js +599 -75
  22. package/dist/commands/biome.command.d.ts.map +1 -1
  23. package/dist/commands/biome.command.js +20 -0
  24. package/dist/commands/boundaries.command.d.ts +9 -1
  25. package/dist/commands/boundaries.command.d.ts.map +1 -1
  26. package/dist/commands/boundaries.command.js +88 -20
  27. package/dist/commands/bundle.command.d.ts.map +1 -1
  28. package/dist/commands/bundle.command.js +57 -11
  29. package/dist/commands/cache-align.command.d.ts.map +1 -1
  30. package/dist/commands/cache-align.command.js +3 -1
  31. package/dist/commands/changelog-data.d.ts.map +1 -1
  32. package/dist/commands/changelog-data.js +164 -0
  33. package/dist/commands/changes.command.d.ts.map +1 -1
  34. package/dist/commands/changes.command.js +15 -0
  35. package/dist/commands/check.command.d.ts.map +1 -1
  36. package/dist/commands/check.command.js +1260 -324
  37. package/dist/commands/checks.command.d.ts.map +1 -1
  38. package/dist/commands/checks.command.js +188 -40
  39. package/dist/commands/ci.command.d.ts.map +1 -1
  40. package/dist/commands/ci.command.js +35 -2
  41. package/dist/commands/code-intel.command.d.ts.map +1 -1
  42. package/dist/commands/code-intel.command.js +8 -1
  43. package/dist/commands/command-catalog.d.ts +23 -1
  44. package/dist/commands/command-catalog.d.ts.map +1 -1
  45. package/dist/commands/command-catalog.js +152 -35
  46. package/dist/commands/commands.command.d.ts +21 -3
  47. package/dist/commands/commands.command.d.ts.map +1 -1
  48. package/dist/commands/commands.command.js +172 -27
  49. package/dist/commands/compress.command.d.ts.map +1 -1
  50. package/dist/commands/compress.command.js +4 -0
  51. package/dist/commands/constructs.command.d.ts.map +1 -1
  52. package/dist/commands/constructs.command.js +50 -12
  53. package/dist/commands/context.command.d.ts.map +1 -1
  54. package/dist/commands/context.command.js +40 -5
  55. package/dist/commands/contract-templates.command.d.ts.map +1 -1
  56. package/dist/commands/contract-templates.command.js +9 -1
  57. package/dist/commands/contract.command.d.ts.map +1 -1
  58. package/dist/commands/contract.command.js +35 -0
  59. package/dist/commands/conventions.command.d.ts.map +1 -1
  60. package/dist/commands/conventions.command.js +189 -29
  61. package/dist/commands/coverage.command.d.ts.map +1 -1
  62. package/dist/commands/coverage.command.js +9 -0
  63. package/dist/commands/daily.commands.d.ts +0 -1
  64. package/dist/commands/daily.commands.d.ts.map +1 -1
  65. package/dist/commands/daily.commands.js +12 -76
  66. package/dist/commands/dashboard.command.d.ts.map +1 -1
  67. package/dist/commands/dashboard.command.js +10 -0
  68. package/dist/commands/dev.command.d.ts.map +1 -1
  69. package/dist/commands/dev.command.js +45 -2
  70. package/dist/commands/diagnostics.command.d.ts +0 -2
  71. package/dist/commands/diagnostics.command.d.ts.map +1 -1
  72. package/dist/commands/diagnostics.command.js +5 -78
  73. package/dist/commands/diff-check.command.d.ts +6 -4
  74. package/dist/commands/diff-check.command.d.ts.map +1 -1
  75. package/dist/commands/diff-check.command.js +190 -103
  76. package/dist/commands/docs-references.command.d.ts +20 -0
  77. package/dist/commands/docs-references.command.d.ts.map +1 -0
  78. package/dist/commands/docs-references.command.js +520 -0
  79. package/dist/commands/doctor.command.d.ts.map +1 -1
  80. package/dist/commands/doctor.command.js +60 -5
  81. package/dist/commands/drift.command.d.ts.map +1 -1
  82. package/dist/commands/drift.command.js +17 -4
  83. package/dist/commands/eslint.command.d.ts.map +1 -1
  84. package/dist/commands/eslint.command.js +20 -0
  85. package/dist/commands/export.command.d.ts.map +1 -1
  86. package/dist/commands/export.command.js +23 -1
  87. package/dist/commands/feedback-dispatch.command.d.ts.map +1 -1
  88. package/dist/commands/feedback-dispatch.command.js +29 -0
  89. package/dist/commands/feedback.command.d.ts.map +1 -1
  90. package/dist/commands/feedback.command.js +6 -1
  91. package/dist/commands/finish.command.d.ts.map +1 -1
  92. package/dist/commands/finish.command.js +32 -8
  93. package/dist/commands/fix.command.d.ts.map +1 -1
  94. package/dist/commands/fix.command.js +19 -0
  95. package/dist/commands/gate.command.d.ts +6 -1
  96. package/dist/commands/gate.command.d.ts.map +1 -1
  97. package/dist/commands/gate.command.js +50 -124
  98. package/dist/commands/gates.command.d.ts +54 -0
  99. package/dist/commands/gates.command.d.ts.map +1 -1
  100. package/dist/commands/gates.command.js +1314 -64
  101. package/dist/commands/gen.command.d.ts.map +1 -1
  102. package/dist/commands/gen.command.js +4 -0
  103. package/dist/commands/generated.command.d.ts +62 -0
  104. package/dist/commands/generated.command.d.ts.map +1 -1
  105. package/dist/commands/generated.command.js +639 -156
  106. package/dist/commands/graph-code-subverbs.d.ts +15 -0
  107. package/dist/commands/graph-code-subverbs.d.ts.map +1 -1
  108. package/dist/commands/graph-code-subverbs.js +203 -25
  109. package/dist/commands/graph.command.d.ts.map +1 -1
  110. package/dist/commands/graph.command.js +79 -37
  111. package/dist/commands/help.command.d.ts +30 -6
  112. package/dist/commands/help.command.d.ts.map +1 -1
  113. package/dist/commands/help.command.js +323 -199
  114. package/dist/commands/helper.command.d.ts +1 -0
  115. package/dist/commands/helper.command.d.ts.map +1 -1
  116. package/dist/commands/helper.command.js +195 -56
  117. package/dist/commands/impact.command.d.ts.map +1 -1
  118. package/dist/commands/impact.command.js +115 -12
  119. package/dist/commands/infer.command.d.ts.map +1 -1
  120. package/dist/commands/infer.command.js +20 -45
  121. package/dist/commands/ingest.command.d.ts +0 -1
  122. package/dist/commands/ingest.command.d.ts.map +1 -1
  123. package/dist/commands/ingest.command.js +25 -41
  124. package/dist/commands/knowledge-author.command.d.ts +0 -3
  125. package/dist/commands/knowledge-author.command.d.ts.map +1 -1
  126. package/dist/commands/knowledge-author.command.js +10 -23
  127. package/dist/commands/knowledge.command.d.ts.map +1 -1
  128. package/dist/commands/knowledge.command.js +469 -129
  129. package/dist/commands/languages.command.d.ts.map +1 -1
  130. package/dist/commands/languages.command.js +27 -0
  131. package/dist/commands/lint.command.d.ts.map +1 -1
  132. package/dist/commands/lint.command.js +3 -0
  133. package/dist/commands/mcp.command.d.ts +18 -0
  134. package/dist/commands/mcp.command.d.ts.map +1 -1
  135. package/dist/commands/mcp.command.js +33 -8
  136. package/dist/commands/onboard.command.d.ts.map +1 -1
  137. package/dist/commands/onboard.command.js +20 -0
  138. package/dist/commands/owners.command.d.ts.map +1 -1
  139. package/dist/commands/owners.command.js +14 -2
  140. package/dist/commands/packs-new.d.ts +13 -1
  141. package/dist/commands/packs-new.d.ts.map +1 -1
  142. package/dist/commands/packs-new.js +500 -335
  143. package/dist/commands/packs.command.d.ts.map +1 -1
  144. package/dist/commands/packs.command.js +378 -36
  145. package/dist/commands/paths.command.d.ts.map +1 -1
  146. package/dist/commands/paths.command.js +9 -1
  147. package/dist/commands/pipelines.command.d.ts.map +1 -1
  148. package/dist/commands/pipelines.command.js +7 -1
  149. package/dist/commands/plan-check.command.d.ts.map +1 -1
  150. package/dist/commands/plan-check.command.js +3 -0
  151. package/dist/commands/plan.command.d.ts.map +1 -1
  152. package/dist/commands/plan.command.js +3 -0
  153. package/dist/commands/playbooks.command.d.ts.map +1 -1
  154. package/dist/commands/playbooks.command.js +7 -2
  155. package/dist/commands/policy-lint.command.d.ts +19 -2
  156. package/dist/commands/policy-lint.command.d.ts.map +1 -1
  157. package/dist/commands/policy-lint.command.js +224 -65
  158. package/dist/commands/policy.command.d.ts.map +1 -1
  159. package/dist/commands/policy.command.js +6 -1
  160. package/dist/commands/pr.command.d.ts.map +1 -1
  161. package/dist/commands/pr.command.js +14 -0
  162. package/dist/commands/presets.command.d.ts.map +1 -1
  163. package/dist/commands/presets.command.js +12 -27
  164. package/dist/commands/profiles.command.d.ts.map +1 -1
  165. package/dist/commands/profiles.command.js +94 -26
  166. package/dist/commands/provenance.command.js +1 -1
  167. package/dist/commands/quality.command.d.ts.map +1 -1
  168. package/dist/commands/quality.command.js +129 -33
  169. package/dist/commands/recommend.command.d.ts +20 -4
  170. package/dist/commands/recommend.command.d.ts.map +1 -1
  171. package/dist/commands/recommend.command.js +172 -278
  172. package/dist/commands/registrations.command.d.ts.map +1 -1
  173. package/dist/commands/registrations.command.js +138 -13
  174. package/dist/commands/registry-lifecycle-run.d.ts +14 -0
  175. package/dist/commands/registry-lifecycle-run.d.ts.map +1 -0
  176. package/dist/commands/registry-lifecycle-run.js +336 -0
  177. package/dist/commands/registry.command.d.ts.map +1 -1
  178. package/dist/commands/registry.command.js +235 -62
  179. package/dist/commands/release.command.d.ts.map +1 -1
  180. package/dist/commands/release.command.js +99 -20
  181. package/dist/commands/report.command.d.ts.map +1 -1
  182. package/dist/commands/report.command.js +38 -2
  183. package/dist/commands/reuse-coverage.command.d.ts +23 -0
  184. package/dist/commands/reuse-coverage.command.d.ts.map +1 -0
  185. package/dist/commands/reuse-coverage.command.js +536 -0
  186. package/dist/commands/reuse.command.d.ts +3 -18
  187. package/dist/commands/reuse.command.d.ts.map +1 -1
  188. package/dist/commands/reuse.command.js +395 -244
  189. package/dist/commands/review.command.d.ts.map +1 -1
  190. package/dist/commands/review.command.js +33 -1
  191. package/dist/commands/rounds.command.d.ts.map +1 -1
  192. package/dist/commands/rounds.command.js +5 -0
  193. package/dist/commands/safety.command.d.ts.map +1 -1
  194. package/dist/commands/safety.command.js +9 -0
  195. package/dist/commands/scaffolds.command.d.ts.map +1 -1
  196. package/dist/commands/scaffolds.command.js +97 -25
  197. package/dist/commands/search.command.d.ts +0 -8
  198. package/dist/commands/search.command.d.ts.map +1 -1
  199. package/dist/commands/search.command.js +136 -28
  200. package/dist/commands/self-config-xrefs.command.d.ts +24 -0
  201. package/dist/commands/self-config-xrefs.command.d.ts.map +1 -0
  202. package/dist/commands/self-config-xrefs.command.js +170 -0
  203. package/dist/commands/self-config.command.d.ts.map +1 -1
  204. package/dist/commands/self-config.command.js +261 -65
  205. package/dist/commands/smart-context.command.d.ts.map +1 -1
  206. package/dist/commands/smart-context.command.js +39 -3
  207. package/dist/commands/spec.command.d.ts.map +1 -1
  208. package/dist/commands/spec.command.js +3 -0
  209. package/dist/commands/stats.command.d.ts.map +1 -1
  210. package/dist/commands/stats.command.js +3 -0
  211. package/dist/commands/surface.command.d.ts +6 -1
  212. package/dist/commands/surface.command.d.ts.map +1 -1
  213. package/dist/commands/surface.command.js +256 -54
  214. package/dist/commands/task-context.command.js +1 -1
  215. package/dist/commands/task.command.d.ts.map +1 -1
  216. package/dist/commands/task.command.js +22 -1
  217. package/dist/commands/templates.command.d.ts +4 -0
  218. package/dist/commands/templates.command.d.ts.map +1 -1
  219. package/dist/commands/templates.command.js +109 -22
  220. package/dist/commands/test.command.d.ts.map +1 -1
  221. package/dist/commands/test.command.js +161 -33
  222. package/dist/commands/tests.command.d.ts.map +1 -1
  223. package/dist/commands/tests.command.js +29 -3
  224. package/dist/commands/trace.command.d.ts.map +1 -1
  225. package/dist/commands/trace.command.js +29 -4
  226. package/dist/commands/watch.command.d.ts.map +1 -1
  227. package/dist/commands/watch.command.js +3 -0
  228. package/dist/commands/why.command.d.ts.map +1 -1
  229. package/dist/commands/why.command.js +3 -0
  230. package/dist/commands/wiring.command.d.ts +18 -4
  231. package/dist/commands/wiring.command.d.ts.map +1 -1
  232. package/dist/commands/wiring.command.js +362 -74
  233. package/dist/dashboard/code-intelligence-data.js +4 -4
  234. package/dist/diff/deleted-orphan-coverage.d.ts +32 -0
  235. package/dist/diff/deleted-orphan-coverage.d.ts.map +1 -0
  236. package/dist/diff/deleted-orphan-coverage.js +107 -0
  237. package/dist/diff/deleted-orphan-scope-notes.d.ts +23 -0
  238. package/dist/diff/deleted-orphan-scope-notes.d.ts.map +1 -0
  239. package/dist/diff/deleted-orphan-scope-notes.js +49 -0
  240. package/dist/diff/deleted-orphans.d.ts +32 -0
  241. package/dist/diff/deleted-orphans.d.ts.map +1 -1
  242. package/dist/diff/deleted-orphans.js +42 -2
  243. package/dist/dispatch/closest-match.d.ts +15 -0
  244. package/dist/dispatch/closest-match.d.ts.map +1 -0
  245. package/dist/dispatch/closest-match.js +47 -0
  246. package/dist/dispatch/global-flags.d.ts +94 -0
  247. package/dist/dispatch/global-flags.d.ts.map +1 -0
  248. package/dist/dispatch/global-flags.js +123 -0
  249. package/dist/dispatch/guard-invocation-input.d.ts +24 -0
  250. package/dist/dispatch/guard-invocation-input.d.ts.map +1 -0
  251. package/dist/dispatch/guard-invocation-input.js +1 -0
  252. package/dist/dispatch/guard-invocation.d.ts +33 -0
  253. package/dist/dispatch/guard-invocation.d.ts.map +1 -0
  254. package/dist/dispatch/guard-invocation.js +183 -0
  255. package/dist/dispatch/help-intercept.d.ts +22 -0
  256. package/dist/dispatch/help-intercept.d.ts.map +1 -0
  257. package/dist/dispatch/help-intercept.js +42 -0
  258. package/dist/dispatch/i-declared-invocation.d.ts +17 -0
  259. package/dist/dispatch/i-declared-invocation.d.ts.map +1 -0
  260. package/dist/dispatch/i-declared-invocation.js +1 -0
  261. package/dist/dispatch/i-unknown-flag-refusal-input.d.ts +28 -0
  262. package/dist/dispatch/i-unknown-flag-refusal-input.d.ts.map +1 -0
  263. package/dist/dispatch/i-unknown-flag-refusal-input.js +1 -0
  264. package/dist/dispatch/invocation-rejection-kind.d.ts +15 -0
  265. package/dist/dispatch/invocation-rejection-kind.d.ts.map +1 -0
  266. package/dist/dispatch/invocation-rejection-kind.js +15 -0
  267. package/dist/dispatch/invocation-rejection.d.ts +18 -0
  268. package/dist/dispatch/invocation-rejection.d.ts.map +1 -0
  269. package/dist/dispatch/invocation-rejection.js +1 -0
  270. package/dist/dispatch/judge-invocation.d.ts +24 -0
  271. package/dist/dispatch/judge-invocation.d.ts.map +1 -0
  272. package/dist/dispatch/judge-invocation.js +50 -0
  273. package/dist/dispatch/positional-mode.d.ts +18 -0
  274. package/dist/dispatch/positional-mode.d.ts.map +1 -0
  275. package/dist/dispatch/positional-mode.js +18 -0
  276. package/dist/dispatch/read-tracking-map.d.ts +28 -0
  277. package/dist/dispatch/read-tracking-map.d.ts.map +1 -0
  278. package/dist/dispatch/read-tracking-map.js +58 -0
  279. package/dist/dispatch/refuse-sibling-valve-flags.d.ts +30 -0
  280. package/dist/dispatch/refuse-sibling-valve-flags.d.ts.map +1 -0
  281. package/dist/dispatch/refuse-sibling-valve-flags.js +65 -0
  282. package/dist/dispatch/subverb-spec.d.ts +26 -0
  283. package/dist/dispatch/subverb-spec.d.ts.map +1 -0
  284. package/dist/dispatch/subverb-spec.js +1 -0
  285. package/dist/dispatch/undocumented-flag-reads.d.ts +22 -0
  286. package/dist/dispatch/undocumented-flag-reads.d.ts.map +1 -0
  287. package/dist/dispatch/undocumented-flag-reads.js +283 -0
  288. package/dist/dispatch/unknown-flag-refusal-mode.d.ts +13 -0
  289. package/dist/dispatch/unknown-flag-refusal-mode.d.ts.map +1 -0
  290. package/dist/dispatch/unknown-flag-refusal-mode.js +13 -0
  291. package/dist/dispatch/unknown-flag-refusal.d.ts +22 -0
  292. package/dist/dispatch/unknown-flag-refusal.d.ts.map +1 -0
  293. package/dist/dispatch/unknown-flag-refusal.js +57 -0
  294. package/dist/dispatch/unread-flags.d.ts +152 -0
  295. package/dist/dispatch/unread-flags.d.ts.map +1 -0
  296. package/dist/dispatch/unread-flags.js +295 -0
  297. package/dist/dispatch/verdict-valve-flag.d.ts +21 -0
  298. package/dist/dispatch/verdict-valve-flag.d.ts.map +1 -0
  299. package/dist/dispatch/verdict-valve-flag.js +21 -0
  300. package/dist/dispatch/walk-declared-subverbs.d.ts +14 -0
  301. package/dist/dispatch/walk-declared-subverbs.d.ts.map +1 -0
  302. package/dist/dispatch/walk-declared-subverbs.js +28 -0
  303. package/dist/exit-codes.d.ts +41 -3
  304. package/dist/exit-codes.d.ts.map +1 -1
  305. package/dist/exit-codes.js +153 -14
  306. package/dist/finish/run-finish.d.ts +72 -5
  307. package/dist/finish/run-finish.d.ts.map +1 -1
  308. package/dist/finish/run-finish.js +453 -104
  309. package/dist/gates/accepted-empty-note.d.ts +10 -0
  310. package/dist/gates/accepted-empty-note.d.ts.map +1 -0
  311. package/dist/gates/accepted-empty-note.js +11 -0
  312. package/dist/gates/allow-empty.d.ts +25 -0
  313. package/dist/gates/allow-empty.d.ts.map +1 -0
  314. package/dist/gates/allow-empty.js +27 -0
  315. package/dist/gates/asset-doctor-failing-units.d.ts +16 -0
  316. package/dist/gates/asset-doctor-failing-units.d.ts.map +1 -0
  317. package/dist/gates/asset-doctor-failing-units.js +14 -0
  318. package/dist/gates/asset-doctor-failure-line.d.ts +16 -0
  319. package/dist/gates/asset-doctor-failure-line.d.ts.map +1 -0
  320. package/dist/gates/asset-doctor-failure-line.js +28 -0
  321. package/dist/gates/empty-rule-advice-lines.d.ts +15 -0
  322. package/dist/gates/empty-rule-advice-lines.d.ts.map +1 -0
  323. package/dist/gates/empty-rule-advice-lines.js +20 -0
  324. package/dist/gates/gate-envelope.d.ts +85 -8
  325. package/dist/gates/gate-envelope.d.ts.map +1 -1
  326. package/dist/gates/gate-envelope.js +56 -7
  327. package/dist/gates/gate-rule-globs.d.ts +67 -0
  328. package/dist/gates/gate-rule-globs.d.ts.map +1 -0
  329. package/dist/gates/gate-rule-globs.js +139 -0
  330. package/dist/gates/gate-rule-view.d.ts +25 -6
  331. package/dist/gates/gate-rule-view.d.ts.map +1 -1
  332. package/dist/gates/gate-rule-view.js +105 -77
  333. package/dist/gates/i-coverage-dead-glob.d.ts +19 -0
  334. package/dist/gates/i-coverage-dead-glob.d.ts.map +1 -0
  335. package/dist/gates/i-coverage-dead-glob.js +1 -0
  336. package/dist/gates/i-coverage-negation.d.ts +13 -0
  337. package/dist/gates/i-coverage-negation.d.ts.map +1 -0
  338. package/dist/gates/i-coverage-negation.js +1 -0
  339. package/dist/gates/i-registration-query-settle-input.d.ts +18 -0
  340. package/dist/gates/i-registration-query-settle-input.d.ts.map +1 -0
  341. package/dist/gates/i-registration-query-settle-input.js +1 -0
  342. package/dist/gates/i-unit-state-note-row.d.ts +16 -0
  343. package/dist/gates/i-unit-state-note-row.d.ts.map +1 -0
  344. package/dist/gates/i-unit-state-note-row.js +1 -0
  345. package/dist/gates/i-unit-state-notes.d.ts +32 -0
  346. package/dist/gates/i-unit-state-notes.d.ts.map +1 -0
  347. package/dist/gates/i-unit-state-notes.js +1 -0
  348. package/dist/gates/incomplete-registry-inventory.d.ts +19 -0
  349. package/dist/gates/incomplete-registry-inventory.d.ts.map +1 -0
  350. package/dist/gates/incomplete-registry-inventory.js +63 -0
  351. package/dist/gates/measure-registration-roles.d.ts +10 -0
  352. package/dist/gates/measure-registration-roles.d.ts.map +1 -0
  353. package/dist/gates/measure-registration-roles.js +9 -0
  354. package/dist/gates/plane-verdict.d.ts +14 -0
  355. package/dist/gates/plane-verdict.d.ts.map +1 -0
  356. package/dist/gates/plane-verdict.js +22 -0
  357. package/dist/gates/qualify-clean-for-units.d.ts +12 -0
  358. package/dist/gates/qualify-clean-for-units.d.ts.map +1 -0
  359. package/dist/gates/qualify-clean-for-units.js +18 -0
  360. package/dist/gates/registration-graph-verdict.d.ts +34 -0
  361. package/dist/gates/registration-graph-verdict.d.ts.map +1 -0
  362. package/dist/gates/registration-graph-verdict.js +70 -0
  363. package/dist/gates/registration-roles.d.ts +9 -0
  364. package/dist/gates/registration-roles.d.ts.map +1 -0
  365. package/dist/gates/registration-roles.js +1 -0
  366. package/dist/gates/registry-liveness.d.ts +11 -0
  367. package/dist/gates/registry-liveness.d.ts.map +1 -0
  368. package/dist/gates/registry-liveness.js +13 -0
  369. package/dist/gates/rule-coverage.d.ts +225 -4
  370. package/dist/gates/rule-coverage.d.ts.map +1 -1
  371. package/dist/gates/rule-coverage.js +610 -43
  372. package/dist/gates/run-gate-planes.d.ts +58 -0
  373. package/dist/gates/run-gate-planes.d.ts.map +1 -0
  374. package/dist/gates/run-gate-planes.js +403 -0
  375. package/dist/gates/scaffold-selftest.d.ts +56 -0
  376. package/dist/gates/scaffold-selftest.d.ts.map +1 -0
  377. package/dist/gates/scaffold-selftest.js +119 -0
  378. package/dist/gates/seam-rejected-rules.d.ts +28 -0
  379. package/dist/gates/seam-rejected-rules.d.ts.map +1 -0
  380. package/dist/gates/seam-rejected-rules.js +95 -0
  381. package/dist/gates/self-test-check.d.ts +29 -0
  382. package/dist/gates/self-test-check.d.ts.map +1 -0
  383. package/dist/gates/self-test-check.js +1 -0
  384. package/dist/gates/self-test-subject.d.ts +28 -0
  385. package/dist/gates/self-test-subject.d.ts.map +1 -0
  386. package/dist/gates/self-test-subject.js +1 -0
  387. package/dist/gates/self-test.d.ts +20 -0
  388. package/dist/gates/self-test.d.ts.map +1 -0
  389. package/dist/gates/self-test.js +82 -0
  390. package/dist/gates/settle-verdict.d.ts +14 -0
  391. package/dist/gates/settle-verdict.d.ts.map +1 -0
  392. package/dist/gates/settle-verdict.js +13 -0
  393. package/dist/gates/settled-verdict.d.ts +8 -0
  394. package/dist/gates/settled-verdict.d.ts.map +1 -0
  395. package/dist/gates/settled-verdict.js +1 -0
  396. package/dist/gates/template-registry-coverage.d.ts +18 -0
  397. package/dist/gates/template-registry-coverage.d.ts.map +1 -0
  398. package/dist/gates/template-registry-coverage.js +24 -0
  399. package/dist/gates/unit-state-notes.d.ts +40 -0
  400. package/dist/gates/unit-state-notes.d.ts.map +1 -0
  401. package/dist/gates/unit-state-notes.js +71 -0
  402. package/dist/gates/verdict-line.d.ts +18 -0
  403. package/dist/gates/verdict-line.d.ts.map +1 -0
  404. package/dist/gates/verdict-line.js +40 -0
  405. package/dist/graph/graph-reuse-lookup.d.ts +10 -0
  406. package/dist/graph/graph-reuse-lookup.d.ts.map +1 -0
  407. package/dist/graph/graph-reuse-lookup.js +41 -0
  408. package/dist/graph/index-behind-hint.d.ts +11 -0
  409. package/dist/graph/index-behind-hint.d.ts.map +1 -0
  410. package/dist/graph/index-behind-hint.js +27 -0
  411. package/dist/knowledge/knowledge-stale-gate-flags.d.ts +3 -0
  412. package/dist/knowledge/knowledge-stale-gate-flags.d.ts.map +1 -0
  413. package/dist/knowledge/knowledge-stale-gate-flags.js +1 -0
  414. package/dist/knowledge/knowledge-stale-gate-input.d.ts +3 -0
  415. package/dist/knowledge/knowledge-stale-gate-input.d.ts.map +1 -0
  416. package/dist/knowledge/knowledge-stale-gate-input.js +1 -0
  417. package/dist/knowledge/knowledge-stale-gate-result.d.ts +3 -0
  418. package/dist/knowledge/knowledge-stale-gate-result.d.ts.map +1 -0
  419. package/dist/knowledge/knowledge-stale-gate-result.js +1 -0
  420. package/dist/knowledge/knowledge-stale-gate.d.ts +10 -0
  421. package/dist/knowledge/knowledge-stale-gate.d.ts.map +1 -0
  422. package/dist/knowledge/knowledge-stale-gate.js +9 -0
  423. package/dist/main.d.ts.map +1 -1
  424. package/dist/main.js +204 -83
  425. package/dist/output/failure-hints.d.ts.map +1 -1
  426. package/dist/output/failure-hints.js +11 -9
  427. package/dist/output/rejected-entries-note.d.ts +57 -0
  428. package/dist/output/rejected-entries-note.d.ts.map +1 -0
  429. package/dist/output/rejected-entries-note.js +103 -0
  430. package/dist/output/stdout-is-pipe.d.ts +13 -0
  431. package/dist/output/stdout-is-pipe.d.ts.map +1 -0
  432. package/dist/output/stdout-is-pipe.js +21 -0
  433. package/dist/packs/stale-build-warning.d.ts +11 -0
  434. package/dist/packs/stale-build-warning.d.ts.map +1 -0
  435. package/dist/packs/stale-build-warning.js +23 -0
  436. package/dist/quality/run-quality.d.ts +123 -0
  437. package/dist/quality/run-quality.d.ts.map +1 -0
  438. package/dist/quality/run-quality.js +442 -0
  439. package/dist/schemas/json-schemas.d.ts +184 -3
  440. package/dist/schemas/json-schemas.d.ts.map +1 -1
  441. package/dist/schemas/json-schemas.js +113 -2
  442. package/dist/shrk.d.ts +3 -0
  443. package/dist/shrk.d.ts.map +1 -0
  444. package/dist/shrk.js +30 -0
  445. package/dist/surface/audience-applicability.d.ts +26 -0
  446. package/dist/surface/audience-applicability.d.ts.map +1 -0
  447. package/dist/surface/audience-applicability.js +60 -0
  448. package/dist/surface/catalog-command-safety.d.ts +18 -0
  449. package/dist/surface/catalog-command-safety.d.ts.map +1 -0
  450. package/dist/surface/catalog-command-safety.js +62 -0
  451. package/dist/surface/cli-command-resolver.d.ts +21 -0
  452. package/dist/surface/cli-command-resolver.d.ts.map +1 -0
  453. package/dist/surface/cli-command-resolver.js +87 -0
  454. package/dist/surface/command-dispatch-kind.d.ts +20 -0
  455. package/dist/surface/command-dispatch-kind.d.ts.map +1 -0
  456. package/dist/surface/command-dispatch-kind.js +20 -0
  457. package/dist/surface/command-index-entry.d.ts +40 -0
  458. package/dist/surface/command-index-entry.d.ts.map +1 -0
  459. package/dist/surface/command-index-entry.js +1 -0
  460. package/dist/surface/command-index.d.ts +77 -0
  461. package/dist/surface/command-index.d.ts.map +1 -0
  462. package/dist/surface/command-index.js +323 -0
  463. package/dist/surface/i-command-index.d.ts +22 -0
  464. package/dist/surface/i-command-index.d.ts.map +1 -0
  465. package/dist/surface/i-command-index.js +1 -0
  466. package/dist/surface/i-command-string-context.d.ts +20 -0
  467. package/dist/surface/i-command-string-context.d.ts.map +1 -0
  468. package/dist/surface/i-command-string-context.js +1 -0
  469. package/dist/surface/i-surface-denial.d.ts +13 -0
  470. package/dist/surface/i-surface-denial.d.ts.map +1 -0
  471. package/dist/surface/i-surface-denial.js +1 -0
  472. package/dist/surface/load-surface-context.d.ts +17 -1
  473. package/dist/surface/load-surface-context.d.ts.map +1 -1
  474. package/dist/surface/load-surface-context.js +48 -10
  475. package/dist/surface/not-enabled-error.d.ts +38 -4
  476. package/dist/surface/not-enabled-error.d.ts.map +1 -1
  477. package/dist/surface/not-enabled-error.js +96 -6
  478. package/dist/surface/profiles.d.ts +7 -0
  479. package/dist/surface/profiles.d.ts.map +1 -1
  480. package/dist/surface/resolve-command-string.d.ts +10 -0
  481. package/dist/surface/resolve-command-string.d.ts.map +1 -0
  482. package/dist/surface/resolve-command-string.js +572 -0
  483. package/dist/surface/surface-config-writer.d.ts +7 -2
  484. package/dist/surface/surface-config-writer.d.ts.map +1 -1
  485. package/dist/surface/surface-config-writer.js +25 -6
  486. package/dist/surface/surface-layer.d.ts +11 -0
  487. package/dist/surface/surface-layer.d.ts.map +1 -0
  488. package/dist/surface/surface-layer.js +11 -0
  489. package/dist/surface/surface-refusal-reason.d.ts +16 -0
  490. package/dist/surface/surface-refusal-reason.d.ts.map +1 -0
  491. package/dist/surface/surface-refusal-reason.js +16 -0
  492. package/dist/surface/surface-selector.d.ts +26 -0
  493. package/dist/surface/surface-selector.d.ts.map +1 -0
  494. package/dist/surface/surface-selector.js +42 -0
  495. package/dist/surface/surface-summary.d.ts +61 -18
  496. package/dist/surface/surface-summary.d.ts.map +1 -1
  497. package/dist/surface/surface-summary.js +147 -60
  498. package/dist/surface/tier.d.ts +85 -22
  499. package/dist/surface/tier.d.ts.map +1 -1
  500. package/dist/surface/tier.js +132 -30
  501. package/dist/usage/usage-log.d.ts +6 -1
  502. package/dist/usage/usage-log.d.ts.map +1 -1
  503. package/dist/usage/usage-log.js +7 -2
  504. package/dist/validation/run-validation-loop.d.ts +9 -0
  505. package/dist/validation/run-validation-loop.d.ts.map +1 -1
  506. package/dist/validation/run-validation-loop.js +14 -11
  507. package/dist/validation/typecheck-emitted.d.ts +8 -9
  508. package/dist/validation/typecheck-emitted.d.ts.map +1 -1
  509. package/dist/validation/typecheck-emitted.js +17 -97
  510. package/package.json +34 -34
@@ -7,7 +7,7 @@
7
7
  * shrk generated list # every declared artifact set
8
8
  * shrk generated check [--id X] # regen → temp dir → diff BOTH ways + headers
9
9
  * [--headers-only] # header contract only; never spawns
10
- * shrk generated update [--id X] # run regen in place (the bless step)
10
+ * shrk generated update [--id X] # regen → temp dir → write the ALIGNED files in place (bless)
11
11
  * shrk generated explain --id X # what it will run + what it currently sees
12
12
  *
13
13
  * The build compiles a hand-edited generated file perfectly happily; only
@@ -17,65 +17,43 @@
17
17
  * fully useful and never spawns, so packs can still ship it.
18
18
  */
19
19
  import { spawnSync } from 'node:child_process';
20
- import { mkdtempSync, readFileSync, readdirSync, rmSync, statSync } from 'node:fs';
20
+ import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
21
21
  import * as nodeOs from 'node:os';
22
22
  import * as nodePath from 'node:path';
23
- import { failsWhenEmpty } from '@shrkcrft/core';
24
- import { checkProvenanceHeaders, compareGeneratedTrees, scanGeneratedFiles, } from '@shrkcrft/boundaries';
23
+ import { failsWhenEmpty, formatEmptyRuleAdvice, normalizeRuleList, RuleEmptiness, settleRuleEmptiness, unitStateLists, UnitLivenessState, } from '@shrkcrft/core';
24
+ import { checkProvenanceHeaders, clearFileReadCache, compareGeneratedTrees, globListSelects, globListWalkGlobs, planeScanExcludeDirs, readGlobListLiveness, readRegenTree, readScopeCoverage, readScopeHasUnread, scanGeneratedFiles, } from '@shrkcrft/boundaries';
25
25
  import { resolveProjectConfig } from '@shrkcrft/inspector';
26
26
  import { flagBool, flagString, resolveCwd, } from "../command-registry.js";
27
27
  import { ExitCode } from "../exit-codes.js";
28
28
  import { asJson, header, kv } from "../output/format-output.js";
29
29
  import { buildGateEnvelope } from "../gates/gate-envelope.js";
30
+ import { verdictLine } from "../gates/verdict-line.js";
31
+ import { acceptedEmptyNote } from "../gates/accepted-empty-note.js";
32
+ import { emptyRuleAdviceLines } from "../gates/empty-rule-advice-lines.js";
33
+ import { qualifyCleanForUnits } from "../gates/qualify-clean-for-units.js";
34
+ import { unitStateNotes } from "../gates/unit-state-notes.js";
35
+ import { planeVerdictForExit } from "../gates/plane-verdict.js";
36
+ import { seamRejectedRules } from "../gates/seam-rejected-rules.js";
30
37
  const SCHEMA = 'sharkcraft.generated-drift/v1';
31
38
  const DEFAULT_TIMEOUT_MS = 120_000;
32
- /** Cap on the temp tree read — a runaway regen must not be read into memory whole. */
33
- const MAX_REGEN_FILE_BYTES = 2_000_000;
34
39
  async function loadRules(cwd) {
35
40
  const loaded = await resolveProjectConfig(cwd);
36
41
  if (!loaded.ok)
37
42
  return { ok: false, message: loaded.error.message };
38
- const rel = nodePath.relative(cwd, loaded.value.sharkcraftDir).split(nodePath.sep).join('/');
39
43
  return {
40
44
  ok: true,
41
45
  value: {
42
46
  rules: loaded.value.config.generatedArtifacts ?? [],
43
47
  planeDiagnostics: loaded.value.planeDiagnostics,
44
- excludeDirs: rel && !rel.startsWith('..') ? [rel] : [],
48
+ excludeDirs: planeScanExcludeDirs(cwd, loaded.value.sharkcraftDir),
49
+ rejected: seamRejectedRules(loaded.value, ['generated']),
45
50
  },
46
51
  };
47
52
  }
48
- /** Read a regenerated temp tree into path→content, relative to `root`. */
49
- function readTree(root) {
50
- const out = new Map();
51
- const visit = (abs) => {
52
- let entries;
53
- try {
54
- entries = readdirSync(abs, { withFileTypes: true });
55
- }
56
- catch {
57
- return;
58
- }
59
- for (const e of entries) {
60
- const child = nodePath.join(abs, e.name);
61
- if (e.isDirectory()) {
62
- visit(child);
63
- continue;
64
- }
65
- if (!e.isFile())
66
- continue;
67
- try {
68
- if (statSync(child).size > MAX_REGEN_FILE_BYTES)
69
- continue;
70
- out.set(nodePath.relative(root, child).split(nodePath.sep).join('/'), readFileSync(child, 'utf8'));
71
- }
72
- catch {
73
- // unreadable — skip
74
- }
75
- }
76
- };
77
- visit(root);
78
- return out;
53
+ /** ` ✗ <id> REJECTED — <why>` — a pack rule the merge seam refused (declared, never run). */
54
+ function writeRejected(rejected) {
55
+ for (const r of rejected)
56
+ process.stdout.write(` ✗ ${r.id} REJECTED ${r.error ?? 'failed validation'}\n`);
79
57
  }
80
58
  /** Number of trailing path SEGMENTS `a` and `b` share (0 when none). */
81
59
  function sharedSuffixSegments(a, b) {
@@ -97,17 +75,21 @@ function sharedSuffixSegments(a, b) {
97
75
  * files. Ties break lexically so the mapping is deterministic, each committed
98
76
  * path is claimed at most once, and anything unmatched keeps its temp-relative
99
77
  * key and surfaces as `only-regenerated` rather than disappearing.
78
+ *
79
+ * Returns temp path → key, so the outputs the reader read AND the ones it did
80
+ * not (over the regen cap) key onto the same committed path through ONE
81
+ * alignment.
100
82
  */
101
- function alignToCommitted(temp, committed) {
102
- const committedPaths = [...committed.keys()].sort();
83
+ function alignKeys(tempPaths, committedPaths) {
84
+ const targets = [...new Set(committedPaths)].sort();
103
85
  const claimed = new Set();
104
86
  const out = new Map();
105
87
  // Best-match first: a temp path with a longer shared suffix has the stronger
106
88
  // claim on a committed path, so resolve those before the weaker ones.
107
- const scored = [...temp.keys()]
89
+ const scored = [...tempPaths]
108
90
  .map((tempPath) => {
109
91
  let best;
110
- for (const c of committedPaths) {
92
+ for (const c of targets) {
111
93
  const score = sharedSuffixSegments(tempPath, c);
112
94
  if (score > 0 && (best === undefined || score > best.score))
113
95
  best = { path: c, score };
@@ -116,60 +98,184 @@ function alignToCommitted(temp, committed) {
116
98
  })
117
99
  .sort((a, b) => (b.best?.score ?? 0) - (a.best?.score ?? 0) || a.tempPath.localeCompare(b.tempPath));
118
100
  for (const { tempPath, best } of scored) {
119
- const content = temp.get(tempPath);
120
101
  if (best && !claimed.has(best.path)) {
121
102
  claimed.add(best.path);
122
- out.set(best.path, content);
103
+ out.set(tempPath, best.path);
123
104
  }
124
105
  else {
125
- out.set(tempPath, content);
106
+ out.set(tempPath, tempPath);
126
107
  }
127
108
  }
128
109
  return out;
129
110
  }
130
- /** Run `regen` into a fresh temp dir and read the result back. Always cleans up. */
131
- function runRegen(cwd, rule, committed) {
111
+ /**
112
+ * Run ONE regen command into a fresh temp dir and read the result back, keyed
113
+ * onto the committed paths it corresponds to. Always cleans up.
114
+ *
115
+ * `committed` is the slice this command OWNS, not the whole tree — that is what
116
+ * keeps a multi-writer tree honest: each command's output is aligned against
117
+ * (and later diffed against) only its own files, so writer A's output can never
118
+ * be reported as writer B's stale committed file.
119
+ */
120
+ function runOneRegen(cwd, command, committed, timeoutMs, unreadCommitted = []) {
132
121
  const tmp = mkdtempSync(nodePath.join(nodeOs.tmpdir(), 'shrk-generated-'));
133
122
  try {
134
- const command = rule.regen.split('{TMP}').join(tmp);
135
- const child = spawnSync(command, {
123
+ const child = spawnSync(command.split('{TMP}').join(tmp), {
136
124
  cwd,
137
125
  shell: true,
138
126
  encoding: 'utf8',
139
- timeout: rule.timeoutMs ?? DEFAULT_TIMEOUT_MS,
127
+ timeout: timeoutMs,
140
128
  maxBuffer: 16 * 1024 * 1024,
141
129
  });
130
+ // A spawned command may rewrite files anywhere: drop every memoised tree
131
+ // read, so a read-cache window (`withFileReadCache`, e.g. `shrk quality`'s
132
+ // plane section) never serves a pre-spawn snapshot after it.
133
+ clearFileReadCache();
142
134
  if (child.error)
143
135
  return { error: `regen failed to start: ${child.error.message}` };
144
136
  if (child.status !== 0) {
145
137
  const tail = String(child.stderr ?? '').trim().split('\n').slice(-3).join(' | ');
146
138
  return { error: `regen exited ${child.status ?? 'null'}${tail ? ` — ${tail}` : ''}` };
147
139
  }
148
- const tree = readTree(tmp);
149
- if (tree.size === 0) {
140
+ const tree = readRegenTree(tmp);
141
+ if (tree.files.size === 0 && tree.unread.length === 0) {
150
142
  return { error: 'regen wrote no files into {TMP} — the command probably ignores the output path' };
151
143
  }
152
- return { files: alignToCommitted(tree, committed) };
144
+ // ONE alignment for the read and the unread outputs, against the committed
145
+ // slice AND the committed files the reader could not read, so an over-cap
146
+ // output keys onto the same committed path a read one would.
147
+ const keys = alignKeys([...tree.files.keys(), ...tree.unread.map((u) => u.path)], [...committed.keys(), ...unreadCommitted]);
148
+ const files = new Map();
149
+ for (const [p, content] of tree.files)
150
+ files.set(keys.get(p) ?? p, content);
151
+ return { files, unread: tree.unread.map((u) => ({ ...u, path: keys.get(u.path) ?? u.path })) };
153
152
  }
154
153
  finally {
155
154
  rmSync(tmp, { recursive: true, force: true });
156
155
  }
157
156
  }
158
- function evaluateRule(cwd, rule, excludeDirs, headersOnly) {
157
+ /**
158
+ * The regen units for a rule: one per `sources[]` writer, or a single unit for
159
+ * the classic `regen` form. A rule declaring neither yields none (header-only).
160
+ */
161
+ function regenUnitsOf(rule, scan) {
162
+ const fallback = rule.timeoutMs ?? DEFAULT_TIMEOUT_MS;
163
+ if (scan.slices.length > 0) {
164
+ return scan.slices.map((slice) => ({
165
+ label: slice.label,
166
+ command: slice.regen,
167
+ committed: slice.files,
168
+ timeoutMs: slice.timeoutMs ?? fallback,
169
+ }));
170
+ }
171
+ if (rule.regen === undefined)
172
+ return [];
173
+ // Single-writer: the checkable set, so a hand-maintained bless inside the
174
+ // glob is not compared against output no generator produces.
175
+ return [{ label: rule.id, command: rule.regen, committed: scan.checkable, timeoutMs: fallback }];
176
+ }
177
+ export function evaluateGeneratedRule(cwd, authored, excludeDirs, headersOnly) {
178
+ // The engine entry normalises idempotently (round 13): a loaded rule comes
179
+ // back as the SAME object; a hand-built `{ pattern, expectEmpty }` entry is a
180
+ // glob plus a marker; a malformed entry is a misconfigured rule, never a crash.
181
+ const normalized = normalizeRuleList(authored, 'generatedGlob');
182
+ if (!normalized.ok) {
183
+ return {
184
+ rule: authored,
185
+ status: 'error',
186
+ committedCount: 0,
187
+ provenance: [],
188
+ driftChecked: false,
189
+ handMaintained: [],
190
+ writers: [],
191
+ error: `generatedGlob ${normalized.error.message}`,
192
+ };
193
+ }
194
+ const rule = normalized.value;
159
195
  const scan = scanGeneratedFiles(cwd, rule, excludeDirs);
160
196
  const severity = rule.severity ?? 'error';
161
- if (scan.generated.size === 0) {
162
- const failed = failsWhenEmpty(rule);
197
+ // A matched file the reader could not read (over the read cap) was never
198
+ // header-checked or compared. It rides on every outcome below, so the
199
+ // coverage names it; it is left out of the drift diff, where its absence
200
+ // from the committed side would otherwise read as `only-regenerated`.
201
+ const unreadScope = readScopeHasUnread(scan.readScope) ? { readScope: scan.readScope } : {};
202
+ const unreadPaths = new Set(scan.readScope.unread.map((u) => u.path));
203
+ // `generatedGlob` settled with its `expectEmpty` markers (round 13) — needed
204
+ // when a unit is marked (its acceptance / went-live line rides on the
205
+ // outcome) or when the rule matched nothing (what the empty rule is decided
206
+ // from). An unmarked rule that matched files pays nothing extra.
207
+ const live = (rule.expectEmptyUnits?.length ?? 0) > 0 || scan.generated.size === 0
208
+ ? readGlobListLiveness(cwd, 'generatedGlob', rule.generatedGlob, rule.expectEmptyUnits ?? [], {
209
+ subject: rule.id,
210
+ excludeDirs: new Set(excludeDirs),
211
+ })
212
+ : undefined;
213
+ const nonLive = live?.liveness.units.filter((u) => u.state !== UnitLivenessState.Live) ?? [];
214
+ const unitFields = {
215
+ ...(live?.liveness.acceptance !== undefined ? { unitAcceptance: live.liveness.acceptance } : {}),
216
+ ...(live !== undefined &&
217
+ live.liveness.dead.length + live.liveness.intendedEmpty.length + live.liveness.wentLive.length > 0
218
+ ? { units: unitStateLists(live.liveness) }
219
+ : {}),
220
+ ...(nonLive.length > 0 ? { unitLiveness: nonLive } : {}),
221
+ };
222
+ if (scan.generated.size === 0 && unreadPaths.size > 0) {
223
+ // Every committed file it matched is unread: not "0 files matched", so
224
+ // PARTIAL, never failOnEmpty's failure and never a pass.
163
225
  return {
164
226
  rule,
165
- status: failed ? 'failed' : 'skipped',
227
+ status: 'passed',
166
228
  committedCount: 0,
167
229
  provenance: [],
168
230
  driftChecked: false,
169
- skipReason: `0 files matched generatedGlob (${rule.generatedGlob.join(', ')})`,
231
+ handMaintained: [],
232
+ writers: [],
233
+ ...unreadScope,
234
+ ...unitFields,
170
235
  };
171
236
  }
172
- const headers = checkProvenanceHeaders(rule, scan.generated, scan.outside);
237
+ if (scan.generated.size === 0 && live !== undefined) {
238
+ // THE rule-emptiness settle (round 13, `settleRuleEmptiness`): every
239
+ // inclusion glob of `generatedGlob` marked `expectEmpty` and no file
240
+ // matched → a generated tree that does not exist yet, accepted and printed.
241
+ // Anything else is the loud skip (failOnEmpty's 1, else 2).
242
+ const noFiles = `0 files matched generatedGlob (${rule.generatedGlob.join(', ')})`;
243
+ const emptiness = settleRuleEmptiness({
244
+ subject: rule.id,
245
+ unitLabel: 'generated files',
246
+ filesMatched: 0,
247
+ unitsMatched: 0,
248
+ unread: false,
249
+ liveness: live.liveness,
250
+ primaryLists: ['generatedGlob'],
251
+ failOnEmpty: failsWhenEmpty(rule),
252
+ noFilesReason: noFiles,
253
+ noUnitsReason: noFiles,
254
+ });
255
+ const empty = {
256
+ rule,
257
+ committedCount: 0,
258
+ provenance: [],
259
+ driftChecked: false,
260
+ handMaintained: [],
261
+ writers: [],
262
+ ...unitFields,
263
+ };
264
+ if (emptiness.state === RuleEmptiness.IntendedEmpty && emptiness.coverage !== undefined) {
265
+ return { ...empty, status: 'passed', emptyCoverage: emptiness.coverage };
266
+ }
267
+ return {
268
+ ...empty,
269
+ status: emptiness.fails ? 'failed' : 'skipped',
270
+ skipReason: emptiness.skipReason ?? noFiles,
271
+ };
272
+ }
273
+ // Headers + byte checks run over the CHECKABLE set — a hand-maintained bless
274
+ // is excluded from both, which is the whole point of declaring it.
275
+ const headers = checkProvenanceHeaders(rule, scan.checkable, scan.outside, {
276
+ unclassified: scan.unclassified,
277
+ staleHandMaintained: scan.staleHandMaintained,
278
+ });
173
279
  if (headers.error) {
174
280
  return {
175
281
  rule,
@@ -177,26 +283,74 @@ function evaluateRule(cwd, rule, excludeDirs, headersOnly) {
177
283
  committedCount: scan.generated.size,
178
284
  provenance: [],
179
285
  driftChecked: false,
286
+ handMaintained: scan.handMaintained,
287
+ writers: [],
180
288
  error: headers.error,
289
+ ...unreadScope,
181
290
  };
182
291
  }
183
- let treeDiff;
184
- let error;
185
- const wantDrift = !headersOnly && rule.regen !== undefined;
186
- if (wantDrift) {
187
- const regen = runRegen(cwd, rule, scan.generated);
188
- if (regen.error)
189
- error = regen.error;
190
- else
191
- treeDiff = compareGeneratedTrees(scan.generated, regen.files, rule.compare ?? 'bytes');
292
+ const units = headersOnly ? [] : regenUnitsOf(rule, scan);
293
+ const differences = [];
294
+ const writers = [];
295
+ const errors = [];
296
+ // Regen outputs the reader did not read (over the regen cap) that key onto a
297
+ // COMMITTED file: never byte-compared, so they join the rule's read scope
298
+ // and its coverage names them. Never dropped.
299
+ const uncomparable = [];
300
+ let committedCompared = 0;
301
+ let regeneratedCount = 0;
302
+ for (const unit of units) {
303
+ const regen = runOneRegen(cwd, unit.command, unit.committed, unit.timeoutMs, [...unreadPaths]);
304
+ if (regen.error) {
305
+ errors.push(units.length > 1 ? `${unit.label}: ${regen.error}` : regen.error);
306
+ writers.push({ label: unit.label, committedCount: unit.committed.size, differences: 0, error: regen.error });
307
+ continue;
308
+ }
309
+ const regenUnread = regen.unread ?? [];
310
+ const keyedOnCommitted = (p) => unit.committed.has(p) || unreadPaths.has(p);
311
+ const unitUncomparable = regenUnread.filter((u) => keyedOnCommitted(u.path));
312
+ uncomparable.push(...unitUncomparable);
313
+ const skip = new Set([...unreadPaths, ...unitUncomparable.map((u) => u.path)]);
314
+ const diff = compareGeneratedTrees(unit.committed, regen.files, rule.compare ?? 'bytes');
315
+ // A path whose committed or regenerated side was never read cannot be
316
+ // compared: it is in the coverage gap, not the diff. An unread output
317
+ // keyed onto NOTHING committed is drift its path alone proves.
318
+ const compared = [
319
+ ...diff.differences.filter((d) => !skip.has(d.file)),
320
+ ...regenUnread
321
+ .filter((u) => !keyedOnCommitted(u.path))
322
+ .map((u) => ({ file: u.path, kind: 'only-regenerated' })),
323
+ ];
324
+ differences.push(...compared);
325
+ committedCompared += diff.committedCount - unitUncomparable.filter((u) => unit.committed.has(u.path)).length;
326
+ regeneratedCount += diff.regeneratedCount + regenUnread.length;
327
+ writers.push({ label: unit.label, committedCount: unit.committed.size, differences: compared.length });
192
328
  }
329
+ const ranAnyWriter = units.length > 0 && errors.length < units.length;
330
+ const treeDiff = ranAnyWriter
331
+ ? { differences, committedCount: committedCompared, regeneratedCount }
332
+ : undefined;
333
+ const error = errors.length > 0 ? errors.join(' · ') : undefined;
193
334
  const hardFindings = headers.findings.filter((f) => f.severity === 'error');
194
- const drifted = (treeDiff?.differences.length ?? 0) > 0;
335
+ const drifted = differences.length > 0;
336
+ // A writer that failed to RUN proved nothing, so the rule is in error even if
337
+ // its siblings were clean — a partial regen must never read as a full pass.
195
338
  const status = error !== undefined
196
339
  ? 'error'
197
340
  : drifted || (hardFindings.length > 0 && severity === 'error')
198
341
  ? 'failed'
199
342
  : 'passed';
343
+ // The rule's read scope: the committed files the reader could not read, plus
344
+ // each uncomparable regen output keyed onto a READ committed file (read, but
345
+ // never compared, so it moves from examined to unexamined).
346
+ const extra = new Map();
347
+ for (const u of uncomparable)
348
+ if (!unreadPaths.has(u.path) && !extra.has(u.path))
349
+ extra.set(u.path, u);
350
+ const finalScope = {
351
+ read: scan.readScope.read - extra.size,
352
+ unread: [...scan.readScope.unread, ...extra.values()].sort((a, b) => a.path.localeCompare(b.path)),
353
+ };
200
354
  return {
201
355
  rule,
202
356
  status,
@@ -204,12 +358,59 @@ function evaluateRule(cwd, rule, excludeDirs, headersOnly) {
204
358
  ...(treeDiff ? { treeDiff } : {}),
205
359
  provenance: headers.findings,
206
360
  ...(error ? { error } : {}),
207
- driftChecked: wantDrift && error === undefined,
361
+ driftChecked: units.length > 0 && errors.length === 0,
362
+ handMaintained: scan.handMaintained,
363
+ writers: writers.length > 1 ? writers : [],
364
+ ...(readScopeHasUnread(finalScope) ? { readScope: finalScope } : {}),
365
+ // The acceptance of a marked planned glob and the unit lines ride on a
366
+ // connected rule too (round 13), not only on an empty one.
367
+ ...unitFields,
208
368
  };
209
369
  }
210
- function hintFor(rule) {
370
+ /**
371
+ * What one generated-artifact rule examined — shared by `generated check` and
372
+ * the aggregate (`gates check`, `quality`), so both report the same scope.
373
+ *
374
+ * `driftSkipped` is the aggregate's `--no-spawn` case: headers and
375
+ * classification ran, the regen diff did not, so no file's CONTENTS were
376
+ * verified. (`generated check --headers-only` is a narrowing the caller asked
377
+ * for, not a gap.)
378
+ */
379
+ export function generatedCoverage(o, driftSkipped = false) {
380
+ // An intended-empty rule (round 13: every `generatedGlob` inclusion glob
381
+ // marked `expectEmpty`, no file matched) is covered by its acceptance — the
382
+ // settle's own record, printed at exit 0.
383
+ if (o.emptyCoverage !== undefined)
384
+ return o.emptyCoverage;
385
+ // A file in scope the reader could not read folds in through the one rule
386
+ // every plane uses (`readScopeCoverage`): `examined N of M files`, named.
387
+ return readScopeCoverage(planeGeneratedCoverage(o, driftSkipped), o.readScope);
388
+ }
389
+ function planeGeneratedCoverage(o, driftSkipped) {
390
+ if (o.skipReason !== undefined) {
391
+ return { unit: 'generated files', expected: 0, examined: 0, reason: o.skipReason };
392
+ }
393
+ if (o.status === 'error') {
394
+ return {
395
+ unit: 'generated files',
396
+ expected: o.committedCount,
397
+ examined: 0,
398
+ reason: 'the rule could not be evaluated',
399
+ };
400
+ }
401
+ if (driftSkipped) {
402
+ return {
403
+ unit: 'generated files',
404
+ expected: o.committedCount,
405
+ examined: 0,
406
+ reason: 'regen drift check skipped by --no-spawn',
407
+ };
408
+ }
409
+ return { unit: 'generated files', expected: o.committedCount, examined: o.committedCount };
410
+ }
411
+ export function generatedHintFor(rule) {
211
412
  return (rule.hint ??
212
- (rule.regen
413
+ (rule.regen || rule.sources
213
414
  ? `regenerate with \`shrk generated update --id ${rule.id}\` and commit the result`
214
415
  : 'add the provenance header to the generated file, or move it out of the generated glob'));
215
416
  }
@@ -221,14 +422,18 @@ function outcomeJson(o) {
221
422
  severity: o.rule.severity ?? 'error',
222
423
  committedCount: o.committedCount,
223
424
  driftChecked: o.driftChecked,
425
+ handMaintained: o.handMaintained,
426
+ ...(o.writers.length > 0 ? { writers: o.writers } : {}),
224
427
  ...(o.treeDiff ? { differences: o.treeDiff.differences, regeneratedCount: o.treeDiff.regeneratedCount } : {}),
225
428
  provenance: o.provenance,
226
429
  ...(o.error ? { error: o.error } : {}),
227
430
  ...(o.skipReason ? { skipReason: o.skipReason } : {}),
228
- hint: hintFor(o.rule),
431
+ hint: generatedHintFor(o.rule),
229
432
  };
230
433
  }
231
- async function prepare(args) {
434
+ async function prepare(args,
435
+ /** `rejectedKnown`: a verdict verb may select a rejected rule by `--id` (its errored row). */
436
+ opts = {}) {
232
437
  const cwd = resolveCwd(args);
233
438
  const json = flagBool(args, 'json');
234
439
  const loaded = await loadRules(cwd);
@@ -241,15 +446,20 @@ async function prepare(args) {
241
446
  }
242
447
  const id = flagString(args, 'id');
243
448
  let rules = loaded.value.rules;
449
+ let rejected = loaded.value.rejected;
244
450
  if (id) {
245
451
  const wanted = id.split(',').map((s) => s.trim()).filter(Boolean);
246
- const known = new Set(rules.map((r) => r.id));
452
+ const known = new Set([
453
+ ...rules.map((r) => r.id),
454
+ ...(opts.rejectedKnown === true ? rejected.map((r) => r.id) : []),
455
+ ]);
247
456
  const unknown = wanted.filter((w) => !known.has(w));
248
457
  if (unknown.length > 0) {
249
458
  process.stderr.write(`Unknown generated-artifact id(s): ${unknown.join(', ')}. Declared: ${[...known].join(', ') || '(none)'}\n`);
250
459
  return { ok: false, code: ExitCode.UsageError };
251
460
  }
252
461
  rules = rules.filter((r) => wanted.includes(r.id));
462
+ rejected = rejected.filter((r) => wanted.includes(r.id));
253
463
  }
254
464
  return {
255
465
  ok: true,
@@ -258,18 +468,47 @@ async function prepare(args) {
258
468
  all: loaded.value.rules,
259
469
  excludeDirs: loaded.value.excludeDirs,
260
470
  planeDiagnostics: loaded.value.planeDiagnostics,
471
+ rejected,
261
472
  };
262
473
  }
263
- function writeNoRules(json) {
474
+ /**
475
+ * The "no rules declared" landing. A VERDICT verb passes its name so its JSON
476
+ * still carries the settled `gate` envelope — nothing declared is `2`.
477
+ */
478
+ function writeNoRules(json, verb) {
479
+ // A VERDICT verb settles first and renders second, in text AND JSON: nothing
480
+ // declared proposes 0 and the run coverage (expected 0) settles it to 2. The
481
+ // exit comes from the envelope — never a hard-coded code — so text, JSON and
482
+ // gate.exit cannot disagree. List / explain subverbs stay informational.
483
+ const gate = verb !== undefined
484
+ ? buildGateEnvelope(verb, ExitCode.VerifiedPass, [], {
485
+ unit: 'generated-artifact rules',
486
+ expected: 0,
487
+ examined: 0,
488
+ reason: 'no generatedArtifacts[] declared',
489
+ })
490
+ : undefined;
491
+ const exit = gate?.exit ?? ExitCode.NotVerified;
264
492
  if (json) {
265
- process.stdout.write(asJson({ schema: SCHEMA, results: [], evaluated: 0, verdict: 'not-verified' }) + '\n');
266
- return ExitCode.NotVerified;
493
+ process.stdout.write(asJson({
494
+ schema: SCHEMA,
495
+ results: [],
496
+ evaluated: 0,
497
+ verdict: gate ? planeVerdictForExit(gate.exit) : 'not-verified',
498
+ ...(gate ? { exitCode: gate.exit, gate } : {}),
499
+ }) + '\n');
500
+ return exit;
267
501
  }
268
502
  process.stdout.write(header('Generated artifacts'));
269
503
  process.stdout.write(' No generated-artifact rules declared. Add `generatedArtifacts[]` to\n' +
270
504
  ' sharkcraft.config.ts to catch hand-edited generated files and missing\n' +
271
505
  ' "do not edit" headers (see docs/generated-drift.md).\n');
272
- return ExitCode.NotVerified;
506
+ if (gate) {
507
+ const line = verdictLine(gate, 'Nothing declared — accepted.');
508
+ if (line)
509
+ process.stdout.write(`\n${line}\n`);
510
+ }
511
+ return exit;
273
512
  }
274
513
  export const generatedListCommand = {
275
514
  name: 'list',
@@ -281,19 +520,26 @@ export const generatedListCommand = {
281
520
  if (!prep.ok)
282
521
  return prep.code;
283
522
  const json = flagBool(args, 'json');
284
- if (prep.all.length === 0)
523
+ if (prep.all.length === 0 && prep.rejected.length === 0)
285
524
  return writeNoRules(json);
286
525
  if (json) {
287
526
  process.stdout.write(asJson({
288
527
  schema: SCHEMA,
528
+ // Declared by a pack, refused by the merge seam: never checked.
529
+ rejected: prep.rejected.map((r) => ({ id: r.id, error: r.error ?? null })),
289
530
  rules: prep.all.map((r) => ({
290
531
  id: r.id,
291
532
  description: r.description ?? null,
292
533
  generatedGlob: r.generatedGlob,
293
534
  regen: r.regen ?? null,
535
+ sources: r.sources ?? null,
536
+ handMaintained: r.handMaintained ?? null,
537
+ handMaintainedMarker: r.handMaintainedMarker ?? null,
294
538
  compare: r.compare ?? 'bytes',
295
539
  provenanceHeader: r.provenanceHeader ?? null,
296
- failOnEmpty: r.failOnEmpty === true,
540
+ // The EFFECTIVE failOnEmpty (round 13) — THE authority, never the
541
+ // raw field: an error rule fails on empty by default.
542
+ failOnEmpty: failsWhenEmpty(r),
297
543
  })),
298
544
  diagnostics: prep.planeDiagnostics,
299
545
  }) + '\n');
@@ -303,13 +549,32 @@ export const generatedListCommand = {
303
549
  for (const r of prep.all) {
304
550
  process.stdout.write(` • ${r.id}\n`);
305
551
  process.stdout.write(` glob ${r.generatedGlob.join(', ')}\n`);
306
- process.stdout.write(` regen ${r.regen ?? '(header-only never spawns)'}\n`);
552
+ if (r.sources && r.sources.length > 0) {
553
+ process.stdout.write(` regen ${r.sources.length} writer(s):\n`);
554
+ r.sources.forEach((src, i) => {
555
+ process.stdout.write(` [${src.id ?? i}] ${src.regen}\n`);
556
+ process.stdout.write(` owns ${src.glob.join(', ')}\n`);
557
+ });
558
+ }
559
+ else {
560
+ process.stdout.write(` regen ${r.regen ?? '(header-only — never spawns)'}\n`);
561
+ }
562
+ if (r.handMaintained && r.handMaintained.length > 0) {
563
+ process.stdout.write(` hand ${r.handMaintained.join(', ')} (exempt from header + byte checks)\n`);
564
+ }
565
+ if (r.handMaintainedMarker) {
566
+ process.stdout.write(` marker /${r.handMaintainedMarker}/ (in-file bless)\n`);
567
+ }
307
568
  if (r.provenanceHeader) {
308
569
  process.stdout.write(` header /${r.provenanceHeader.mustMatch}/${r.provenanceHeader.forbidOutside ? ' + mislabel check' : ''}\n`);
309
570
  }
310
571
  if (r.description)
311
572
  process.stdout.write(` ${r.description}\n`);
312
573
  }
574
+ if (prep.rejected.length > 0) {
575
+ process.stdout.write(`\n rejected at the pack-plane merge seam — never checked (${prep.rejected.length}):\n`);
576
+ writeRejected(prep.rejected);
577
+ }
313
578
  for (const d of prep.planeDiagnostics)
314
579
  process.stdout.write(` ! ${d}\n`);
315
580
  return ExitCode.VerifiedPass;
@@ -321,63 +586,92 @@ export const generatedCheckCommand = {
321
586
  usage: 'shrk generated check [--id <ids>] [--headers-only] [--json]',
322
587
  booleanFlags: new Set(['json', 'headers-only']),
323
588
  async run(args) {
324
- const prep = await prepare(args);
589
+ const prep = await prepare(args, { rejectedKnown: true });
325
590
  if (!prep.ok)
326
591
  return prep.code;
327
592
  const json = flagBool(args, 'json');
328
593
  const headersOnly = flagBool(args, 'headers-only');
329
- if (prep.rules.length === 0)
330
- return writeNoRules(json);
331
- const outcomes = prep.rules.map((r) => evaluateRule(prep.cwd, r, prep.excludeDirs, headersOnly));
594
+ // A pack rule the merge seam rejected is a declared rule that was never
595
+ // checked — an errored row, exit 1 (round 12 review, R12-X1).
596
+ const rejected = prep.rejected;
597
+ if (prep.rules.length === 0 && rejected.length === 0)
598
+ return writeNoRules(json, 'generated check');
599
+ const outcomes = prep.rules.map((r) => evaluateGeneratedRule(prep.cwd, r, prep.excludeDirs, headersOnly));
332
600
  const failed = outcomes.filter((o) => o.status === 'failed' || (o.status === 'error' && (o.rule.severity ?? 'error') === 'error'));
333
601
  const evaluated = outcomes.filter((o) => o.status !== 'skipped').length;
334
602
  const skippedCount = outcomes.length - evaluated;
335
- const exit = failed.length > 0
603
+ const proposed = failed.length > 0 || rejected.length > 0
336
604
  ? ExitCode.Failure
337
605
  : evaluated === 0 || skippedCount > 0
338
606
  ? ExitCode.NotVerified
339
607
  : ExitCode.VerifiedPass;
608
+ // Settle first, render second: one envelope for text AND JSON, and the ✓
609
+ // line is printed only from the settled verdict. `--headers-only` is a
610
+ // narrowing the caller asked for, so it is not a coverage gap here.
611
+ const unexamined = [
612
+ ...outcomes.filter((o) => o.status === 'skipped' || o.status === 'error').map((o) => o.rule.id),
613
+ ...rejected.map((r) => r.id),
614
+ ];
615
+ const env = buildGateEnvelope('generated check', proposed, [...outcomes.map((o) => ({
616
+ id: o.rule.id,
617
+ type: 'generated',
618
+ status: o.status,
619
+ severity: o.rule.severity ?? 'error',
620
+ counts: {
621
+ files: o.committedCount,
622
+ differences: o.treeDiff?.differences.length ?? 0,
623
+ provenance: o.provenance.length,
624
+ },
625
+ violations: [
626
+ ...(o.treeDiff?.differences ?? []).map((d) => ({
627
+ id: d.file,
628
+ file: d.file,
629
+ message: d.kind,
630
+ hint: generatedHintFor(o.rule),
631
+ })),
632
+ ...o.provenance.map((f) => ({
633
+ id: f.file,
634
+ file: f.file,
635
+ message: f.message,
636
+ hint: generatedHintFor(o.rule),
637
+ })),
638
+ ],
639
+ ...(o.skipReason ? { skipReason: o.skipReason } : {}),
640
+ ...(o.error ? { error: o.error } : {}),
641
+ coverage: generatedCoverage(o),
642
+ // The rule's `expectEmpty` acceptance and unit lines (round 13), folded
643
+ // into the envelope's one settle.
644
+ ...(o.unitAcceptance !== undefined ? { unitAcceptance: o.unitAcceptance } : {}),
645
+ ...(o.units !== undefined ? { units: o.units } : {}),
646
+ })), ...rejected], {
647
+ unit: 'generated-artifact rules',
648
+ expected: outcomes.length + rejected.length,
649
+ examined: outcomes.length + rejected.length - unexamined.length,
650
+ ...(unexamined.length > 0 ? { unexamined, reason: 'checked nothing or could not run' } : {}),
651
+ });
652
+ const exit = env.exit;
340
653
  if (json) {
341
654
  process.stdout.write(asJson({
342
655
  schema: SCHEMA,
343
656
  headersOnly,
344
657
  results: outcomes.map(outcomeJson),
658
+ // Pack rules the merge seam refused — errored rows in `gate.rules`.
659
+ rejected: rejected.map((r) => ({ id: r.id, error: r.error ?? null })),
345
660
  evaluated,
346
661
  skipped: outcomes.filter((o) => o.status === 'skipped').length,
347
- verdict: failed.length > 0 ? 'errors' : evaluated === 0 ? 'not-verified' : 'pass',
662
+ verdict: exit === ExitCode.Failure ? 'errors' : exit === ExitCode.VerifiedPass ? 'pass' : 'not-verified',
348
663
  diagnostics: prep.planeDiagnostics,
349
- gate: buildGateEnvelope('generated check', exit, outcomes.map((o) => ({
350
- id: o.rule.id,
351
- type: 'generated',
352
- status: o.status,
353
- severity: o.rule.severity ?? 'error',
354
- counts: {
355
- files: o.committedCount,
356
- differences: o.treeDiff?.differences.length ?? 0,
357
- provenance: o.provenance.length,
358
- },
359
- violations: [
360
- ...(o.treeDiff?.differences ?? []).map((d) => ({
361
- id: d.file,
362
- file: d.file,
363
- message: d.kind,
364
- hint: hintFor(o.rule),
365
- })),
366
- ...o.provenance.map((f) => ({
367
- id: f.file,
368
- file: f.file,
369
- message: f.message,
370
- hint: hintFor(o.rule),
371
- })),
372
- ],
373
- ...(o.skipReason ? { skipReason: o.skipReason } : {}),
374
- ...(o.error ? { error: o.error } : {}),
375
- }))),
664
+ exitCode: exit,
665
+ gate: env,
376
666
  }) + '\n');
377
667
  return exit;
378
668
  }
379
669
  process.stdout.write(header('Generated-artifact drift'));
380
- process.stdout.write(kv('evaluated', `${evaluated} of ${prep.rules.length}`) + '\n');
670
+ // Round 13 (K6): the printed count is the envelope's (`gate.evaluated`), which
671
+ // never counts a rule accepted as intended-empty — it is named apart.
672
+ process.stdout.write(kv('evaluated', `${env.evaluated} of ${prep.rules.length + rejected.length}${acceptedEmptyNote(env.acceptedEmpty)}` +
673
+ (rejected.length > 0 ? ` (${rejected.length} rejected at the pack-plane merge seam — NOT evaluated)` : '')) + '\n');
674
+ writeRejected(rejected);
381
675
  if (headersOnly)
382
676
  process.stdout.write(kv('scope', 'headers only — no regen was run') + '\n');
383
677
  for (const o of outcomes) {
@@ -389,13 +683,34 @@ export const generatedCheckCommand = {
389
683
  process.stdout.write(` ! ${o.rule.id} ${o.error}\n`);
390
684
  continue;
391
685
  }
686
+ // A rule that matched nothing and FAILS on it (failOnEmpty) produced no
687
+ // diff and no regen output — its skip reason is the only true line. It is
688
+ // never rendered as drift with a bless hint (round 13).
689
+ if (o.status === 'failed' && o.skipReason !== undefined) {
690
+ process.stdout.write(` ✗ ${o.rule.id} FAILED — ${o.skipReason}\n`);
691
+ process.stdout.write(` → ${formatEmptyRuleAdvice({ fails: true })}\n`);
692
+ continue;
693
+ }
392
694
  const diffs = o.treeDiff?.differences ?? [];
393
- if (o.status === 'passed') {
394
- process.stdout.write(` ${o.rule.id} (${o.committedCount} files` +
695
+ // Rendered from the SETTLED rule: a pass over part of its scope is
696
+ // `partial`, never a ✓ (the keystone emitter pattern, step 5).
697
+ const settledRule = env.rules.find((x) => x.id === o.rule.id);
698
+ if (o.status === 'passed' && settledRule?.status === 'partial') {
699
+ process.stdout.write(` ~ ${o.rule.id} PARTIAL — ${settledRule.shortfall ?? 'part of its scope was not examined'}\n`);
700
+ }
701
+ else if (o.status === 'passed') {
702
+ const exempt = o.handMaintained.length > 0 ? `, ${o.handMaintained.length} hand-maintained` : '';
703
+ process.stdout.write(` ✓ ${o.rule.id} (${o.committedCount} files${exempt}` +
395
704
  `${o.driftChecked ? ', byte-identical to a fresh regen' : ', headers only'})\n`);
705
+ for (const w of o.writers) {
706
+ process.stdout.write(` · ${w.label}: ${w.committedCount} file(s) verified\n`);
707
+ }
396
708
  }
397
709
  else {
398
710
  process.stdout.write(` ✗ ${o.rule.id} ${diffs.length} file(s) differ from a fresh regen\n`);
711
+ for (const w of o.writers) {
712
+ process.stdout.write(` · ${w.label}: ${w.error ? `ERROR — ${w.error}` : `${w.differences} of ${w.committedCount} differ`}\n`);
713
+ }
399
714
  for (const d of diffs.slice(0, 25)) {
400
715
  const label = d.kind === 'content'
401
716
  ? 'hand-edited (or source changed)'
@@ -414,22 +729,122 @@ export const generatedCheckCommand = {
414
729
  process.stdout.write(` … (${o.provenance.length - 25} more header finding(s))\n`);
415
730
  }
416
731
  if (o.status === 'failed')
417
- process.stdout.write(` → ${hintFor(o.rule)}\n`);
732
+ process.stdout.write(` → ${generatedHintFor(o.rule)}\n`);
418
733
  }
419
734
  for (const d of prep.planeDiagnostics)
420
735
  process.stdout.write(` ! ${d}\n`);
421
- if (exit === ExitCode.NotVerified) {
422
- process.stdout.write('\nNothing was checked this is NOT a pass. Every rule matched 0 files.\n');
423
- }
424
- else if (exit === ExitCode.VerifiedPass) {
425
- process.stdout.write('\nEvery generated artifact matches its source. ✓\n');
736
+ // THE empty-rule advice (round 13) for a SOFT skip: a failing one is
737
+ // advised inline (above); a rule that matched nothing and does not fail on
738
+ // it was told nothing.
739
+ for (const a of emptyRuleAdviceLines(outcomes.filter((o) => o.status === 'skipped' && o.skipReason !== undefined).map(() => ({ fails: false })))) {
740
+ process.stdout.write(` ${a}.\n`);
426
741
  }
742
+ // THE shared unit-state block (round 13, K2): a dead `generatedGlob` of a
743
+ // rule that still matched, and a LOCAL expectEmpty marker whose target
744
+ // appeared, withhold the ✓ (exit unchanged); a pack marker is INFO.
745
+ const unitNotes = unitStateNotes(outcomes.map((o) => ({
746
+ id: o.rule.id,
747
+ ...(o.unitLiveness !== undefined ? { unitLiveness: o.unitLiveness } : {}),
748
+ reportedEmpty: o.status === 'skipped' || o.skipReason !== undefined,
749
+ })));
750
+ process.stdout.write(unitNotes.text);
751
+ const line = verdictLine(env, qualifyCleanForUnits('Every generated artifact matches its source. ✓', unitNotes), proposed === ExitCode.NotVerified && evaluated === 0
752
+ ? 'Nothing was checked — this is NOT a pass. Every rule matched 0 files.'
753
+ : undefined);
754
+ if (line)
755
+ process.stdout.write(`\n${line}\n`);
427
756
  return exit;
428
757
  },
429
758
  };
759
+ const EMPTY_WRITES = { written: [], unchanged: [], created: [], notPlaced: [], noLongerProduced: [] };
760
+ /** The leading literal directory of a glob (`docs/schemas/*.json` → `docs/schemas`). */
761
+ function globStaticDir(glob) {
762
+ const out = [];
763
+ for (const seg of glob.split('/').slice(0, -1)) {
764
+ if (/[*?[\]{}]/.test(seg))
765
+ break;
766
+ out.push(seg);
767
+ }
768
+ return out.length > 0 ? out.join('/') : undefined;
769
+ }
770
+ /**
771
+ * Where a regen unit's NEW files belong: the single directory its committed
772
+ * slice lives in, or — for a first generation with nothing committed yet — the
773
+ * single literal directory its INCLUSION globs name. Ambiguous → undefined,
774
+ * and the new file is reported instead of guessed.
775
+ *
776
+ * Only an inclusion glob says where files live: a negation
777
+ * (`!gen/**\/*.hand.ts`) names what is carved OUT, and read raw its leading
778
+ * `!gen` was a second "directory" that made every first generation ambiguous.
779
+ */
780
+ function sliceTargetDir(committed, globs) {
781
+ const dirs = new Set([...committed.keys()].map((p) => nodePath.posix.dirname(p)));
782
+ if (dirs.size === 1)
783
+ return [...dirs][0];
784
+ if (dirs.size > 1)
785
+ return undefined;
786
+ const statics = new Set(globListWalkGlobs(globs)
787
+ .map(globStaticDir)
788
+ .filter((d) => d !== undefined));
789
+ return statics.size === 1 ? [...statics][0] : undefined;
790
+ }
791
+ function writeOut(cwd, rel, content) {
792
+ const abs = nodePath.join(cwd, rel);
793
+ mkdirSync(nodePath.dirname(abs), { recursive: true });
794
+ writeFileSync(abs, content, 'utf8');
795
+ }
796
+ /**
797
+ * Write a regen unit's ALIGNED output over the committed tree.
798
+ *
799
+ * The keys come from {@link runOneRegen} — the same alignment `generated check`
800
+ * diffs against — so "which committed file does this output replace" has one
801
+ * answer for both verbs. Output that aligns to nothing is a new file: it is
802
+ * written only where the rule's own glob says generated files live, never at a
803
+ * bare temp-relative path (which is how a flat regen used to land at the repo
804
+ * root). A committed file the regen stopped producing is reported, not
805
+ * deleted: removing tracked files is a decision for a human.
806
+ */
807
+ function writeAligned(cwd, rule, scan, committed, files) {
808
+ const written = [];
809
+ const unchanged = [];
810
+ const created = [];
811
+ const notPlaced = [];
812
+ const blessed = new Set(scan.handMaintained);
813
+ const targetDir = sliceTargetDir(committed, rule.generatedGlob);
814
+ for (const [key, content] of files) {
815
+ if (committed.has(key)) {
816
+ if (committed.get(key) === content)
817
+ unchanged.push(key);
818
+ else {
819
+ writeOut(cwd, key, content);
820
+ written.push(key);
821
+ }
822
+ continue;
823
+ }
824
+ const candidates = [key, ...(targetDir ? [nodePath.posix.join(targetDir, key)] : [])];
825
+ // A new output is placed only where `generatedGlob` SELECTS it — never on
826
+ // a path the list excludes, which the next check would not cover.
827
+ const dest = candidates.find((c) => globListSelects(c, rule.generatedGlob) && !blessed.has(c));
828
+ if (!dest) {
829
+ notPlaced.push(key);
830
+ continue;
831
+ }
832
+ writeOut(cwd, dest, content);
833
+ created.push(dest);
834
+ }
835
+ const produced = new Set([...written, ...unchanged]);
836
+ const noLongerProduced = [...committed.keys()].filter((p) => !produced.has(p)).sort();
837
+ return {
838
+ written: written.sort(),
839
+ unchanged: unchanged.sort(),
840
+ created: created.sort(),
841
+ notPlaced: notPlaced.sort(),
842
+ noLongerProduced,
843
+ };
844
+ }
430
845
  export const generatedUpdateCommand = {
431
846
  name: 'update',
432
- description: 'Run the declared regen command in place the one-command bless step after an intentional source change. Writes files.',
847
+ description: 'The bless step after an intentional source change: regenerate into a temp dir with the SAME regen + path alignment `generated check` uses, then write the aligned files over their committed paths. Writes files; never deletes a committed file.',
433
848
  usage: 'shrk generated update [--id <ids>] [--json]',
434
849
  booleanFlags: new Set(['json']),
435
850
  async run(args) {
@@ -441,44 +856,74 @@ export const generatedUpdateCommand = {
441
856
  return writeNoRules(json);
442
857
  const results = [];
443
858
  for (const rule of prep.rules) {
444
- if (!rule.regen) {
445
- results.push({ id: rule.id, ran: false, error: 'header-only rule nothing to regenerate' });
859
+ // `{TMP}` means the same thing here as in `check`. Update used to
860
+ // substitute the project root instead, so a flat regen such as
861
+ // `schemas emit --out {TMP}` wrote every file at the repo ROOT while
862
+ // `check` compared against docs/schemas/ — two answers to "where does
863
+ // the output land". Regenerating through runOneRegen makes it one.
864
+ // A multi-writer rule blesses EVERY writer's slice, never just one.
865
+ const scan = scanGeneratedFiles(prep.cwd, rule, prep.excludeDirs);
866
+ const units = regenUnitsOf(rule, scan);
867
+ if (units.length === 0) {
868
+ results.push({ id: rule.id, ran: false, error: 'header-only rule — nothing to regenerate', ...EMPTY_WRITES });
446
869
  continue;
447
870
  }
448
- // `{TMP}` is the CHECK contract; `update` writes in place, so it is
449
- // substituted with the project root and the regen writes its real output.
450
- const command = rule.regen.split('{TMP}').join(prep.cwd);
451
- const child = spawnSync(command, {
452
- cwd: prep.cwd,
453
- shell: true,
454
- encoding: 'utf8',
455
- timeout: rule.timeoutMs ?? DEFAULT_TIMEOUT_MS,
456
- maxBuffer: 16 * 1024 * 1024,
457
- stdio: json ? 'pipe' : 'inherit',
458
- });
459
- if (child.error) {
460
- results.push({ id: rule.id, ran: false, error: child.error.message });
461
- }
462
- else if (child.status !== 0) {
463
- results.push({ id: rule.id, ran: true, error: `exited ${child.status ?? 'null'}` });
464
- }
465
- else {
466
- results.push({ id: rule.id, ran: true });
871
+ for (const unit of units) {
872
+ const regen = runOneRegen(prep.cwd, unit.command, unit.committed, unit.timeoutMs);
873
+ if (regen.error || !regen.files) {
874
+ results.push({ id: unit.label, ran: true, error: regen.error ?? 'regen produced no files', ...EMPTY_WRITES });
875
+ continue;
876
+ }
877
+ // An output the reader did not read (over the regen cap) is never
878
+ // written: it is listed, and the bless is reported incomplete (1).
879
+ // Its committed counterpart is not "no longer produced".
880
+ const unreadKeys = new Set((regen.unread ?? []).map((u) => u.path));
881
+ const aligned = writeAligned(prep.cwd, rule, scan, unit.committed, regen.files);
882
+ results.push({
883
+ id: unit.label,
884
+ ran: true,
885
+ ...aligned,
886
+ noLongerProduced: aligned.noLongerProduced.filter((p) => !unreadKeys.has(p)),
887
+ ...(unreadKeys.size > 0 ? { unreadOutputs: [...unreadKeys].sort() } : {}),
888
+ });
467
889
  }
468
890
  }
469
891
  const failed = results.filter((r) => r.error !== undefined);
892
+ // The bless is incomplete when output could not be placed, a committed
893
+ // file is no longer produced, or an output was too large to read:
894
+ // `generated check` would still fail, so exiting 0 here would claim a sync
895
+ // that did not happen.
896
+ const outOfSync = results.filter((r) => r.notPlaced.length > 0 || r.noLongerProduced.length > 0 || (r.unreadOutputs?.length ?? 0) > 0);
897
+ const exit = failed.length > 0 || outOfSync.length > 0 ? ExitCode.Failure : ExitCode.VerifiedPass;
470
898
  if (json) {
471
- process.stdout.write(asJson({ schema: SCHEMA, results }) + '\n');
472
- return failed.length > 0 ? ExitCode.Failure : ExitCode.VerifiedPass;
899
+ process.stdout.write(asJson({ schema: SCHEMA, inSync: failed.length === 0 && outOfSync.length === 0, results, exitCode: exit }) + '\n');
900
+ return exit;
473
901
  }
474
902
  process.stdout.write(header('Generated update'));
475
903
  for (const r of results) {
476
- process.stdout.write(` ${r.error ? '!' : '✓'} ${r.id}${r.error ? ` — ${r.error}` : ''}\n`);
477
- }
478
- if (failed.length === 0) {
479
- process.stdout.write('\nRegenerated. Review `git diff` before committing.\n');
904
+ if (r.error) {
905
+ process.stdout.write(` ! ${r.id} — ${r.error}\n`);
906
+ continue;
907
+ }
908
+ process.stdout.write(` ✓ ${r.id} — ${r.written.length} rewritten, ${r.created.length} new, ${r.unchanged.length} unchanged\n`);
909
+ for (const f of r.written)
910
+ process.stdout.write(` ~ ${f}\n`);
911
+ for (const f of r.created)
912
+ process.stdout.write(` + ${f}\n`);
913
+ for (const f of r.notPlaced) {
914
+ process.stdout.write(` ! not written: ${f} — maps to no committed file and lies outside generatedGlob\n`);
915
+ }
916
+ for (const f of r.unreadOutputs ?? []) {
917
+ process.stdout.write(` ! not written: ${f} — over the regen read cap, so it was never read\n`);
918
+ }
919
+ for (const f of r.noLongerProduced) {
920
+ process.stdout.write(` ? no longer produced (left in place — delete it if that is intended): ${f}\n`);
921
+ }
480
922
  }
481
- return failed.length > 0 ? ExitCode.Failure : ExitCode.VerifiedPass;
923
+ process.stdout.write(exit === ExitCode.VerifiedPass
924
+ ? '\nRegenerated. Review `git diff` before committing.\n'
925
+ : '\nNot fully blessed — `generated check` will still report the files above.\n');
926
+ return exit;
482
927
  },
483
928
  };
484
929
  export const generatedExplainCommand = {
@@ -502,12 +947,23 @@ export const generatedExplainCommand = {
502
947
  }
503
948
  // explain never spawns — the point is to show what the rule SEES.
504
949
  const scan = scanGeneratedFiles(prep.cwd, rule, prep.excludeDirs);
505
- const outcome = evaluateRule(prep.cwd, rule, prep.excludeDirs, true);
950
+ const outcome = evaluateGeneratedRule(prep.cwd, rule, prep.excludeDirs, true);
506
951
  if (flagBool(args, 'json')) {
507
952
  process.stdout.write(asJson({
508
953
  schema: 'sharkcraft.generated-explain/v1',
509
954
  ...outcomeJson(outcome),
510
955
  files: [...scan.generated.keys()],
956
+ checkable: [...scan.checkable.keys()],
957
+ handMaintained: scan.handMaintained,
958
+ markedHandMaintained: scan.markedHandMaintained,
959
+ staleHandMaintained: scan.staleHandMaintained,
960
+ unclassified: scan.unclassified,
961
+ slices: scan.slices.map((sl) => ({
962
+ label: sl.label,
963
+ regen: sl.regen,
964
+ glob: sl.glob,
965
+ files: [...sl.files.keys()],
966
+ })),
511
967
  outsideScanned: scan.outside.size,
512
968
  outsideGlobs: scan.outsideGlobs,
513
969
  regen: rule.regen ?? null,
@@ -519,7 +975,34 @@ export const generatedExplainCommand = {
519
975
  process.stdout.write(` ${rule.description}\n`);
520
976
  process.stdout.write(kv('glob', rule.generatedGlob.join(', ')) + '\n');
521
977
  process.stdout.write(kv('files matched', String(scan.generated.size)) + '\n');
522
- process.stdout.write(kv('regen', rule.regen ?? '(header-only)') + '\n');
978
+ if (scan.slices.length > 0) {
979
+ process.stdout.write(kv('writers', String(scan.slices.length)) + '\n');
980
+ for (const sl of scan.slices) {
981
+ process.stdout.write(` [${sl.label}] ${sl.files.size} file(s) — ${sl.regen}\n`);
982
+ process.stdout.write(` owns ${sl.glob.join(', ')}\n`);
983
+ }
984
+ }
985
+ else {
986
+ process.stdout.write(kv('regen', rule.regen ?? '(header-only)') + '\n');
987
+ }
988
+ if (scan.handMaintained.length > 0) {
989
+ process.stdout.write(kv('hand-maintained', `${scan.handMaintained.length} file(s) — exempt from header + byte checks`) + '\n');
990
+ const marked = new Set(scan.markedHandMaintained);
991
+ // Say WHICH mechanism blessed each file: a config entry and an in-file
992
+ // marker are reviewed in different places, so "why is this exempt?" has
993
+ // two different answers and the reader needs to know which one applies.
994
+ for (const f of scan.handMaintained.slice(0, 20)) {
995
+ process.stdout.write(` ${f}${marked.has(f) ? ' (in-file marker)' : ' (handMaintained[])'}\n`);
996
+ }
997
+ }
998
+ if (scan.unclassified.length > 0) {
999
+ process.stdout.write(kv('unclassified', `${scan.unclassified.length} file(s) — owned by no writer, not blessed`) + '\n');
1000
+ for (const f of scan.unclassified.slice(0, 20))
1001
+ process.stdout.write(` ${f}\n`);
1002
+ }
1003
+ for (const pattern of scan.staleHandMaintained) {
1004
+ process.stdout.write(` ! handMaintained "${pattern}" matches no file — stale bless\n`);
1005
+ }
523
1006
  process.stdout.write(kv('compare', rule.compare ?? 'bytes') + '\n');
524
1007
  if (rule.provenanceHeader) {
525
1008
  process.stdout.write(kv('header', `/${rule.provenanceHeader.mustMatch}/`) + '\n');