@shrkcrft/cli 0.1.0-alpha.30 → 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 +54 -3
  20. package/dist/commands/baseline.command.d.ts.map +1 -1
  21. package/dist/commands/baseline.command.js +582 -74
  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 +138 -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 +1241 -322
  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 +3 -0
  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 +13 -0
  77. package/dist/commands/docs-references.command.d.ts.map +1 -1
  78. package/dist/commands/docs-references.command.js +269 -72
  79. package/dist/commands/doctor.command.d.ts.map +1 -1
  80. package/dist/commands/doctor.command.js +58 -4
  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 +52 -0
  99. package/dist/commands/gates.command.d.ts.map +1 -1
  100. package/dist/commands/gates.command.js +811 -185
  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 +33 -3
  104. package/dist/commands/generated.command.d.ts.map +1 -1
  105. package/dist/commands/generated.command.js +499 -147
  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 +198 -24
  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 +360 -73
  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 +37 -3
  328. package/dist/gates/gate-rule-globs.d.ts.map +1 -1
  329. package/dist/gates/gate-rule-globs.js +78 -40
  330. package/dist/gates/gate-rule-view.d.ts +17 -4
  331. package/dist/gates/gate-rule-view.d.ts.map +1 -1
  332. package/dist/gates/gate-rule-view.js +105 -90
  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 +187 -4
  370. package/dist/gates/rule-coverage.d.ts.map +1 -1
  371. package/dist/gates/rule-coverage.js +501 -49
  372. package/dist/gates/run-gate-planes.d.ts +14 -0
  373. package/dist/gates/run-gate-planes.d.ts.map +1 -1
  374. package/dist/gates/run-gate-planes.js +172 -30
  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 +200 -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
@@ -14,20 +14,32 @@
14
14
  * Distinct from `shrk gate` (singular), which RUNS the quality-gate pipeline.
15
15
  * This verb inspects the data-defined RULES themselves.
16
16
  */
17
- import { existsSync, readFileSync } from 'node:fs';
17
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
18
18
  import * as nodePath from 'node:path';
19
- import { resolvePlaneExtractors } from '@shrkcrft/core';
20
- import { explainWiring, inspectSource, scanRegistry } from '@shrkcrft/boundaries';
21
- import { BaselineRuleSchema, DocReferenceRuleSchema, GeneratedArtifactRuleSchema, PolicyRuleSchema, RegistrationIdiomSchema, RegistryDeclarationSchema, WiringRuleSchema, } from '@shrkcrft/config';
22
- import { inspectSharkcraft, warmReferenceRegistries, refExists, resolveChangedFiles, resolveProjectConfig, } from '@shrkcrft/inspector';
23
- import { clearFileReadCache } from '@shrkcrft/boundaries';
24
- import { firstUnknownFlag, flagBool, flagString, resolveCwd, } from "../command-registry.js";
19
+ import { coverageShortfall, formatCoverage, DEAD_SELECTOR_CAUSES, formatEmptyRuleAdvice, formatUnitLiveness, resolvePlaneExtractors, RuleEmptiness, ruleVerdictRecords, UnitLivenessState, validateResolvedPlaneSources, } from '@shrkcrft/core';
20
+ import { explainWiring, inspectSource, scanRegistry, settleGlobLists, sourceLivenessRequest } from '@shrkcrft/boundaries';
21
+ import { BaselineRuleSchema, DocReferenceRuleSchema, GATE_PLANE_CONFIG_KEY, GeneratedArtifactRuleSchema, normalizePlaneRule, PolicyRuleSchema, RegistrationIdiomSchema, RegistryDeclarationSchema, WiringRuleSchema, } from '@shrkcrft/config';
22
+ import { warmCliReferenceRegistries } from "../surface/cli-command-resolver.js";
23
+ import { inspectSharkcraft, refExists, resolveChangedFiles, resolveProjectConfig, } from '@shrkcrft/inspector';
24
+ import { clearFileReadCache, planeScanExcludeDirs } from '@shrkcrft/boundaries';
25
+ import { flagBool, flagString, resolveCwd, } from "../command-registry.js";
26
+ import { GLOBAL_FLAGS } from "../dispatch/global-flags.js";
27
+ import { PositionalMode } from "../dispatch/positional-mode.js";
25
28
  import { ExitCode } from "../exit-codes.js";
26
29
  import { asJson, header, kv } from "../output/format-output.js";
27
30
  import { collectGateRules, GATE_PLANES, } from "../gates/gate-rule-view.js";
28
- import { buildGateCoverage } from "../gates/rule-coverage.js";
31
+ import { buildGateCoverage, coverageDeadUnits, coverageWentLiveUnits, formatDeadUnits, formatNegations, reportsDeadGlobs, settleGateCoverage, withRejectedRules, } from "../gates/rule-coverage.js";
32
+ import { seamRejectedRules } from "../gates/seam-rejected-rules.js";
33
+ import { settleVerdict } from "../gates/settle-verdict.js";
34
+ import { insertSelfTest, scaffoldSelfTest } from "../gates/scaffold-selftest.js";
29
35
  import { buildGateEnvelope } from "../gates/gate-envelope.js";
30
- import { ruleTouchedBy } from "../gates/gate-rule-globs.js";
36
+ import { ALLOW_EMPTY_FLAG, allowEmptyValve } from "../gates/allow-empty.js";
37
+ import { verdictLine } from "../gates/verdict-line.js";
38
+ import { acceptedEmptyNote } from "../gates/accepted-empty-note.js";
39
+ import { emptyRuleAdviceLines } from "../gates/empty-rule-advice-lines.js";
40
+ import { qualifyCleanForUnits } from "../gates/qualify-clean-for-units.js";
41
+ import { unitStateNotes } from "../gates/unit-state-notes.js";
42
+ import { gateRuleLabeledSources, ruleTouchedBy } from "../gates/gate-rule-globs.js";
31
43
  import { runGatePlanes } from "../gates/run-gate-planes.js";
32
44
  import { baselineExplainCommand } from "./baseline.command.js";
33
45
  import { generatedExplainCommand } from "./generated.command.js";
@@ -35,7 +47,7 @@ import { docsReferencesExplainCommand } from "./docs-references.command.js";
35
47
  import { renderPolicyExplain, runPolicyExplain } from "./policy-lint.command.js";
36
48
  import { renderWiringExplain } from "./wiring.command.js";
37
49
  const SCHEMA = 'sharkcraft.gates/v1';
38
- async function prepare(args) {
50
+ export async function prepare(args) {
39
51
  const cwd = resolveCwd(args);
40
52
  // Coverage memoizes its tree reads inside one call (see `withFileReadCache`).
41
53
  // `--no-cache` drops anything already memoized so a suspected caching bug can
@@ -52,15 +64,17 @@ async function prepare(args) {
52
64
  process.stderr.write(`Could not load config: ${msg}\n Run \`shrk doctor\` for details.\n`);
53
65
  return { ok: false, code: ExitCode.UsageError };
54
66
  }
55
- const rel = nodePath.relative(cwd, loaded.value.sharkcraftDir).split(nodePath.sep).join('/');
56
67
  return {
57
68
  ok: true,
58
69
  value: {
59
70
  cwd,
60
71
  rules: collectGateRules(loaded.value.config),
61
- excludeDirs: rel && !rel.startsWith('..') ? [rel] : [],
72
+ // THE plane scan scope the one authority every plane verb reads too.
73
+ excludeDirs: planeScanExcludeDirs(cwd, loaded.value.sharkcraftDir),
62
74
  planeDiagnostics: loaded.value.planeDiagnostics,
75
+ rejectedRules: seamRejectedRules(loaded.value),
63
76
  extractors: loaded.value.config.extractors ?? {},
77
+ configFile: loaded.value.configFile,
64
78
  },
65
79
  };
66
80
  }
@@ -100,7 +114,7 @@ function writeNoRules(json) {
100
114
  * silently degrade to an empty diff, which would narrow every rule out of
101
115
  * scope and produce a green run that checked nothing.
102
116
  */
103
- function resolveScope(args, cwd) {
117
+ export function resolveScope(args, cwd) {
104
118
  const changedOnly = flagBool(args, 'changed-only');
105
119
  const since = flagString(args, 'since') ?? flagString(args, 'base');
106
120
  if (!changedOnly && !since)
@@ -125,7 +139,7 @@ function resolveScope(args, cwd) {
125
139
  * must never print the same headline as a full one — "0 violations across 2 of
126
140
  * 9 rules" and "0 violations across 9 of 9" are different facts.
127
141
  */
128
- function narrowToScope(rules, files) {
142
+ export function narrowToScope(rules, files) {
129
143
  if (files === undefined)
130
144
  return { selected: rules, skippedByScope: 0 };
131
145
  const selected = rules.filter((r) => ruleTouchedBy(r, files));
@@ -134,6 +148,8 @@ function narrowToScope(rules, files) {
134
148
  /** Flags every `gates` verb accepts; anything else is a typo, not an opt-in. */
135
149
  const GATES_FLAGS = new Set([
136
150
  'json',
151
+ 'margin',
152
+ 'write',
137
153
  'strict',
138
154
  'plane',
139
155
  'only',
@@ -144,28 +160,32 @@ const GATES_FLAGS = new Set([
144
160
  'no-cache',
145
161
  'full',
146
162
  'limit',
147
- 'cwd',
148
163
  'id',
149
164
  'rule-file',
150
165
  'wiring',
151
- 'no-hints',
152
- 'exit-trailer',
166
+ // Every dispatcher global (THE list) — a direct handler call may carry them.
167
+ ...GLOBAL_FLAGS,
153
168
  ]);
154
169
  /**
155
- * Reject an unrecognized flag rather than ignoring it.
156
- *
157
- * Without this a mistyped `--changed-only` parses as an unrelated `true`, the
158
- * verb runs its UNSCOPED form, and exit `0` reads as "the scoped check passed".
159
- * A flag the tool does not understand must never look like a satisfied request.
170
+ * `--fail-on-dead-units`: a dead glob inside a CONNECTED rule — an inclusion
171
+ * glob that selects nothing, or a negation that excludes nothing — fails the
172
+ * run. A live negation is never dead, so the flag is usable in a repo whose
173
+ * rules exclude their tests. The same flag name the boundary plane uses for
174
+ * its dead scope globs, so one CI switch means one thing across every plane.
160
175
  */
161
- function rejectUnknownFlags(args) {
162
- const bad = firstUnknownFlag(args, GATES_FLAGS);
163
- if (bad === undefined)
164
- return undefined;
165
- process.stderr.write(`Unknown flag "--${bad}". \`shrk gates\` accepts: ` +
166
- `${[...GATES_FLAGS].filter((f) => f !== 'cwd' && f !== 'no-hints' && f !== 'exit-trailer').map((f) => `--${f}`).join(', ')}.\n`);
167
- return ExitCode.UsageError;
168
- }
176
+ const FAIL_ON_DEAD_UNITS_FLAG = 'fail-on-dead-units';
177
+ /** Flags only `gates coverage` accepts on top of {@link GATES_FLAGS}. */
178
+ const COVERAGE_ONLY_FLAGS = new Set([FAIL_ON_DEAD_UNITS_FLAG]);
179
+ /** Flags only `gates try` accepts on top of {@link GATES_FLAGS}. */
180
+ const TRY_ONLY_FLAGS = new Set(['flags']);
181
+ /** Flags only `gates check` accepts on top of {@link GATES_FLAGS}. */
182
+ const CHECK_ONLY_FLAGS = new Set([ALLOW_EMPTY_FLAG]);
183
+ // An unrecognized flag is refused rather than ignored: a mistyped
184
+ // `--changed-only` would otherwise parse as an unrelated `true`, run the
185
+ // UNSCOPED form, and exit `0` as though the scoped check had passed. Each
186
+ // guarded verb DECLARES its complete set (`flags`: GATES_FLAGS plus its own
187
+ // extras — the identical sets the inline `rejectUnknownFlags` guard used), and
188
+ // the dispatcher refuses anything else before `run`, exiting 3 (UsageError).
169
189
  /**
170
190
  * Apply `--plane` and `--only` narrowing.
171
191
  *
@@ -173,19 +193,28 @@ function rejectUnknownFlags(args) {
173
193
  * typo'd rule id would otherwise narrow the run to zero rules and report a
174
194
  * confident pass over an empty set.
175
195
  */
176
- function filterRules(all, planes, only) {
196
+ function filterRules(all, planes, only, rejectedAll = []) {
177
197
  let rules = planes ? all.filter((r) => planes.has(r.plane)) : all;
198
+ let rejected = planes ? rejectedAll.filter((r) => planes.has(r.type)) : rejectedAll;
178
199
  if (!only)
179
- return { ok: true, rules };
200
+ return { ok: true, rules, rejected };
180
201
  const wanted = only.split(',').map((x) => x.trim()).filter(Boolean);
181
- const known = new Set(all.map((r) => r.id));
202
+ // A rejected rule is a DECLARED rule (it did not run), so `--only <its id>`
203
+ // selects its errored row rather than refusing the id as unknown.
204
+ const known = new Set([...all.map((r) => r.id), ...rejectedAll.map((r) => r.id)]);
182
205
  const unknown = wanted.filter((w) => !known.has(w));
183
206
  if (unknown.length > 0) {
184
207
  process.stderr.write(`Unknown rule id(s) in --only: ${unknown.join(', ')}. Run \`shrk gates list\` to see the ${all.length} declared rule(s).\n`);
185
208
  return { ok: false };
186
209
  }
187
210
  rules = rules.filter((r) => wanted.includes(r.id));
188
- return { ok: true, rules };
211
+ rejected = rejected.filter((r) => wanted.includes(r.id));
212
+ return { ok: true, rules, rejected };
213
+ }
214
+ /** ` ✗ [policy] pk-x — REJECTED: <why>` — one line per merge-seam-rejected rule. */
215
+ function writeRejectedRules(rejected) {
216
+ for (const r of rejected)
217
+ process.stdout.write(` ✗ [${r.type}] ${r.id} REJECTED — ${r.error ?? 'failed validation'}\n`);
189
218
  }
190
219
  /**
191
220
  * Build the registries the doc-reference plane resolves against — only when a
@@ -202,7 +231,9 @@ async function inspectionIfNeeded(cwd, rules) {
202
231
  // resolver is sync. Warming here is what makes a correct pack playbook cited
203
232
  // in prose actually resolve.
204
233
  const inspection = await inspectSharkcraft({ cwd });
205
- await warmReferenceRegistries(inspection);
234
+ // WITH the command resolver, so a doc-reference rule resolving `command`
235
+ // ids checks them against the live command index.
236
+ await warmCliReferenceRegistries(inspection);
206
237
  return inspection;
207
238
  }
208
239
  export const gatesListCommand = {
@@ -221,7 +252,10 @@ export const gatesListCommand = {
221
252
  const rules = planes.planes
222
253
  ? prep.value.rules.filter((r) => planes.planes.has(r.plane))
223
254
  : prep.value.rules;
224
- if (rules.length === 0)
255
+ const rejected = planes.planes
256
+ ? prep.value.rejectedRules.filter((r) => planes.planes.has(r.type))
257
+ : prep.value.rejectedRules;
258
+ if (rules.length === 0 && rejected.length === 0)
225
259
  return writeNoRules(json);
226
260
  if (json) {
227
261
  process.stdout.write(asJson({
@@ -235,6 +269,8 @@ export const gatesListCommand = {
235
269
  failOnEmpty: r.failOnEmpty,
236
270
  selfTest: r.selfTest ?? null,
237
271
  })),
272
+ // Declared by a pack, refused by the merge seam: they never run.
273
+ rejected: rejected.map((r) => ({ id: r.id, plane: r.type, error: r.error ?? null })),
238
274
  diagnostics: prep.value.planeDiagnostics,
239
275
  }) + '\n');
240
276
  return ExitCode.VerifiedPass;
@@ -256,6 +292,10 @@ export const gatesListCommand = {
256
292
  process.stdout.write(` ${r.description}\n`);
257
293
  }
258
294
  }
295
+ if (rejected.length > 0) {
296
+ process.stdout.write(`\nrejected at the pack-plane merge seam — never run (${rejected.length})\n`);
297
+ writeRejectedRules(rejected);
298
+ }
259
299
  for (const d of prep.value.planeDiagnostics)
260
300
  process.stdout.write(` ! ${d}\n`);
261
301
  process.stdout.write('\nRun `shrk gates coverage` to see what each one actually matches.\n');
@@ -265,12 +305,10 @@ export const gatesListCommand = {
265
305
  export const gatesCoverageCommand = {
266
306
  name: 'coverage',
267
307
  description: 'What every rule MATCHED against the live tree — the stale-selector detector. A rule matching 0 files/ids is a bug in the rule, never a pass. Also runs each rule\'s declared selfTest expectations.',
268
- usage: 'shrk gates coverage [--plane <p>] [--changed-only | --since <ref>] [--only <ids>] [--strict] [--json]',
269
- booleanFlags: new Set(['json', 'strict', 'changed-only', 'no-cache']),
308
+ usage: 'shrk gates coverage [--plane <p>] [--changed-only | --since <ref>] [--only <ids>] [--fail-on-dead-units] [--strict] [--json]',
309
+ booleanFlags: new Set(['json', 'strict', 'changed-only', 'no-cache', FAIL_ON_DEAD_UNITS_FLAG]),
310
+ flags: new Set([...GATES_FLAGS, ...COVERAGE_ONLY_FLAGS]),
270
311
  async run(args) {
271
- const flagReject = rejectUnknownFlags(args);
272
- if (flagReject !== undefined)
273
- return flagReject;
274
312
  const prep = await prepare(args);
275
313
  if (!prep.ok)
276
314
  return prep.code;
@@ -278,11 +316,15 @@ export const gatesCoverageCommand = {
278
316
  if (!planes.ok)
279
317
  return ExitCode.UsageError;
280
318
  const json = flagBool(args, 'json');
281
- const filtered = filterRules(prep.value.rules, planes.planes, flagString(args, 'only'));
319
+ const filtered = filterRules(prep.value.rules, planes.planes, flagString(args, 'only'), prep.value.rejectedRules);
282
320
  if (!filtered.ok)
283
321
  return ExitCode.UsageError;
284
- if (filtered.rules.length === 0)
285
- return writeNoRules(json);
322
+ // A rule the merge seam rejected is a configured rule that never ran — it
323
+ // is never "no rules declared", and it is never narrowed out by scope.
324
+ const rejected = filtered.rejected;
325
+ if (filtered.rules.length === 0 && rejected.length === 0) {
326
+ return writeNoRulesVerdict('gates coverage', args, json, false);
327
+ }
286
328
  // Scoping the STALE-SELECTOR check to the diff is what moves it from a
287
329
  // CI-only report to something you can afford on every save — a stale glob
288
330
  // is then caught the moment you cause it, not the next morning.
@@ -292,8 +334,14 @@ export const gatesCoverageCommand = {
292
334
  return ExitCode.UsageError;
293
335
  }
294
336
  const { selected, skippedByScope } = narrowToScope(filtered.rules, scope.files);
295
- if (selected.length === 0) {
337
+ if (selected.length === 0 && rejected.length === 0) {
296
338
  // Nothing in scope means nothing was PROVEN — never a green.
339
+ const emptyEnv = buildGateEnvelope('gates coverage', ExitCode.VerifiedPass, [], {
340
+ unit: 'rules',
341
+ expected: 0,
342
+ examined: 0,
343
+ reason: `no rule's footprint intersects the changeset (${skippedByScope} skipped by scope)`,
344
+ });
297
345
  if (json) {
298
346
  process.stdout.write(asJson({
299
347
  schema: 'sharkcraft.gate-coverage/v1',
@@ -301,7 +349,8 @@ export const gatesCoverageCommand = {
301
349
  total: 0,
302
350
  scoped: true,
303
351
  skippedByScope,
304
- exitCode: ExitCode.NotVerified,
352
+ exitCode: emptyEnv.exit,
353
+ gate: emptyEnv,
305
354
  }) + '\n');
306
355
  }
307
356
  else {
@@ -309,44 +358,31 @@ export const gatesCoverageCommand = {
309
358
  process.stdout.write(` No rule's footprint intersects the changeset (${skippedByScope} skipped by scope).\n` +
310
359
  ' Nothing was checked — this is NOT a pass.\n');
311
360
  }
312
- return ExitCode.NotVerified;
361
+ return emptyEnv.exit;
313
362
  }
314
363
  const rules = selected;
315
- const report = buildGateCoverage(prep.value.cwd, rules, prep.value.excludeDirs, prep.value.extractors, false, await inspectionIfNeeded(prep.value.cwd, rules));
364
+ const report = withRejectedRules(buildGateCoverage(prep.value.cwd, rules, prep.value.excludeDirs, prep.value.extractors, false, await inspectionIfNeeded(prep.value.cwd, rules)), rejected);
365
+ const failOnDeadUnits = flagBool(args, FAIL_ON_DEAD_UNITS_FLAG);
316
366
  // A rule that matched nothing is NOT-VERIFIED (2) by default — it neither
317
367
  // passed nor failed, it never ran. `failOnEmpty` on the rule (or the global
318
- // --strict promotion) turns that into a hard failure.
319
- const hardFailures = report.rules.filter((r) => r.status === 'error' || r.status === 'failed-expectation' || (r.status === 'empty' && r.failOnEmpty));
368
+ // --strict promotion) turns that into a hard failure, as does a dead glob
369
+ // under --fail-on-dead-units. Settle first, render second: a rule that is
370
+ // connected but only PARTIALLY is `partial` here exactly as it is in
371
+ // `gates check`. `settleGateCoverage` is the ONE derivation — `shrk
372
+ // quality`'s coverage item reads the same one.
373
+ const env = settleGateCoverage(report, { failOnDeadUnits });
374
+ const exit = env.exit;
320
375
  const softEmpty = report.rules.filter((r) => r.status === 'empty' && !r.failOnEmpty);
321
- const exit = hardFailures.length > 0
322
- ? ExitCode.Failure
323
- : softEmpty.length > 0
324
- ? ExitCode.NotVerified
325
- : ExitCode.VerifiedPass;
326
376
  if (json) {
327
377
  process.stdout.write(asJson({
328
378
  ...report,
329
- hardFailures: hardFailures.length,
379
+ // Rules the settled envelope fails (errored, broken selfTest, a
380
+ // failOnEmpty rule matching nothing, a dead glob under the flag).
381
+ hardFailures: env.failed,
382
+ failOnDeadUnits,
330
383
  ...(scope.files ? { scoped: true, skippedByScope } : {}),
331
384
  exitCode: exit,
332
- gate: buildGateEnvelope('gates coverage', exit, report.rules.map((r) => ({
333
- id: r.id,
334
- type: r.plane,
335
- status: r.status === 'ok'
336
- ? 'passed'
337
- : r.status === 'empty'
338
- ? r.failOnEmpty
339
- ? 'failed'
340
- : 'skipped'
341
- : r.status === 'error'
342
- ? 'error'
343
- : 'failed',
344
- severity: r.failOnEmpty ? 'error' : 'warning',
345
- counts: { files: r.filesMatched, units: r.unitsMatched },
346
- violations: r.expectationFailures.map((f) => ({ id: r.id, message: f })),
347
- ...(r.status === 'empty' ? { skipReason: `matched 0 ${r.unitLabel}` } : {}),
348
- ...(r.error ? { error: r.error } : {}),
349
- }))),
385
+ gate: env,
350
386
  }) + '\n');
351
387
  return exit;
352
388
  }
@@ -355,12 +391,27 @@ export const gatesCoverageCommand = {
355
391
  if (scope.files) {
356
392
  process.stdout.write(kv('scope', `changed-only (${scope.files.length} file(s))`) + '\n');
357
393
  }
358
- process.stdout.write(kv('matched nothing', `${report.empty}${report.empty > 0 ? ' ← stale selector suspects' : ''}`) + '\n');
394
+ // A rule its own negations emptied is not a stale-selector suspect, so the
395
+ // header counts it apart — agreeing with the row it draws below.
396
+ const emptiedByNegations = report.rules.filter((r) => r.status === 'empty' && r.excludedByNegations !== undefined).length;
397
+ const staleSuspects = report.empty - emptiedByNegations;
398
+ const emptyNote = report.empty === 0
399
+ ? ''
400
+ : emptiedByNegations === 0
401
+ ? ' ← stale selector suspects'
402
+ : staleSuspects === 0
403
+ ? ' ← emptied by their own negations, not stale'
404
+ : ` ← ${staleSuspects} stale selector suspect(s), ${emptiedByNegations} emptied by their own negations`;
405
+ process.stdout.write(kv('matched nothing', `${report.empty}${emptyNote}`) + '\n');
359
406
  if (report.errored > 0)
360
407
  process.stdout.write(kv('misconfigured', String(report.errored)) + '\n');
361
408
  if (report.expectationFailures > 0) {
362
409
  process.stdout.write(kv('broken selfTest', String(report.expectationFailures)) + '\n');
363
410
  }
411
+ if (report.deadGlobCount > 0) {
412
+ process.stdout.write(kv('dead globs', `${report.deadGlobCount} ← inside rules that still match something` +
413
+ (failOnDeadUnits ? ` (--${FAIL_ON_DEAD_UNITS_FLAG}: failing)` : '')) + '\n');
414
+ }
364
415
  // The shared extractors, ONCE. Their whole value is that N consumers cannot
365
416
  // disagree about which set they check — so one line proving the shared set
366
417
  // is live and non-empty covers all N.
@@ -380,32 +431,122 @@ export const gatesCoverageCommand = {
380
431
  }
381
432
  }
382
433
  process.stdout.write('\n');
383
- for (const r of report.rules) {
384
- const mark = r.status === 'ok' ? '✓' : r.status === 'empty' ? (r.failOnEmpty ? '✗' : '–') : '✗';
434
+ for (const [i, r] of report.rules.entries()) {
435
+ // The settled status: `partial` is derived by the envelope builder alone.
436
+ const settled = env.rules[i];
437
+ const partial = settled?.status === 'partial';
438
+ const failedSettled = settled?.status === 'failed' || settled?.status === 'error';
439
+ const mark = partial
440
+ ? '~'
441
+ : failedSettled
442
+ ? '✗'
443
+ : r.status === 'ok'
444
+ ? reportsDeadGlobs(r) || coverageWentLiveUnits(r).some((u) => u.mark?.packageName === undefined)
445
+ ? '⚠'
446
+ : '✓'
447
+ : r.status === 'empty'
448
+ ? '–'
449
+ : '✗';
385
450
  process.stdout.write(` ${mark} [${r.plane}] ${r.id}${r.viaExtractor ? ` (via $use:${r.viaExtractor})` : ''}` +
386
451
  ` — ${r.unitsMatched} ${r.unitLabel} across ${r.filesMatched} file(s)\n`);
452
+ if (partial && settled?.shortfall) {
453
+ process.stdout.write(` PARTIAL — ${settled.shortfall}\n`);
454
+ }
455
+ // One glob of a connected rule matching nothing is the rename that
456
+ // silently halves a rule while its siblings keep it green. A rule
457
+ // already reported as matching nothing gets no second line for it.
458
+ if (reportsDeadGlobs(r)) {
459
+ const dead = coverageDeadUnits(r);
460
+ process.stdout.write(` ⚠ ${dead.length} of ${r.globsChecked} glob(s) dead: ${formatDeadUnits(dead)}\n`);
461
+ }
462
+ // Round 13: a glob marked `expectEmpty` whose target now exists — the
463
+ // fence went live. The ✓ is withheld (a LOCAL marker is a stale
464
+ // assertion; `--fail-on-dead-units` fails it); a PACK marker reads as
465
+ // INFO, never a failure — the consumer cannot edit it.
466
+ for (const u of coverageWentLiveUnits(r)) {
467
+ process.stdout.write(` ${u.mark?.packageName !== undefined ? 'i' : '⚠'} ${formatUnitLiveness(u, { causes: false })}\n`);
468
+ }
469
+ // …and the globs it asserts are intended-empty, each as its ONE per-unit
470
+ // line (`formatUnitLiveness`) under a neutral bullet — never a ✓: the ✓
471
+ // and the acceptance belong to `verdictLine`, which prints them only at
472
+ // exit 0 (a FAILED or SKIPPED rule's marked unit is no acceptance).
473
+ for (const u of (r.unitLiveness ?? []).filter((x) => x.state === UnitLivenessState.IntendedEmpty)) {
474
+ process.stdout.write(` · ${formatUnitLiveness(u, { causes: false })}\n`);
475
+ }
476
+ // A LIVE negation is load-bearing narrowing, not a warning: print what
477
+ // it excludes, so the scope the rule does NOT measure is on the page.
478
+ const negations = r.negations ?? [];
479
+ if (negations.length > 0)
480
+ process.stdout.write(` excludes: ${formatNegations(negations)}\n`);
387
481
  if (r.sampleIds.length > 0) {
388
482
  process.stdout.write(` e.g. ${r.sampleIds.join(', ')}\n`);
389
483
  }
390
484
  if (r.status === 'empty') {
391
- process.stdout.write(` ${r.failOnEmpty ? 'FAILED' : 'SKIPPED'} matched nothing; the selector is probably stale\n`);
485
+ // A rule its own `!` emptied is not stale: its inclusion globs matched
486
+ // files and every one was excluded as written. Say that, never "stale".
487
+ const emptiedBy = r.excludedByNegations;
488
+ process.stdout.write(emptiedBy !== undefined
489
+ ? ` ${failedSettled ? 'FAILED' : 'SKIPPED'} — matched nothing after its own negations: ` +
490
+ `every file its inclusion globs select is excluded (${formatNegations(emptiedBy)})\n`
491
+ : r.emptyReason !== undefined
492
+ ? // A fence over a DEAD input (round 13): the settle's own reason.
493
+ ` ${failedSettled ? 'FAILED' : 'SKIPPED'} — ${r.emptyReason}\n`
494
+ : ` ${failedSettled ? 'FAILED' : 'SKIPPED'} — matched nothing; the selector is probably stale\n`);
392
495
  // When the engine knows WHY a correct zero-match is probably not what
393
496
  // the author meant, say so here rather than leaving them to rediscover
394
497
  // it — this is the one dead end `import-edges` reliably produces.
395
498
  if (r.hint)
396
499
  process.stdout.write(` → ${r.hint}\n`);
397
500
  }
501
+ // A hint on a rule that DID match — e.g. a zoned pattern's blank-run
502
+ // backtracking hazard (findBlankRunHazards) — is worth seeing before it bites.
503
+ if (r.status !== 'empty' && r.hint)
504
+ process.stdout.write(` → ${r.hint}\n`);
398
505
  if (r.error)
399
506
  process.stdout.write(` ! ${r.error}\n`);
400
507
  for (const f of r.expectationFailures)
401
508
  process.stdout.write(` ! selfTest: ${f}\n`);
402
509
  }
403
- if (exit === ExitCode.VerifiedPass) {
404
- process.stdout.write('\nEvery rule is connected to something. ✓\n');
510
+ // The dead-selector causes ONCE, as a footer — the ⚠ rows above name each
511
+ // dead unit without them, and `check boundaries` appends them to every
512
+ // dead unit (round 13 review: they were printed nowhere here).
513
+ if (report.rules.some(reportsDeadGlobs)) {
514
+ process.stdout.write(`\n Dead selectors: ${DEAD_SELECTOR_CAUSES}.\n`);
515
+ }
516
+ // THE empty-rule advice with each rule's REAL `fails` (the shared
517
+ // renderer): a FAILING empty rule (failOnEmpty) was told nothing, and a
518
+ // soft one only on a NOT VERIFIED run (the lead below carries it there).
519
+ for (const a of emptyRuleAdviceLines([
520
+ ...report.rules.filter((r) => r.status === 'empty' && r.failOnEmpty).map(() => ({ fails: true })),
521
+ ...(exit === ExitCode.NotVerified ? [] : softEmpty.map(() => ({ fails: false }))),
522
+ ])) {
523
+ process.stdout.write(`\n ${a}.\n`);
405
524
  }
406
- else if (exit === ExitCode.NotVerified) {
407
- process.stdout.write(`\n${softEmpty.length} rule(s) matched nothing NOT a pass. Fix the selector, or set \`failOnEmpty: true\`\n` +
408
- 'once the rule is known to have real subjects (then this becomes a hard failure).\n');
525
+ // THE empty-rule advice (round 13) — one sentence on every plane verb; a
526
+ // soft-empty rule is told how to make it fail, a marker how to say "planned".
527
+ const lead = exit === ExitCode.NotVerified && softEmpty.length > 0
528
+ ? `${softEmpty.length} rule(s) matched nothing — NOT a pass. ${formatEmptyRuleAdvice({ fails: false })}.`
529
+ : undefined;
530
+ // "Every rule is connected" is only the whole truth when no glob inside a
531
+ // connected rule is dead and no LOCAL `expectEmpty` marker went live —
532
+ // otherwise the clean line says so (the ✓ is withheld). A pack marker that
533
+ // went live is INFO: the consumer cannot edit it.
534
+ const localWentLive = report.rules.reduce((n, r) => n + coverageWentLiveUnits(r).filter((u) => u.mark?.packageName === undefined).length, 0);
535
+ const stale = [
536
+ ...(report.deadGlobCount > 0
537
+ ? [`${report.deadGlobCount} glob(s) inside connected rules select or exclude nothing`]
538
+ : []),
539
+ ...(localWentLive > 0 ? [`${localWentLive} expectEmpty unit(s) went live — remove the markers`] : []),
540
+ ];
541
+ const clean = stale.length > 0
542
+ ? `Every rule is connected to something, but ${stale.join(', and ')} (⚠ above).\n` +
543
+ ` Fix them — or pass --${FAIL_ON_DEAD_UNITS_FLAG} to make a dead or went-live unit fail the run.`
544
+ : 'Every rule is connected to something. ✓';
545
+ const line = verdictLine(env, clean, lead);
546
+ if (line)
547
+ process.stdout.write(`\n${line}\n`);
548
+ if (exit === ExitCode.Failure && failOnDeadUnits && stale.length > 0) {
549
+ process.stdout.write(`\n${stale.join('; ')} — and --${FAIL_ON_DEAD_UNITS_FLAG} is set — FAILED.\n`);
409
550
  }
410
551
  return exit;
411
552
  },
@@ -413,12 +554,10 @@ export const gatesCoverageCommand = {
413
554
  export const gatesCheckCommand = {
414
555
  name: 'check',
415
556
  description: 'Run EVERY data-defined rule plane\'s violation check in one pass — one exit code, one JSON envelope. The CI / pre-commit primitive. Distinct from `gates coverage` (are the rules still connected) and `shrk quality` (the whole pre-PR bundle).',
416
- usage: 'shrk gates check [--plane <p>] [--only <ids>] [--changed-only | --since <ref>] [--no-spawn] [--strict] [--json]',
417
- booleanFlags: new Set(['json', 'strict', 'changed-only', 'no-spawn', 'no-cache']),
557
+ usage: 'shrk gates check [--plane <p>] [--only <ids>] [--changed-only | --since <ref>] [--no-spawn] [--allow-empty] [--strict] [--json]',
558
+ booleanFlags: new Set(['json', 'strict', 'changed-only', 'no-spawn', 'no-cache', ALLOW_EMPTY_FLAG]),
559
+ flags: new Set([...GATES_FLAGS, ...CHECK_ONLY_FLAGS]),
418
560
  async run(args) {
419
- const flagReject = rejectUnknownFlags(args);
420
- if (flagReject !== undefined)
421
- return flagReject;
422
561
  const prep = await prepare(args);
423
562
  if (!prep.ok)
424
563
  return prep.code;
@@ -426,11 +565,18 @@ export const gatesCheckCommand = {
426
565
  if (!planes.ok)
427
566
  return ExitCode.UsageError;
428
567
  const json = flagBool(args, 'json');
429
- const filtered = filterRules(prep.value.rules, planes.planes, flagString(args, 'only'));
568
+ const filtered = filterRules(prep.value.rules, planes.planes, flagString(args, 'only'), prep.value.rejectedRules);
430
569
  if (!filtered.ok)
431
570
  return ExitCode.UsageError;
432
- if (filtered.rules.length === 0)
433
- return writeNoRules(json);
571
+ // A pack rule the merge seam rejected is a configured rule that did NOT run
572
+ // (round 12 review, R12-X1): an ERRORED row in the results and the run
573
+ // coverage — never dropped with an advisory `!` line under a ✓. Scope never
574
+ // narrows it out: it has no footprint, and a broken config is broken
575
+ // whatever changed.
576
+ const rejected = filtered.rejected;
577
+ if (filtered.rules.length === 0 && rejected.length === 0) {
578
+ return writeNoRulesVerdict('gates check', args, json, true);
579
+ }
434
580
  const scope = resolveScope(args, prep.value.cwd);
435
581
  if (scope.error) {
436
582
  process.stderr.write(`Cannot scope to the changeset: ${scope.error}\n`);
@@ -438,7 +584,7 @@ export const gatesCheckCommand = {
438
584
  }
439
585
  const { selected, skippedByScope } = narrowToScope(filtered.rules, scope.files);
440
586
  const noSpawn = flagBool(args, 'no-spawn');
441
- const run = selected.length === 0
587
+ const planeRun = selected.length === 0
442
588
  ? { results: [], diagnostics: [] }
443
589
  : runGatePlanes(selected, {
444
590
  cwd: prep.value.cwd,
@@ -447,6 +593,8 @@ export const gatesCheckCommand = {
447
593
  ...(noSpawn ? { noSpawn: true } : {}),
448
594
  ...(await inspectionIfNeeded(prep.value.cwd, selected).then((i) => i ? { inspection: i } : {})),
449
595
  });
596
+ const run = { ...planeRun, results: [...planeRun.results, ...rejected] };
597
+ const inScope = selected.length + rejected.length;
450
598
  // Only an ERROR-severity failure blocks. A warning-severity rule reports
451
599
  // without failing, exactly as its own plane's verb does.
452
600
  //
@@ -474,16 +622,42 @@ export const gatesCheckCommand = {
474
622
  // dropped the drift half. Nobody asked for that, and it is exactly the
475
623
  // silent-green this engine exists to prevent, so it is `2`.
476
624
  // Matches `check wiring --changed-only`, which draws the same line.
477
- const exit = blocking.length > 0
625
+ //
626
+ // An EMPTY selection (no rule's footprint intersects the changeset)
627
+ // proposes 0 and lets the run coverage decide: expected 0 is a shortfall
628
+ // (2) unless --allow-empty accepted it. Proposing 2 here would make the
629
+ // valve unreachable on exactly the case it exists for.
630
+ const proposed = blocking.length > 0
478
631
  ? ExitCode.Failure
479
- : evaluated === 0 || skipped > 0
632
+ : (evaluated === 0 && selected.length > 0) || skipped > 0
480
633
  ? ExitCode.NotVerified
481
634
  : ExitCode.VerifiedPass;
635
+ // Settle first, render second. The envelope is built for text AND JSON; a
636
+ // rule that passed over part of its scope (a subset wiring rule whose
637
+ // declared selector never produced some registered tokens) is `partial`, so
638
+ // the run is NOT verified — the scope-narrowed rules are out of `expected`,
639
+ // exactly as they are out of the question.
640
+ const unexamined = run.results
641
+ .filter((r) => r.status === 'skipped' || r.status === 'error')
642
+ .map((r) => r.id);
643
+ const env = buildGateEnvelope('gates check', proposed, run.results, {
644
+ unit: 'rules',
645
+ expected: inScope,
646
+ examined: evaluated,
647
+ ...(unexamined.length > 0 ? { unexamined, reason: 'checked nothing or could not run' } : {}),
648
+ ...(inScope === 0
649
+ ? { reason: `no rule's footprint intersects the changeset (${skippedByScope} skipped by scope)` }
650
+ : {}),
651
+ ...allowEmptyValve(args, inScope),
652
+ });
653
+ const exit = env.exit;
482
654
  const diagnostics = [...run.diagnostics, ...prep.value.planeDiagnostics];
483
655
  if (json) {
484
656
  process.stdout.write(asJson({
485
657
  schema: SCHEMA,
486
- configured: filtered.rules.length,
658
+ configured: filtered.rules.length + rejected.length,
659
+ // Pack rules the merge seam refused — errored rows in `gate.rules`.
660
+ rejected: rejected.length,
487
661
  selected: selected.length,
488
662
  evaluated,
489
663
  skipped,
@@ -493,16 +667,22 @@ export const gatesCheckCommand = {
493
667
  strict,
494
668
  failed: blocking.length,
495
669
  failedWarnings: failedWarnings.length,
496
- verdict: blocking.length > 0 ? 'errors' : evaluated === 0 ? 'not-verified' : 'pass',
670
+ verdict: exit === ExitCode.Failure ? 'errors' : exit === ExitCode.VerifiedPass ? 'pass' : 'not-verified',
497
671
  diagnostics,
498
672
  exitCode: exit,
499
- gate: buildGateEnvelope('gates check', exit, run.results),
673
+ gate: env,
500
674
  }) + '\n');
501
675
  return exit;
502
676
  }
503
677
  process.stdout.write(header('Gate check — every rule plane'));
504
- process.stdout.write(kv('evaluated', `${evaluated} of ${filtered.rules.length}` +
505
- (skippedByScope > 0 ? ` (${skippedByScope} skipped by scope)` : '')) + '\n');
678
+ // Round 13 (K6): a rule accepted as intended-empty examined 0 files — the
679
+ // printed count is the envelope's (`gate.evaluated`, which leaves it out)
680
+ // and the accepted rules are named apart. The local `evaluated` keeps
681
+ // counting them for the run coverage and the "nothing ran" proposal above.
682
+ process.stdout.write(kv('evaluated', `${env.evaluated} of ${filtered.rules.length + rejected.length}` +
683
+ acceptedEmptyNote(env.acceptedEmpty) +
684
+ (skippedByScope > 0 ? ` (${skippedByScope} skipped by scope)` : '') +
685
+ (rejected.length > 0 ? ` (${rejected.length} rejected at the pack-plane merge seam — NOT evaluated)` : '')) + '\n');
506
686
  if (scope.files) {
507
687
  process.stdout.write(kv('scope', `changed-only (${scope.files.length} file(s))`) + '\n');
508
688
  }
@@ -510,17 +690,42 @@ export const gatesCheckCommand = {
510
690
  process.stdout.write(kv('mode', '--no-spawn — shell-executing checks skipped') + '\n');
511
691
  process.stdout.write(kv('violations', `${blocking.length} blocking rule(s), ${failedWarnings.length} warning rule(s)` +
512
692
  (strict && failedWarnings.length > 0 ? ' — --strict: warnings block' : '')) + '\n\n');
693
+ // THE shared unit-state block (round 13, K2) — the plane verbs' own: a dead
694
+ // unit of a rule that still matched, a LOCAL expectEmpty marker whose
695
+ // target appeared (its row reads ⚠, the ✓ is withheld, the exit is
696
+ // unchanged), a pack marker as INFO.
697
+ const unitNotes = unitStateNotes(planeRun.noteRows ?? []);
698
+ // Rendered from the SETTLED rules, so `partial` and its shortfall show in
699
+ // text exactly as they do in `--json`.
513
700
  for (const plane of GATE_PLANES) {
514
- const inPlane = run.results.filter((r) => r.type === plane);
701
+ const inPlane = env.rules.filter((r) => r.type === plane);
515
702
  if (inPlane.length === 0)
516
703
  continue;
517
704
  for (const r of inPlane) {
518
- const mark = r.status === 'passed' ? '✓' : r.status === 'skipped' ? '–' : r.severity === 'error' ? '✗' : '!';
705
+ const mark = r.status === 'passed'
706
+ ? unitNotes.staleIds.has(r.id)
707
+ ? '⚠'
708
+ : '✓'
709
+ : r.status === 'partial'
710
+ ? '~'
711
+ : r.status === 'skipped'
712
+ ? '–'
713
+ : r.severity === 'error'
714
+ ? '✗'
715
+ : '!';
519
716
  const counts = Object.entries(r.counts).map(([k, v]) => `${k} ${v}`).join(', ');
520
717
  process.stdout.write(` ${mark} [${plane}] ${r.id}${counts ? ` (${counts})` : ''}\n`);
521
718
  if (r.status === 'skipped' && r.skipReason) {
522
719
  process.stdout.write(` SKIPPED — ${r.skipReason}\n`);
523
720
  }
721
+ // A failOnEmpty rule that matched nothing FAILS with no violation to
722
+ // list, so its reason is the only line that says why.
723
+ if (r.status === 'failed' && r.skipReason && r.violations.length === 0) {
724
+ process.stdout.write(` FAILED — ${r.skipReason}\n`);
725
+ }
726
+ if (r.status === 'partial' && r.shortfall) {
727
+ process.stdout.write(` PARTIAL — ${r.shortfall}\n`);
728
+ }
524
729
  if (r.error)
525
730
  process.stdout.write(` ! ${r.error}\n`);
526
731
  for (const v of r.violations.slice(0, 10)) {
@@ -537,31 +742,84 @@ export const gatesCheckCommand = {
537
742
  }
538
743
  for (const d of diagnostics)
539
744
  process.stdout.write(` ! ${d}\n`);
745
+ process.stdout.write(unitNotes.text);
540
746
  if (strict && failedWarnings.length > 0 && exit === ExitCode.Failure) {
541
747
  process.stdout.write(`\n${failedWarnings.length} warning rule(s) reported findings and --strict promoted them to failures.\n`);
542
748
  }
543
- else if (exit === ExitCode.VerifiedPass && failedWarnings.length > 0) {
544
- // A warning-severity rule reports without blocking, but the banner must
545
- // still say it FIRED. "Everything passed" next to a printed violation is
546
- // the kind of half-truth that trains people to stop reading the output.
547
- process.stdout.write(`\nNo blocking violations, but ${failedWarnings.length} warning rule(s) reported findings.\n` +
548
- (skippedByScope > 0 ? `(${skippedByScope} rule(s) outside the changeset were not run)\n` : ''));
749
+ // The final line comes from the SETTLED verdict only. A warning-severity
750
+ // rule reports without blocking, but the banner must still say it FIRED —
751
+ // "everything passed" next to a printed violation is the kind of half-truth
752
+ // that trains people to stop reading the output.
753
+ if (rejected.length > 0) {
754
+ process.stdout.write(`\n${rejected.length} pack rule(s) failed validation at the pack-plane merge seam and never ran — FAILED.\n` +
755
+ ' Fix the pack (or override the rule locally); `shrk packs contributions` names every rejected entry.\n');
549
756
  }
550
- else if (exit === ExitCode.VerifiedPass && skippedByScope === 0) {
551
- process.stdout.write('\nEvery declared rule ran and passed. ✓\n');
552
- }
553
- else if (exit === ExitCode.NotVerified) {
554
- process.stdout.write(`\n${skipped} rule(s) in scope checked NOTHING — this is not a pass.\n` +
555
- 'Run `shrk gates coverage` to see which selectors are stale.\n');
556
- }
557
- else if (skippedByScope > 0) {
558
- process.stdout.write(`\nEvery rule in scope passed. ✓ (${skippedByScope} outside the changeset were not run)\n`);
757
+ // A dead unit or a LOCAL went-live marker listed above withholds the ✓ (K2).
758
+ const clean = qualifyCleanForUnits(inScope === 0
759
+ ? 'Nothing in scope — accepted.'
760
+ : failedWarnings.length > 0
761
+ ? `No blocking violations, but ${failedWarnings.length} warning rule(s) reported findings.` +
762
+ (skippedByScope > 0 ? `\n(${skippedByScope} rule(s) outside the changeset were not run)` : '')
763
+ : skippedByScope > 0
764
+ ? `Every rule in scope passed. ✓ (${skippedByScope} outside the changeset were not run)`
765
+ : 'Every declared rule ran and passed. ✓', unitNotes);
766
+ // A rule its own negations emptied is not a stale selector — its SKIPPED
767
+ // line says so, and the lead must not send the reader to fix a selector
768
+ // that is fine (round 12 review, R12-DOC-2).
769
+ const emptiedByOwn = run.results.filter((r) => r.status === 'skipped' && (r.skipReason ?? '').includes('its own negations')).length;
770
+ const lead = proposed === ExitCode.NotVerified && skipped > 0
771
+ ? `${skipped} rule(s) in scope checked NOTHING — this is not a pass.\n` +
772
+ (emptiedByOwn === skipped
773
+ ? 'Each was emptied by its own negations (see SKIPPED above) — not a stale selector.'
774
+ : `Run \`shrk gates coverage\` to see which selectors are stale` +
775
+ (emptiedByOwn > 0 ? ` (${emptiedByOwn} emptied by their own negations are not).` : '.'))
776
+ : undefined;
777
+ const line = verdictLine(env, clean, lead);
778
+ if (line)
779
+ process.stdout.write(`\n${line}\n`);
780
+ if (inScope === 0 && exit === ExitCode.NotVerified) {
781
+ process.stdout.write(` Pass --${ALLOW_EMPTY_FLAG} to accept an empty changeset explicitly.\n`);
559
782
  }
560
783
  return exit;
561
784
  },
562
785
  };
563
- /** Parse `--wiring 'declared=<glob>:<pattern> registered=<glob>:<pattern>'`. */
564
- function parseInlineWiring(spec) {
786
+ /**
787
+ * The "no rules declared" landing for a VERDICT verb: the same explanation as
788
+ * {@link writeNoRules}, settled through the envelope so the JSON carries `gate`
789
+ * and the exit is `2` — unless the verb honours `--allow-empty` and it was
790
+ * passed, in which case the acceptance is printed.
791
+ */
792
+ function writeNoRulesVerdict(verb, args, json, honoursAllowEmpty) {
793
+ const env = buildGateEnvelope(verb, ExitCode.VerifiedPass, [], {
794
+ unit: 'rules',
795
+ expected: 0,
796
+ examined: 0,
797
+ reason: 'no data-defined rules are declared (on the selected plane)',
798
+ ...(honoursAllowEmpty ? allowEmptyValve(args, 0) : {}),
799
+ });
800
+ if (json) {
801
+ process.stdout.write(asJson({ schema: SCHEMA, rules: [], total: 0, exitCode: env.exit, gate: env }) + '\n');
802
+ return env.exit;
803
+ }
804
+ writeNoRules(false);
805
+ process.stdout.write(`\n${verdictLine(env, 'Nothing declared — accepted.')}\n`);
806
+ if (honoursAllowEmpty && env.exit !== ExitCode.VerifiedPass) {
807
+ process.stdout.write(` Pass --${ALLOW_EMPTY_FLAG} to accept an empty rule set explicitly.\n`);
808
+ }
809
+ return env.exit;
810
+ }
811
+ /**
812
+ * Parse `--wiring 'declared=<glob>:<pattern> registered=<glob>:<pattern>'`.
813
+ *
814
+ * The glob/pattern split is at the FIRST colon: a project-relative POSIX glob
815
+ * never contains one, while a regex often does (`name: '(\w+)'`). Splitting at
816
+ * the last colon turned such a pattern's prefix into the glob, which then
817
+ * matched 0 files — the exact dead end this REPL exists to prevent. `flags`
818
+ * (from `--flags`) applies to BOTH sides, so an anchored inline pattern
819
+ * compiles exactly as the same rule does from config (`flags: 'm'`) — one
820
+ * source shape, one compile path.
821
+ */
822
+ function parseInlineWiring(spec, flags) {
565
823
  const sides = new Map();
566
824
  // Split on whitespace that precedes a `<side>=`, so a glob may contain none.
567
825
  for (const part of spec.trim().split(/\s+(?=(?:declared|registered)=)/)) {
@@ -573,7 +831,7 @@ function parseInlineWiring(spec) {
573
831
  return { error: `unknown side "${side}" — use declared= or registered=` };
574
832
  }
575
833
  const rest = part.slice(eq + 1);
576
- const colon = rest.lastIndexOf(':');
834
+ const colon = rest.indexOf(':');
577
835
  if (colon <= 0)
578
836
  return { error: `"${side}=" needs <glob>:<pattern>` };
579
837
  sides.set(side, { files: [rest.slice(0, colon)], match: rest.slice(colon + 1) });
@@ -587,28 +845,175 @@ function parseInlineWiring(spec) {
587
845
  files: side.files,
588
846
  extract: 'regex-capture',
589
847
  pattern: side.match,
848
+ ...(flags !== undefined && flags.length > 0 ? { flags } : {}),
590
849
  });
591
850
  return {
592
851
  rule: { id: '(try)', declared: toSource(declared), registered: toSource(registered) },
593
852
  };
594
853
  }
854
+ /** True when `pattern` holds a `^` or `$` outside a character class (an escape skips the next char). */
855
+ function hasLineAnchor(pattern) {
856
+ let inClass = false;
857
+ for (let i = 0; i < pattern.length; i += 1) {
858
+ const ch = pattern[i];
859
+ if (ch === '\\') {
860
+ i += 1;
861
+ continue;
862
+ }
863
+ if (inClass) {
864
+ if (ch === ']')
865
+ inClass = false;
866
+ continue;
867
+ }
868
+ if (ch === '[') {
869
+ inClass = true;
870
+ continue;
871
+ }
872
+ if (ch === '^' || ch === '$')
873
+ return true;
874
+ }
875
+ return false;
876
+ }
877
+ /**
878
+ * Advisory notes on a candidate's patterns. A `^`/`$` anchor without the `m`
879
+ * flag matches only at the start/end of the whole FILE (every engine compiles
880
+ * with `g` plus exactly the flags the rule spells) — the "0 matches" that sends
881
+ * an author off rewriting a correct pattern. Config and the dry-run behave the
882
+ * same way; the note says how to spell the flag on each.
883
+ */
884
+ function anchorNotes(view, inline) {
885
+ const how = inline ? 'pass --flags m' : "spell flags: 'm' on it, as a config rule would";
886
+ const notes = [];
887
+ const check = (label, pattern, flags) => {
888
+ if (pattern === undefined || !hasLineAnchor(pattern) || (flags ?? '').includes('m'))
889
+ return;
890
+ notes.push(`${label}: /${pattern}/ has a ^/$ anchor but no m flag, so it matches only at file start/end — ${how}`);
891
+ };
892
+ for (const { label, source } of gateRuleLabeledSources(view))
893
+ check(label, source.pattern, source.flags);
894
+ if (view.plane === 'policy') {
895
+ const rule = view.raw;
896
+ check('pattern', rule.pattern, rule.flags);
897
+ }
898
+ return notes;
899
+ }
900
+ /** The `selfTest` summary `gates try --json` carries, for every plane. */
901
+ function selfTestPayload(cov, declared) {
902
+ const checks = cov.selfTestChecks ?? [];
903
+ return {
904
+ declared,
905
+ evaluated: checks.length > 0 && checks.every((c) => c.status !== 'not-evaluable'),
906
+ held: checks.filter((c) => c.status === 'held').length,
907
+ failed: checks.filter((c) => c.status === 'failed').length,
908
+ notEvaluable: checks.filter((c) => c.status === 'not-evaluable').length,
909
+ checks,
910
+ failures: cov.expectationFailures,
911
+ consulted: cov.consulted,
912
+ };
913
+ }
914
+ /** The `selfTest` block of `gates try`: EVERY expectation with its result, from the one evaluator. */
915
+ function writeSelfTestBlock(ruleId, cov, declared) {
916
+ const checks = cov.selfTestChecks ?? [];
917
+ if (!declared) {
918
+ process.stdout.write(`\n selfTest: none declared — once the rule is in config, \`shrk gates scaffold-selftest ${ruleId}\` writes one from what it matches.\n`);
919
+ return;
920
+ }
921
+ if (checks.length === 0) {
922
+ process.stdout.write('\n selfTest: declared with no expectations — it asserts nothing.\n');
923
+ return;
924
+ }
925
+ const failed = checks.filter((c) => c.status === 'failed').length;
926
+ const unmeasured = checks.filter((c) => c.status === 'not-evaluable').length;
927
+ process.stdout.write(`\n selfTest (${checks.length} expectation(s)` +
928
+ (failed > 0 ? `, ${failed} FAILED` : '') +
929
+ (unmeasured > 0 ? `, ${unmeasured} NOT evaluable` : '') +
930
+ '):\n');
931
+ for (const c of checks) {
932
+ const tag = c.status === 'held' ? 'held ' : c.status === 'failed' ? 'FAILED ' : 'NOT EVALUABLE';
933
+ process.stdout.write(` ${tag} ${c.field}: ${c.message}\n`);
934
+ }
935
+ }
936
+ function writeNotes(notes) {
937
+ for (const n of notes)
938
+ process.stdout.write(`\n note: ${n}\n`);
939
+ }
940
+ /**
941
+ * The dry-run's final line, from its SETTLED verdict: a failure says what
942
+ * broke, a not-verified run names what it did not examine, and only a clean
943
+ * run gets the clean sentence.
944
+ */
945
+ function writeTryVerdict(settled, cov, lead) {
946
+ // Round 13: the text prints the selector units its `--json` already carries
947
+ // — the rule-authoring REPL is exactly where a planned glob gets written, so
948
+ // a dead glob, a went-live marker or an accepted planned unit is never
949
+ // silent here.
950
+ const dead = reportsDeadGlobs(cov) ? coverageDeadUnits(cov) : [];
951
+ if (dead.length > 0) {
952
+ process.stdout.write(`\n ⚠ ${dead.length} of ${cov.globsChecked} glob(s) dead: ${formatDeadUnits(dead)}\n`);
953
+ // The causes ONCE, as a footer — exactly as `gates coverage` prints them.
954
+ process.stdout.write(` Dead selectors: ${DEAD_SELECTOR_CAUSES}.\n`);
955
+ }
956
+ const wentLive = coverageWentLiveUnits(cov);
957
+ for (const u of wentLive) {
958
+ process.stdout.write(` ${u.mark?.packageName !== undefined ? 'i' : '⚠'} ${formatUnitLiveness(u, { causes: false })}\n`);
959
+ }
960
+ // Each intended-empty unit as its ONE per-unit line under a neutral bullet —
961
+ // never a ✓: the ✓ and the acceptance belong to `verdictLine` (exit 0 only),
962
+ // so a FAILED or SKIPPED candidate never reads "asserted empty ✓".
963
+ for (const u of (cov.unitLiveness ?? []).filter((x) => x.state === UnitLivenessState.IntendedEmpty)) {
964
+ process.stdout.write(` · ${formatUnitLiveness(u, { causes: false })}\n`);
965
+ }
966
+ if (settled.exit === ExitCode.Failure) {
967
+ const why = cov.status === 'error'
968
+ ? `the candidate is misconfigured: ${cov.error ?? 'it could not run'}`
969
+ : `${cov.expectationFailures.length} selfTest expectation(s) failed (listed above)`;
970
+ process.stdout.write(`\n FAILED — ${why}.\n`);
971
+ }
972
+ // "Connected over everything" is only the whole truth when no glob inside
973
+ // the candidate is dead and no marker went live.
974
+ const stale = [
975
+ ...(dead.length > 0 ? [`${dead.length} glob(s) inside it select or exclude nothing`] : []),
976
+ ...(wentLive.length > 0 ? [`${wentLive.length} expectEmpty unit(s) went live`] : []),
977
+ ];
978
+ const clean = stale.length > 0
979
+ ? `The candidate is connected, but ${stale.join(' and ')} (⚠ above) — fix them before adding it to config.`
980
+ : (cov.selfTestChecks ?? []).length > 0
981
+ ? 'The candidate is connected over everything it was asked to examine, and every selfTest expectation held.'
982
+ : 'The candidate is connected over everything it was asked to examine.';
983
+ const line = verdictLine(settled, clean, lead);
984
+ if (line)
985
+ process.stdout.write(`\n ${line.split('\n').join('\n ')}\n`);
986
+ }
595
987
  export const gatesTryCommand = {
596
988
  name: 'try',
597
- description: 'Dry-run a rule spec against the live tree WITHOUT adding it to config — the rule-authoring REPL. Prints the resolved sets and the diff, so a selector is tightened before it is committed.',
598
- usage: "shrk gates try --rule-file <rule.json> [--plane <p>] [--full | --limit N] | --wiring 'declared=<glob>:<pat> registered=<glob>:<pat>' [--json]",
989
+ description: "Dry-run a rule spec against the live tree WITHOUT adding it to config — the rule-authoring REPL. Prints the resolved sets and the diff, and evaluates the candidate's selfTest with the SAME evaluator `gates coverage` uses (exit 1 when an expectation fails), so a selector is tightened before it is committed.",
990
+ usage: "shrk gates try --rule-file <rule.json> [--plane <p>] [--full | --limit N] | --wiring 'declared=<glob>:<pat> registered=<glob>:<pat>' [--flags <f>] [--json]",
599
991
  booleanFlags: new Set(['json', 'full']),
992
+ flags: new Set([...GATES_FLAGS, ...TRY_ONLY_FLAGS]),
600
993
  async run(args) {
601
- const flagReject = rejectUnknownFlags(args);
602
- if (flagReject !== undefined)
603
- return flagReject;
994
+ // `--flags` with no value (or an empty one) would otherwise be dropped in
995
+ // silence: the run goes ahead with no flags, looking as if it took them.
996
+ const flagsRaw = args.flags.get('flags');
997
+ if (flagsRaw === true || flagsRaw === '') {
998
+ process.stderr.write('--flags needs a value, e.g. --flags m\n');
999
+ return ExitCode.UsageError;
1000
+ }
604
1001
  const ruleFile = flagString(args, 'rule-file') ?? args.positional[0];
605
1002
  const inline = flagString(args, 'wiring');
1003
+ const inlineFlags = flagString(args, 'flags');
606
1004
  if (!ruleFile && !inline) {
607
1005
  process.stderr.write('Usage: shrk gates try --rule-file <rule.json> [--plane <p>] [--full | --limit N]\n' +
608
- " shrk gates try --wiring 'declared=<glob>:<pat> registered=<glob>:<pat>'\n" +
609
- ' Runs the extraction and prints the resolved sets — config is never touched.\n' +
1006
+ " shrk gates try --wiring 'declared=<glob>:<pat> registered=<glob>:<pat>' [--flags <f>]\n" +
1007
+ ' Runs the extraction, evaluates the selfTest and prints the resolved sets — config is never touched.\n' +
610
1008
  ' --full dump the WHOLE extracted set (default: first 5)\n' +
611
- ' --limit N dump the first N\n');
1009
+ ' --limit N dump the first N\n' +
1010
+ ' --flags <f> regex flags for BOTH inline patterns (m: a ^/$ anchor matches per line)\n');
1011
+ return ExitCode.UsageError;
1012
+ }
1013
+ // A flag that changes nothing must never look like a satisfied request: a
1014
+ // --rule-file spells `flags` per source, exactly as config does.
1015
+ if (inlineFlags !== undefined && !inline) {
1016
+ process.stderr.write("--flags applies to the inline --wiring form only; a --rule-file spells `flags: '<f>'` on each source, as config does.\n");
612
1017
  return ExitCode.UsageError;
613
1018
  }
614
1019
  const cwd = resolveCwd(args);
@@ -618,19 +1023,11 @@ export const gatesTryCommand = {
618
1023
  // declared rule is read, and nothing is written).
619
1024
  const loaded = await resolveProjectConfig(cwd);
620
1025
  const extractors = loaded.ok ? (loaded.value.config.extractors ?? {}) : {};
621
- const excludeDirs = loaded.ok
622
- ? (() => {
623
- const rel = nodePath
624
- .relative(cwd, loaded.value.sharkcraftDir)
625
- .split(nodePath.sep)
626
- .join('/');
627
- return rel && !rel.startsWith('..') ? [rel] : [];
628
- })()
629
- : [];
1026
+ const excludeDirs = loaded.ok ? planeScanExcludeDirs(cwd, loaded.value.sharkcraftDir) : [];
630
1027
  let raw;
631
1028
  let plane = 'wiring';
632
1029
  if (inline) {
633
- const parsed = parseInlineWiring(inline);
1030
+ const parsed = parseInlineWiring(inline, inlineFlags);
634
1031
  if (parsed.error) {
635
1032
  process.stderr.write(`Invalid --wiring spec: ${parsed.error}\n`);
636
1033
  return ExitCode.UsageError;
@@ -675,10 +1072,24 @@ export const gatesTryCommand = {
675
1072
  process.stderr.write(`Invalid ${plane} rule: ${summary}\n`);
676
1073
  return ExitCode.UsageError;
677
1074
  }
1075
+ // Normalise the candidate's markable lists exactly as the loader does
1076
+ // (round 13, `normalizePlaneRule`): plain string lists plus the
1077
+ // `expectEmptyUnits` ledger, so a planned `{ pattern, expectEmpty: true }`
1078
+ // unit is judged here as it will be once pasted into config.
1079
+ const normalizedCandidate = normalizePlaneRule(plane, parsed.data);
1080
+ if (!normalizedCandidate.ok) {
1081
+ const summary = normalizedCandidate.error.message;
1082
+ if (json)
1083
+ process.stdout.write(asJson({ schema: SCHEMA, plane, valid: false, error: summary }) + '\n');
1084
+ else
1085
+ process.stderr.write(`Invalid ${plane} rule: ${summary}\n`);
1086
+ return ExitCode.UsageError;
1087
+ }
1088
+ const candidate = normalizedCandidate.value;
678
1089
  // Resolve `$use` exactly as the loader would, so a candidate referencing a
679
1090
  // shared extractor is tried against the real shared definition.
680
1091
  const planeKey = PLANE_CONFIG_KEY[plane];
681
- const resolved = resolvePlaneExtractors({ [planeKey]: [parsed.data] }, extractors);
1092
+ const resolved = resolvePlaneExtractors({ [planeKey]: [candidate] }, extractors);
682
1093
  if (resolved.errors.length > 0) {
683
1094
  const summary = resolved.errors.map((e) => e.message).join('; ');
684
1095
  if (json)
@@ -687,33 +1098,75 @@ export const gatesTryCommand = {
687
1098
  process.stderr.write(`Unresolvable extractor reference: ${summary}\n`);
688
1099
  return ExitCode.UsageError;
689
1100
  }
1101
+ // …and judged on its MERGED shape, as the loader judges a local rule (THE
1102
+ // post-resolution check, core) — a candidate that would fail config load is
1103
+ // refused here, never tried as "misconfigured" (round 12 review, R12-X3).
1104
+ const mergedProblems = validateResolvedPlaneSources(resolved);
1105
+ if (mergedProblems.length > 0) {
1106
+ const summary = mergedProblems.map((p) => `${p.path} ${p.message}`).join('; ');
1107
+ if (json)
1108
+ process.stdout.write(asJson({ schema: SCHEMA, plane, valid: false, error: summary }) + '\n');
1109
+ else
1110
+ process.stderr.write(`Invalid ${plane} rule: ${summary}\n`);
1111
+ return ExitCode.UsageError;
1112
+ }
690
1113
  const resolvedByKey = resolved;
691
- const rule = resolvedByKey[planeKey]?.[0] ?? parsed.data;
692
- const view = {
693
- id: rule.id ?? rule.name ?? '(try)',
694
- plane,
695
- severity: 'error',
696
- failOnEmpty: false,
697
- raw: rule,
698
- };
1114
+ const rule = resolvedByKey[planeKey]?.[0] ?? candidate;
1115
+ // The candidate's view comes from the ONE normaliser the loader's rules go
1116
+ // through, so severity, failOnEmpty and selfTest mean here exactly what
1117
+ // they will mean once the rule is pasted into config. A hand-built view
1118
+ // was how the dry-run discarded `selfTest` without a word.
1119
+ const view = collectGateRules({ [planeKey]: [rule] })[0];
1120
+ if (!view) {
1121
+ process.stderr.write(`The candidate did not normalise to a ${plane} rule.\n`);
1122
+ return ExitCode.UsageError;
1123
+ }
1124
+ const inspection = await inspectionIfNeeded(cwd, [view]);
1125
+ // What the candidate MATCHED, and its selfTest — through the same plane
1126
+ // adapters and the same evaluator `gates coverage` runs.
1127
+ const cov = buildGateCoverage(cwd, [view], excludeDirs, extractors, true, inspection).rules[0];
1128
+ const declared = view.selfTest !== undefined;
1129
+ const notes = anchorNotes(view, inline !== undefined);
1130
+ // A broken selfTest, or a candidate that cannot run, fails the dry-run.
1131
+ // Violations do NOT: finding them is the rule doing its job.
1132
+ const broken = cov.status === 'error' || cov.status === 'failed-expectation';
699
1133
  // The wiring plane already has a BOTH-SIDES explainer (resolved declared
700
1134
  // set, resolved registered set, and the set-difference between them) — the
701
1135
  // exact view an author tightening a selector needs. Reuse it rather than
702
1136
  // printing a second, thinner one that could disagree with `gates explain`.
703
1137
  if (plane === 'wiring') {
704
1138
  const explain = explainWiring(cwd, rule, { excludeDirs });
705
- const code = renderWiringExplain(explain, json);
706
- if (!json) {
707
- process.stdout.write('\n Nothing was written. Paste the rule into `wiringRules[]` to keep it.\n');
1139
+ // An empty side means the selector proved nothing — not a pass — unless
1140
+ // the empty result is the INTENDED one (round 13: every source-side glob
1141
+ // marked `expectEmpty`, accepted by the one settle). A partial candidate
1142
+ // settles to 2 on the engine's own coverage, the guard every verdict verb
1143
+ // uses; the candidate's `expectEmpty` acceptance joins it (the one fold,
1144
+ // `ruleVerdictRecords`), so it is printed at exit 0.
1145
+ const acceptedEmpty = cov.emptiness === RuleEmptiness.IntendedEmpty || cov.emptiness === RuleEmptiness.AssertedEmptyOutput;
1146
+ const emptySide = !acceptedEmpty && (explain.declared.distinctCount === 0 || explain.registered.distinctCount === 0);
1147
+ const settled = settleVerdict(broken ? ExitCode.Failure : emptySide ? ExitCode.NotVerified : ExitCode.VerifiedPass,
1148
+ // The engine's own acceptance (the explain now settles a planned rule
1149
+ // exactly as `check wiring` does); coverage's is the same claim.
1150
+ ruleVerdictRecords(explain.coverage, explain.unitAcceptance ?? cov.unitAcceptance));
1151
+ if (json) {
1152
+ // The explain payload stays top-level (existing consumers keep
1153
+ // working); the selfTest and the settled outcome are additive keys.
1154
+ process.stdout.write(asJson({
1155
+ ...explain,
1156
+ selfTest: selfTestPayload(cov, declared),
1157
+ ...(notes.length > 0 ? { notes } : {}),
1158
+ exitCode: settled.exit,
1159
+ settled,
1160
+ }) + '\n');
1161
+ return settled.exit;
708
1162
  }
709
- // An empty side means the selector proved nothing — not a pass.
710
- return explain.declared.distinctCount === 0 || explain.registered.distinctCount === 0
711
- ? ExitCode.NotVerified
712
- : code;
1163
+ renderWiringExplain(explain, false);
1164
+ writeSelfTestBlock(view.id, cov, declared);
1165
+ writeNotes(notes);
1166
+ process.stdout.write('\n Nothing was written. Paste the rule into `wiringRules[]` to keep it.\n');
1167
+ writeTryVerdict(settled, cov, emptySide ? 'A side of the candidate matched nothing — it proved nothing.' : undefined);
1168
+ return settled.exit;
713
1169
  }
714
- // The candidate's COVERAGE (what it matched) is the answer the author
715
- // needs, and the violation run tells them whether the rule would be green.
716
- const coverage = buildGateCoverage(cwd, [view], excludeDirs, extractors, true, await inspectionIfNeeded(cwd, [view]));
717
1170
  // `noSpawn` is not a performance choice here: a `--rule-file` is arbitrary
718
1171
  // JSON, and honouring a `regen` / `compute.run` from it would turn a
719
1172
  // read-only preview into shell execution from an untrusted file.
@@ -721,11 +1174,35 @@ export const gatesTryCommand = {
721
1174
  cwd,
722
1175
  excludeDirs,
723
1176
  noSpawn: true,
724
- ...(await inspectionIfNeeded(cwd, [view]).then((i) => (i ? { inspection: i } : {}))),
1177
+ ...(inspection ? { inspection } : {}),
725
1178
  });
726
1179
  const result = run.results[0];
727
- const cov = coverage.rules[0];
728
1180
  const allIds = cov.allIds ?? cov.sampleIds;
1181
+ // Settled over what the candidate matched AND what its check examined (a
1182
+ // skipped shell half, an unreadable ledger), so a dry-run never reads
1183
+ // clean over a half it did not examine.
1184
+ // What the candidate matched and what its check examined are often the
1185
+ // SAME gap (one unread file): settle over each distinct shortfall once, so
1186
+ // the NOT VERIFIED line does not repeat it.
1187
+ // Round 13: each side's `expectEmpty` acceptance joins its record (the one
1188
+ // fold, `ruleVerdictRecords`), and a record both sides carry — the same
1189
+ // acceptance, reached by coverage and by the plane check — is settled once.
1190
+ const seenRecord = new Set();
1191
+ const tryCoverage = [
1192
+ ...ruleVerdictRecords(cov.coverage, cov.unitAcceptance),
1193
+ ...(result ? ruleVerdictRecords(result.coverage, result.unitAcceptance) : []),
1194
+ ].filter((c) => {
1195
+ const { subject: _subject, ...claim } = c;
1196
+ const key = JSON.stringify(claim);
1197
+ if (seenRecord.has(key))
1198
+ return false;
1199
+ seenRecord.add(key);
1200
+ return true;
1201
+ });
1202
+ const settled = settleVerdict(broken ? ExitCode.Failure : cov.status === 'empty' ? ExitCode.NotVerified : ExitCode.VerifiedPass, tryCoverage.filter((c, i) => {
1203
+ const s = coverageShortfall(c);
1204
+ return s === undefined || tryCoverage.findIndex((d) => coverageShortfall(d) === s) === i;
1205
+ }));
729
1206
  if (json) {
730
1207
  process.stdout.write(asJson({
731
1208
  schema: 'sharkcraft.gates-try/v1',
@@ -734,13 +1211,20 @@ export const gatesTryCommand = {
734
1211
  coverage: cov,
735
1212
  ...(cov.hint ? { hint: cov.hint } : {}),
736
1213
  result: result ?? null,
1214
+ selfTest: selfTestPayload(cov, declared),
1215
+ ...(notes.length > 0 ? { notes } : {}),
737
1216
  note: 'nothing was written — paste the rule into sharkcraft.config.ts to keep it',
1217
+ exitCode: settled.exit,
1218
+ settled,
738
1219
  }) + '\n');
739
- return cov.status === 'empty' ? ExitCode.NotVerified : ExitCode.VerifiedPass;
1220
+ return settled.exit;
740
1221
  }
741
1222
  process.stdout.write(header(`gates try — candidate ${plane} rule "${view.id}"`));
742
- process.stdout.write(kv('files matched', String(cov.filesMatched)) + '\n');
1223
+ // `filesMatched` counts the files the rule READ: a glob-matched file over
1224
+ // the read cap is in the coverage line below, never in this count.
1225
+ process.stdout.write(kv('files read', String(cov.filesMatched)) + '\n');
743
1226
  process.stdout.write(kv(cov.unitLabel, String(cov.unitsMatched)) + '\n');
1227
+ process.stdout.write(kv('coverage', formatCoverage(cov.coverage)) + '\n');
744
1228
  if (cov.viaExtractor)
745
1229
  process.stdout.write(kv('via extractor', `$use:${cov.viaExtractor}`) + '\n');
746
1230
  // Tuning a selector against a large tree needs the WHOLE candidate set —
@@ -774,25 +1258,25 @@ export const gatesTryCommand = {
774
1258
  process.stdout.write(` … (${result.violations.length - 20} more)\n`);
775
1259
  }
776
1260
  }
1261
+ writeSelfTestBlock(view.id, cov, declared);
1262
+ writeNotes(notes);
777
1263
  if (cov.status === 'empty') {
778
1264
  process.stdout.write('\n This selector matched NOTHING — tighten it before adding it to config.\n');
779
1265
  if (cov.hint)
780
1266
  process.stdout.write(` → ${cov.hint}\n`);
1267
+ process.stdout.write(` Once pasted into config, \`gates coverage\` would exit ` +
1268
+ `${cov.failOnEmpty ? ExitCode.Failure : ExitCode.NotVerified} for it (failOnEmpty=${cov.failOnEmpty}).\n`);
781
1269
  }
782
1270
  process.stdout.write('\n Nothing was written. Paste the rule into sharkcraft.config.ts to keep it.\n');
783
- return cov.status === 'empty' ? ExitCode.NotVerified : ExitCode.VerifiedPass;
1271
+ writeTryVerdict(settled, cov, cov.status === 'empty' ? 'The candidate matched nothing — it proved nothing.' : undefined);
1272
+ return settled.exit;
784
1273
  },
785
1274
  };
786
- /** Which config key each plane's rules live under, for `$use` resolution. */
787
- const PLANE_CONFIG_KEY = {
788
- wiring: 'wiringRules',
789
- policy: 'policyRules',
790
- registry: 'registries',
791
- registration: 'registrationGraph',
792
- baseline: 'baselines',
793
- generated: 'generatedArtifacts',
794
- 'doc-reference': 'docReferences',
795
- };
1275
+ /**
1276
+ * Which config key each plane's rules live under, for `$use` resolution — THE
1277
+ * list beside the config schema (`GATE_PLANE_CONFIG_KEY`), never a CLI copy.
1278
+ */
1279
+ const PLANE_CONFIG_KEY = GATE_PLANE_CONFIG_KEY;
796
1280
  /** The loader's own schema for each plane — one validation, not a second one. */
797
1281
  const PLANE_SCHEMAS = {
798
1282
  wiring: WiringRuleSchema,
@@ -830,8 +1314,23 @@ function inferPlane(raw) {
830
1314
  return 'registry';
831
1315
  return undefined;
832
1316
  }
1317
+ /**
1318
+ * THE unit-state block of the registry / registration explain views (round 13,
1319
+ * K6) — the block the wiring and policy explain views print, through the same
1320
+ * shared helper (`unitStateNotes`, each line `formatUnitLiveness`): a dead glob,
1321
+ * a LOCAL marker that went live, a pack marker (INFO) and every intended-empty
1322
+ * unit. The units are settled from THE per-rule request `gates coverage` builds
1323
+ * (`sourceLivenessRequest` over `gateRuleLabeledSources`), so the explain view
1324
+ * cannot name a unit's state differently from the coverage report.
1325
+ */
1326
+ function writeExplainUnitStates(cwd, view, excludeDirs) {
1327
+ const settled = settleGlobLists(sourceLivenessRequest(cwd, gateRuleLabeledSources(view), excludeDirs, view.id));
1328
+ const unitLiveness = settled.units.filter((u) => u.state !== UnitLivenessState.Live);
1329
+ process.stdout.write(unitStateNotes([{ id: view.id, unitLiveness }], { intendedEmpty: true }).text);
1330
+ }
833
1331
  /** Render a registry inventory as the trust-layer explain view. */
834
- function explainRegistry(cwd, decl, excludeDirs) {
1332
+ function explainRegistry(cwd, view, excludeDirs) {
1333
+ const decl = view.raw;
835
1334
  const inventory = scanRegistry(cwd, decl, { excludeDirs });
836
1335
  const insp = inspectSource(cwd, decl.source, excludeDirs);
837
1336
  if (decl.source.$use)
@@ -846,9 +1345,11 @@ function explainRegistry(cwd, decl, excludeDirs) {
846
1345
  }
847
1346
  for (const d of inventory.diagnostics)
848
1347
  process.stdout.write(` ! ${d}\n`);
1348
+ writeExplainUnitStates(cwd, view, excludeDirs);
849
1349
  }
850
1350
  /** Render the three sides of a registration idiom. */
851
- function explainRegistration(cwd, idiom, excludeDirs) {
1351
+ function explainRegistration(cwd, view, excludeDirs) {
1352
+ const idiom = view.raw;
852
1353
  for (const [label, source] of [
853
1354
  ['declared', idiom.declared],
854
1355
  ['provided', idiom.provided],
@@ -865,6 +1366,7 @@ function explainRegistration(cwd, idiom, excludeDirs) {
865
1366
  if (insp.sites.length > 20)
866
1367
  process.stdout.write(` … (${insp.sites.length - 20} more)\n`);
867
1368
  }
1369
+ writeExplainUnitStates(cwd, view, excludeDirs);
868
1370
  process.stdout.write(`\n Query one token's chain with \`shrk wiring chain <token>\`.\n`);
869
1371
  }
870
1372
  export const gatesExplainCommand = {
@@ -950,10 +1452,10 @@ export const gatesExplainCommand = {
950
1452
  if (view.description)
951
1453
  process.stdout.write(` ${view.description}\n`);
952
1454
  if (view.plane === 'registry') {
953
- explainRegistry(prep.value.cwd, view.raw, prep.value.excludeDirs);
1455
+ explainRegistry(prep.value.cwd, view, prep.value.excludeDirs);
954
1456
  }
955
1457
  else {
956
- explainRegistration(prep.value.cwd, view.raw, prep.value.excludeDirs);
1458
+ explainRegistration(prep.value.cwd, view, prep.value.excludeDirs);
957
1459
  }
958
1460
  return ExitCode.VerifiedPass;
959
1461
  },
@@ -978,23 +1480,147 @@ export async function tryExplainGateRule(args, id) {
978
1480
  const forwarded = { ...args, positional: [id] };
979
1481
  return gatesExplainCommand.run(forwarded);
980
1482
  }
1483
+ export const gatesScaffoldSelfTestCommand = {
1484
+ name: 'scaffold-selftest',
1485
+ description: "Generate a rule's `selfTest` from what it matches TODAY — the fixture the trust layer asks for, turned from a blank page into three lines to review. `--write` inserts it into sharkcraft.config.ts in place.",
1486
+ usage: 'shrk gates scaffold-selftest <ruleId> [--margin N] [--write] [--json]',
1487
+ booleanFlags: new Set(['json', 'write']),
1488
+ async run(args) {
1489
+ const ruleId = args.positional[0];
1490
+ if (!ruleId) {
1491
+ process.stderr.write('Usage: shrk gates scaffold-selftest <ruleId> [--margin N] [--write] [--json]\n');
1492
+ return ExitCode.UsageError;
1493
+ }
1494
+ const prep = await prepare(args);
1495
+ if (!prep.ok)
1496
+ return prep.code;
1497
+ const json = flagBool(args, 'json');
1498
+ const write = flagBool(args, 'write');
1499
+ const marginRaw = flagString(args, 'margin');
1500
+ const margin = marginRaw === undefined ? 20 : Number.parseInt(marginRaw, 10);
1501
+ if (!Number.isFinite(margin) || margin < 0 || margin >= 100) {
1502
+ process.stderr.write(`--margin must be a percentage in [0, 100), got "${marginRaw}".\n`);
1503
+ return ExitCode.UsageError;
1504
+ }
1505
+ const view = prep.value.rules.find((r) => r.id === ruleId);
1506
+ if (!view) {
1507
+ process.stderr.write(`No gate rule with id "${ruleId}". Run \`shrk gates list\` to see every declared rule.\n`);
1508
+ return ExitCode.UsageError;
1509
+ }
1510
+ if (view.selfTest) {
1511
+ process.stderr.write(`Rule "${ruleId}" already declares a selfTest — refusing to overwrite an author's fixture. ` +
1512
+ 'Delete it first if you mean to re-scaffold.\n');
1513
+ return ExitCode.UsageError;
1514
+ }
1515
+ const report = buildGateCoverage(prep.value.cwd, [view], prep.value.excludeDirs, prep.value.extractors, true, await inspectionIfNeeded(prep.value.cwd, [view]));
1516
+ const cov = report.rules[0];
1517
+ // Scaffolding from a rule that matches nothing would produce a selfTest
1518
+ // asserting the broken state — pinning the very bug the fixture exists to
1519
+ // catch. Fix the selector first.
1520
+ if (cov.status === 'error' || cov.unitsMatched === 0) {
1521
+ const why = cov.error ?? 'the rule currently matches nothing';
1522
+ if (json) {
1523
+ process.stdout.write(asJson({ schema: SCHEMA, ruleId, error: why }) + '\n');
1524
+ }
1525
+ else {
1526
+ process.stderr.write(`Cannot scaffold from "${ruleId}": ${why}.\n` +
1527
+ ` A selfTest built on an empty match set would pin the broken state as correct.\n` +
1528
+ ` Fix the selector first — \`shrk gates explain ${ruleId}\` shows what it resolved.\n`);
1529
+ }
1530
+ return ExitCode.NotVerified;
1531
+ }
1532
+ // A count read off an incomplete scan (a file over the read cap, a role
1533
+ // that could not run) is not a measured number: a floor derived from it
1534
+ // pins a set that misses whatever the unexamined part holds. Refuse, and
1535
+ // refuse --write, naming the shortfall: NOT VERIFIED (2).
1536
+ const coverageRecord = { ...cov.coverage, subject: cov.coverage.subject ?? ruleId };
1537
+ if (coverageShortfall(coverageRecord) !== undefined) {
1538
+ const settled = settleVerdict(ExitCode.VerifiedPass, [coverageRecord]);
1539
+ if (json) {
1540
+ process.stdout.write(asJson({
1541
+ schema: SCHEMA,
1542
+ ruleId,
1543
+ error: 'not verified — what the rule matches today was not fully examined',
1544
+ shortfalls: settled.shortfalls,
1545
+ exitCode: settled.exit,
1546
+ }) + '\n');
1547
+ }
1548
+ else {
1549
+ const lead = `Cannot scaffold from "${ruleId}": what it matches today was not fully examined, so ` +
1550
+ `${cov.unitsMatched} ${cov.unitLabel} is not a measured count.${write ? ' --write refused.' : ''}`;
1551
+ process.stdout.write(`${verdictLine(settled, '', lead)}\n`);
1552
+ }
1553
+ return settled.exit;
1554
+ }
1555
+ const scaffold = scaffoldSelfTest(cov, margin);
1556
+ let written;
1557
+ let writeError;
1558
+ if (write) {
1559
+ const configFile = prep.value.configFile;
1560
+ if (!configFile || !existsSync(configFile)) {
1561
+ writeError = 'no local sharkcraft.config.ts to write to';
1562
+ }
1563
+ else {
1564
+ const res = insertSelfTest(readFileSync(configFile, 'utf8'), ruleId, scaffold.snippet);
1565
+ if (!res.ok)
1566
+ writeError = res.error;
1567
+ else {
1568
+ writeFileSync(configFile, res.text, 'utf8');
1569
+ written = configFile;
1570
+ }
1571
+ }
1572
+ }
1573
+ if (json) {
1574
+ process.stdout.write(asJson({
1575
+ schema: SCHEMA,
1576
+ ...scaffold,
1577
+ ...(written ? { written } : {}),
1578
+ ...(writeError ? { writeError } : {}),
1579
+ }) + '\n');
1580
+ return writeError ? ExitCode.Failure : ExitCode.VerifiedPass;
1581
+ }
1582
+ process.stdout.write(header(`Scaffolded selfTest — [${scaffold.plane}] ${ruleId}`));
1583
+ process.stdout.write(kv('matches now', `${scaffold.currentCount} ${cov.unitLabel}`) + '\n');
1584
+ process.stdout.write(kv('floor', `${scaffold.expectMatchesAtLeast} (${scaffold.marginPercent}% margin below the current count)`) + '\n');
1585
+ process.stdout.write('\n');
1586
+ for (const line of scaffold.snippet.split('\n'))
1587
+ process.stdout.write(` ${line}\n`);
1588
+ if (scaffold.note)
1589
+ process.stdout.write(`\n note: ${scaffold.note}\n`);
1590
+ if (written) {
1591
+ process.stdout.write(`\n wrote ${nodePath.relative(prep.value.cwd, written)} — review the diff before committing.\n`);
1592
+ }
1593
+ else if (writeError) {
1594
+ process.stdout.write(`\n ! --write refused: ${writeError}\n`);
1595
+ process.stdout.write(' Paste the block above into the rule by hand.\n');
1596
+ }
1597
+ else {
1598
+ process.stdout.write('\n Paste this into the rule, or re-run with --write to insert it.\n');
1599
+ }
1600
+ return writeError ? ExitCode.Failure : ExitCode.VerifiedPass;
1601
+ },
1602
+ };
981
1603
  export const gatesCommand = {
982
1604
  name: 'gates',
983
- description: 'Rule-authoring trust layer: run every plane\'s violation check in one pass (`check`), list every data-defined rule, show what each one MATCHED (`coverage` — the stale-selector detector), explain any one of them, and dry-run a candidate rule before adding it (`try`). Never writes config. Not `shrk gate`, singular, which runs the quality-gate pipeline.',
984
- usage: 'shrk gates check | coverage | list | explain <id> | try --rule-file <f>',
1605
+ description: 'Rule-authoring trust layer: run every plane\'s violation check in one pass (`check`), list every data-defined rule, show what each one MATCHED (`coverage` — the stale-selector detector), explain any one of them, dry-run a candidate rule before adding it (`try`), and scaffold its selfTest (`scaffold-selftest`). Only `scaffold-selftest --write` ever touches config. Not `shrk gate`, singular, which runs the quality-gate pipeline.',
1606
+ usage: 'shrk gates check | coverage | list | explain <id> | try --rule-file <f> | scaffold-selftest <id>',
1607
+ // Every subverb is a registered trie child; any other bare token is refused
1608
+ // by the dispatcher guard (closest match named) before this body runs — the
1609
+ // one unknown-subcommand authority. Only a bare `shrk gates` lands here.
1610
+ positionals: PositionalMode.None,
985
1611
  booleanFlags: new Set(['json', 'strict', 'changed-only', 'no-spawn']),
986
- async run(args) {
987
- const sub = args.positional[0];
988
- process.stderr.write((sub ? `Unknown subcommand "${sub}". ` : '') +
989
- 'Usage: shrk gates <subcommand>\n' +
1612
+ async run() {
1613
+ process.stderr.write('Usage: shrk gates <subcommand>\n' +
990
1614
  ' check [--plane <p>] [--only <ids>] [--changed-only|--since <ref>] [--no-spawn]\n' +
991
1615
  ' run EVERY plane\'s violation check — one exit code (the CI / pre-commit primitive)\n' +
992
- ' coverage [--plane <p>] [--changed-only|--since <ref>] [--strict]\n' +
993
- ' what each rule MATCHED — the stale-selector detector\n' +
1616
+ ' coverage [--plane <p>] [--changed-only|--since <ref>] [--fail-on-dead-units] [--strict]\n' +
1617
+ ' what each rule MATCHED — the stale-selector detector (+ each selfTest, + dead globs)\n' +
994
1618
  ' list [--plane <p>] every declared rule, with severity + empty-match policy\n' +
995
1619
  ' explain <id> the concrete inputs one rule resolved\n' +
996
- " try --rule-file <f> | --wiring 'declared=<g>:<p> registered=<g>:<p>'\n" +
997
- ' dry-run a candidate rule WITHOUT touching config\n' +
1620
+ " try --rule-file <f> | --wiring 'declared=<g>:<p> registered=<g>:<p>' [--flags <f>]\n" +
1621
+ ' dry-run a candidate rule (and its selfTest) WITHOUT touching config\n' +
1622
+ ' scaffold-selftest <id> [--margin N] [--write]\n' +
1623
+ " generate the rule's selfTest from what it matches today\n" +
998
1624
  '(`shrk gate`, singular, runs the quality-gate pipeline — a different verb.)\n');
999
1625
  return ExitCode.UsageError;
1000
1626
  },