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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (510) hide show
  1. package/dist/asset-preview/apply-asset-preview.js +1 -1
  2. package/dist/authoring/authoring-kit.d.ts +9 -1
  3. package/dist/authoring/authoring-kit.d.ts.map +1 -1
  4. package/dist/authoring/authoring-kit.js +26 -13
  5. package/dist/bootstrap/unlinked-workspace-dependency.d.ts +11 -0
  6. package/dist/bootstrap/unlinked-workspace-dependency.d.ts.map +1 -0
  7. package/dist/bootstrap/unlinked-workspace-dependency.js +99 -0
  8. package/dist/command-registry.d.ts +100 -1
  9. package/dist/command-registry.d.ts.map +1 -1
  10. package/dist/command-registry.js +130 -3
  11. package/dist/commands/api-diff.command.d.ts.map +1 -1
  12. package/dist/commands/api-diff.command.js +12 -0
  13. package/dist/commands/apply.command.d.ts.map +1 -1
  14. package/dist/commands/apply.command.js +3 -0
  15. package/dist/commands/arch.command.d.ts.map +1 -1
  16. package/dist/commands/arch.command.js +26 -7
  17. package/dist/commands/architecture.command.d.ts.map +1 -1
  18. package/dist/commands/architecture.command.js +8 -2
  19. package/dist/commands/baseline.command.d.ts +76 -0
  20. package/dist/commands/baseline.command.d.ts.map +1 -1
  21. package/dist/commands/baseline.command.js +599 -75
  22. package/dist/commands/biome.command.d.ts.map +1 -1
  23. package/dist/commands/biome.command.js +20 -0
  24. package/dist/commands/boundaries.command.d.ts +9 -1
  25. package/dist/commands/boundaries.command.d.ts.map +1 -1
  26. package/dist/commands/boundaries.command.js +88 -20
  27. package/dist/commands/bundle.command.d.ts.map +1 -1
  28. package/dist/commands/bundle.command.js +57 -11
  29. package/dist/commands/cache-align.command.d.ts.map +1 -1
  30. package/dist/commands/cache-align.command.js +3 -1
  31. package/dist/commands/changelog-data.d.ts.map +1 -1
  32. package/dist/commands/changelog-data.js +164 -0
  33. package/dist/commands/changes.command.d.ts.map +1 -1
  34. package/dist/commands/changes.command.js +15 -0
  35. package/dist/commands/check.command.d.ts.map +1 -1
  36. package/dist/commands/check.command.js +1260 -324
  37. package/dist/commands/checks.command.d.ts.map +1 -1
  38. package/dist/commands/checks.command.js +188 -40
  39. package/dist/commands/ci.command.d.ts.map +1 -1
  40. package/dist/commands/ci.command.js +35 -2
  41. package/dist/commands/code-intel.command.d.ts.map +1 -1
  42. package/dist/commands/code-intel.command.js +8 -1
  43. package/dist/commands/command-catalog.d.ts +23 -1
  44. package/dist/commands/command-catalog.d.ts.map +1 -1
  45. package/dist/commands/command-catalog.js +152 -35
  46. package/dist/commands/commands.command.d.ts +21 -3
  47. package/dist/commands/commands.command.d.ts.map +1 -1
  48. package/dist/commands/commands.command.js +172 -27
  49. package/dist/commands/compress.command.d.ts.map +1 -1
  50. package/dist/commands/compress.command.js +4 -0
  51. package/dist/commands/constructs.command.d.ts.map +1 -1
  52. package/dist/commands/constructs.command.js +50 -12
  53. package/dist/commands/context.command.d.ts.map +1 -1
  54. package/dist/commands/context.command.js +40 -5
  55. package/dist/commands/contract-templates.command.d.ts.map +1 -1
  56. package/dist/commands/contract-templates.command.js +9 -1
  57. package/dist/commands/contract.command.d.ts.map +1 -1
  58. package/dist/commands/contract.command.js +35 -0
  59. package/dist/commands/conventions.command.d.ts.map +1 -1
  60. package/dist/commands/conventions.command.js +189 -29
  61. package/dist/commands/coverage.command.d.ts.map +1 -1
  62. package/dist/commands/coverage.command.js +9 -0
  63. package/dist/commands/daily.commands.d.ts +0 -1
  64. package/dist/commands/daily.commands.d.ts.map +1 -1
  65. package/dist/commands/daily.commands.js +12 -76
  66. package/dist/commands/dashboard.command.d.ts.map +1 -1
  67. package/dist/commands/dashboard.command.js +10 -0
  68. package/dist/commands/dev.command.d.ts.map +1 -1
  69. package/dist/commands/dev.command.js +45 -2
  70. package/dist/commands/diagnostics.command.d.ts +0 -2
  71. package/dist/commands/diagnostics.command.d.ts.map +1 -1
  72. package/dist/commands/diagnostics.command.js +5 -78
  73. package/dist/commands/diff-check.command.d.ts +6 -4
  74. package/dist/commands/diff-check.command.d.ts.map +1 -1
  75. package/dist/commands/diff-check.command.js +190 -103
  76. package/dist/commands/docs-references.command.d.ts +20 -0
  77. package/dist/commands/docs-references.command.d.ts.map +1 -0
  78. package/dist/commands/docs-references.command.js +520 -0
  79. package/dist/commands/doctor.command.d.ts.map +1 -1
  80. package/dist/commands/doctor.command.js +60 -5
  81. package/dist/commands/drift.command.d.ts.map +1 -1
  82. package/dist/commands/drift.command.js +17 -4
  83. package/dist/commands/eslint.command.d.ts.map +1 -1
  84. package/dist/commands/eslint.command.js +20 -0
  85. package/dist/commands/export.command.d.ts.map +1 -1
  86. package/dist/commands/export.command.js +23 -1
  87. package/dist/commands/feedback-dispatch.command.d.ts.map +1 -1
  88. package/dist/commands/feedback-dispatch.command.js +29 -0
  89. package/dist/commands/feedback.command.d.ts.map +1 -1
  90. package/dist/commands/feedback.command.js +6 -1
  91. package/dist/commands/finish.command.d.ts.map +1 -1
  92. package/dist/commands/finish.command.js +32 -8
  93. package/dist/commands/fix.command.d.ts.map +1 -1
  94. package/dist/commands/fix.command.js +19 -0
  95. package/dist/commands/gate.command.d.ts +6 -1
  96. package/dist/commands/gate.command.d.ts.map +1 -1
  97. package/dist/commands/gate.command.js +50 -124
  98. package/dist/commands/gates.command.d.ts +54 -0
  99. package/dist/commands/gates.command.d.ts.map +1 -1
  100. package/dist/commands/gates.command.js +1314 -64
  101. package/dist/commands/gen.command.d.ts.map +1 -1
  102. package/dist/commands/gen.command.js +4 -0
  103. package/dist/commands/generated.command.d.ts +62 -0
  104. package/dist/commands/generated.command.d.ts.map +1 -1
  105. package/dist/commands/generated.command.js +639 -156
  106. package/dist/commands/graph-code-subverbs.d.ts +15 -0
  107. package/dist/commands/graph-code-subverbs.d.ts.map +1 -1
  108. package/dist/commands/graph-code-subverbs.js +203 -25
  109. package/dist/commands/graph.command.d.ts.map +1 -1
  110. package/dist/commands/graph.command.js +79 -37
  111. package/dist/commands/help.command.d.ts +30 -6
  112. package/dist/commands/help.command.d.ts.map +1 -1
  113. package/dist/commands/help.command.js +323 -199
  114. package/dist/commands/helper.command.d.ts +1 -0
  115. package/dist/commands/helper.command.d.ts.map +1 -1
  116. package/dist/commands/helper.command.js +195 -56
  117. package/dist/commands/impact.command.d.ts.map +1 -1
  118. package/dist/commands/impact.command.js +115 -12
  119. package/dist/commands/infer.command.d.ts.map +1 -1
  120. package/dist/commands/infer.command.js +20 -45
  121. package/dist/commands/ingest.command.d.ts +0 -1
  122. package/dist/commands/ingest.command.d.ts.map +1 -1
  123. package/dist/commands/ingest.command.js +25 -41
  124. package/dist/commands/knowledge-author.command.d.ts +0 -3
  125. package/dist/commands/knowledge-author.command.d.ts.map +1 -1
  126. package/dist/commands/knowledge-author.command.js +10 -23
  127. package/dist/commands/knowledge.command.d.ts.map +1 -1
  128. package/dist/commands/knowledge.command.js +469 -129
  129. package/dist/commands/languages.command.d.ts.map +1 -1
  130. package/dist/commands/languages.command.js +27 -0
  131. package/dist/commands/lint.command.d.ts.map +1 -1
  132. package/dist/commands/lint.command.js +3 -0
  133. package/dist/commands/mcp.command.d.ts +18 -0
  134. package/dist/commands/mcp.command.d.ts.map +1 -1
  135. package/dist/commands/mcp.command.js +33 -8
  136. package/dist/commands/onboard.command.d.ts.map +1 -1
  137. package/dist/commands/onboard.command.js +20 -0
  138. package/dist/commands/owners.command.d.ts.map +1 -1
  139. package/dist/commands/owners.command.js +14 -2
  140. package/dist/commands/packs-new.d.ts +13 -1
  141. package/dist/commands/packs-new.d.ts.map +1 -1
  142. package/dist/commands/packs-new.js +500 -335
  143. package/dist/commands/packs.command.d.ts.map +1 -1
  144. package/dist/commands/packs.command.js +378 -36
  145. package/dist/commands/paths.command.d.ts.map +1 -1
  146. package/dist/commands/paths.command.js +9 -1
  147. package/dist/commands/pipelines.command.d.ts.map +1 -1
  148. package/dist/commands/pipelines.command.js +7 -1
  149. package/dist/commands/plan-check.command.d.ts.map +1 -1
  150. package/dist/commands/plan-check.command.js +3 -0
  151. package/dist/commands/plan.command.d.ts.map +1 -1
  152. package/dist/commands/plan.command.js +3 -0
  153. package/dist/commands/playbooks.command.d.ts.map +1 -1
  154. package/dist/commands/playbooks.command.js +7 -2
  155. package/dist/commands/policy-lint.command.d.ts +19 -2
  156. package/dist/commands/policy-lint.command.d.ts.map +1 -1
  157. package/dist/commands/policy-lint.command.js +224 -65
  158. package/dist/commands/policy.command.d.ts.map +1 -1
  159. package/dist/commands/policy.command.js +6 -1
  160. package/dist/commands/pr.command.d.ts.map +1 -1
  161. package/dist/commands/pr.command.js +14 -0
  162. package/dist/commands/presets.command.d.ts.map +1 -1
  163. package/dist/commands/presets.command.js +12 -27
  164. package/dist/commands/profiles.command.d.ts.map +1 -1
  165. package/dist/commands/profiles.command.js +94 -26
  166. package/dist/commands/provenance.command.js +1 -1
  167. package/dist/commands/quality.command.d.ts.map +1 -1
  168. package/dist/commands/quality.command.js +129 -33
  169. package/dist/commands/recommend.command.d.ts +20 -4
  170. package/dist/commands/recommend.command.d.ts.map +1 -1
  171. package/dist/commands/recommend.command.js +172 -278
  172. package/dist/commands/registrations.command.d.ts.map +1 -1
  173. package/dist/commands/registrations.command.js +138 -13
  174. package/dist/commands/registry-lifecycle-run.d.ts +14 -0
  175. package/dist/commands/registry-lifecycle-run.d.ts.map +1 -0
  176. package/dist/commands/registry-lifecycle-run.js +336 -0
  177. package/dist/commands/registry.command.d.ts.map +1 -1
  178. package/dist/commands/registry.command.js +235 -62
  179. package/dist/commands/release.command.d.ts.map +1 -1
  180. package/dist/commands/release.command.js +99 -20
  181. package/dist/commands/report.command.d.ts.map +1 -1
  182. package/dist/commands/report.command.js +38 -2
  183. package/dist/commands/reuse-coverage.command.d.ts +23 -0
  184. package/dist/commands/reuse-coverage.command.d.ts.map +1 -0
  185. package/dist/commands/reuse-coverage.command.js +536 -0
  186. package/dist/commands/reuse.command.d.ts +3 -18
  187. package/dist/commands/reuse.command.d.ts.map +1 -1
  188. package/dist/commands/reuse.command.js +395 -244
  189. package/dist/commands/review.command.d.ts.map +1 -1
  190. package/dist/commands/review.command.js +33 -1
  191. package/dist/commands/rounds.command.d.ts.map +1 -1
  192. package/dist/commands/rounds.command.js +5 -0
  193. package/dist/commands/safety.command.d.ts.map +1 -1
  194. package/dist/commands/safety.command.js +9 -0
  195. package/dist/commands/scaffolds.command.d.ts.map +1 -1
  196. package/dist/commands/scaffolds.command.js +97 -25
  197. package/dist/commands/search.command.d.ts +0 -8
  198. package/dist/commands/search.command.d.ts.map +1 -1
  199. package/dist/commands/search.command.js +136 -28
  200. package/dist/commands/self-config-xrefs.command.d.ts +24 -0
  201. package/dist/commands/self-config-xrefs.command.d.ts.map +1 -0
  202. package/dist/commands/self-config-xrefs.command.js +170 -0
  203. package/dist/commands/self-config.command.d.ts.map +1 -1
  204. package/dist/commands/self-config.command.js +261 -65
  205. package/dist/commands/smart-context.command.d.ts.map +1 -1
  206. package/dist/commands/smart-context.command.js +39 -3
  207. package/dist/commands/spec.command.d.ts.map +1 -1
  208. package/dist/commands/spec.command.js +3 -0
  209. package/dist/commands/stats.command.d.ts.map +1 -1
  210. package/dist/commands/stats.command.js +3 -0
  211. package/dist/commands/surface.command.d.ts +6 -1
  212. package/dist/commands/surface.command.d.ts.map +1 -1
  213. package/dist/commands/surface.command.js +256 -54
  214. package/dist/commands/task-context.command.js +1 -1
  215. package/dist/commands/task.command.d.ts.map +1 -1
  216. package/dist/commands/task.command.js +22 -1
  217. package/dist/commands/templates.command.d.ts +4 -0
  218. package/dist/commands/templates.command.d.ts.map +1 -1
  219. package/dist/commands/templates.command.js +109 -22
  220. package/dist/commands/test.command.d.ts.map +1 -1
  221. package/dist/commands/test.command.js +161 -33
  222. package/dist/commands/tests.command.d.ts.map +1 -1
  223. package/dist/commands/tests.command.js +29 -3
  224. package/dist/commands/trace.command.d.ts.map +1 -1
  225. package/dist/commands/trace.command.js +29 -4
  226. package/dist/commands/watch.command.d.ts.map +1 -1
  227. package/dist/commands/watch.command.js +3 -0
  228. package/dist/commands/why.command.d.ts.map +1 -1
  229. package/dist/commands/why.command.js +3 -0
  230. package/dist/commands/wiring.command.d.ts +18 -4
  231. package/dist/commands/wiring.command.d.ts.map +1 -1
  232. package/dist/commands/wiring.command.js +362 -74
  233. package/dist/dashboard/code-intelligence-data.js +4 -4
  234. package/dist/diff/deleted-orphan-coverage.d.ts +32 -0
  235. package/dist/diff/deleted-orphan-coverage.d.ts.map +1 -0
  236. package/dist/diff/deleted-orphan-coverage.js +107 -0
  237. package/dist/diff/deleted-orphan-scope-notes.d.ts +23 -0
  238. package/dist/diff/deleted-orphan-scope-notes.d.ts.map +1 -0
  239. package/dist/diff/deleted-orphan-scope-notes.js +49 -0
  240. package/dist/diff/deleted-orphans.d.ts +32 -0
  241. package/dist/diff/deleted-orphans.d.ts.map +1 -1
  242. package/dist/diff/deleted-orphans.js +42 -2
  243. package/dist/dispatch/closest-match.d.ts +15 -0
  244. package/dist/dispatch/closest-match.d.ts.map +1 -0
  245. package/dist/dispatch/closest-match.js +47 -0
  246. package/dist/dispatch/global-flags.d.ts +94 -0
  247. package/dist/dispatch/global-flags.d.ts.map +1 -0
  248. package/dist/dispatch/global-flags.js +123 -0
  249. package/dist/dispatch/guard-invocation-input.d.ts +24 -0
  250. package/dist/dispatch/guard-invocation-input.d.ts.map +1 -0
  251. package/dist/dispatch/guard-invocation-input.js +1 -0
  252. package/dist/dispatch/guard-invocation.d.ts +33 -0
  253. package/dist/dispatch/guard-invocation.d.ts.map +1 -0
  254. package/dist/dispatch/guard-invocation.js +183 -0
  255. package/dist/dispatch/help-intercept.d.ts +22 -0
  256. package/dist/dispatch/help-intercept.d.ts.map +1 -0
  257. package/dist/dispatch/help-intercept.js +42 -0
  258. package/dist/dispatch/i-declared-invocation.d.ts +17 -0
  259. package/dist/dispatch/i-declared-invocation.d.ts.map +1 -0
  260. package/dist/dispatch/i-declared-invocation.js +1 -0
  261. package/dist/dispatch/i-unknown-flag-refusal-input.d.ts +28 -0
  262. package/dist/dispatch/i-unknown-flag-refusal-input.d.ts.map +1 -0
  263. package/dist/dispatch/i-unknown-flag-refusal-input.js +1 -0
  264. package/dist/dispatch/invocation-rejection-kind.d.ts +15 -0
  265. package/dist/dispatch/invocation-rejection-kind.d.ts.map +1 -0
  266. package/dist/dispatch/invocation-rejection-kind.js +15 -0
  267. package/dist/dispatch/invocation-rejection.d.ts +18 -0
  268. package/dist/dispatch/invocation-rejection.d.ts.map +1 -0
  269. package/dist/dispatch/invocation-rejection.js +1 -0
  270. package/dist/dispatch/judge-invocation.d.ts +24 -0
  271. package/dist/dispatch/judge-invocation.d.ts.map +1 -0
  272. package/dist/dispatch/judge-invocation.js +50 -0
  273. package/dist/dispatch/positional-mode.d.ts +18 -0
  274. package/dist/dispatch/positional-mode.d.ts.map +1 -0
  275. package/dist/dispatch/positional-mode.js +18 -0
  276. package/dist/dispatch/read-tracking-map.d.ts +28 -0
  277. package/dist/dispatch/read-tracking-map.d.ts.map +1 -0
  278. package/dist/dispatch/read-tracking-map.js +58 -0
  279. package/dist/dispatch/refuse-sibling-valve-flags.d.ts +30 -0
  280. package/dist/dispatch/refuse-sibling-valve-flags.d.ts.map +1 -0
  281. package/dist/dispatch/refuse-sibling-valve-flags.js +65 -0
  282. package/dist/dispatch/subverb-spec.d.ts +26 -0
  283. package/dist/dispatch/subverb-spec.d.ts.map +1 -0
  284. package/dist/dispatch/subverb-spec.js +1 -0
  285. package/dist/dispatch/undocumented-flag-reads.d.ts +22 -0
  286. package/dist/dispatch/undocumented-flag-reads.d.ts.map +1 -0
  287. package/dist/dispatch/undocumented-flag-reads.js +283 -0
  288. package/dist/dispatch/unknown-flag-refusal-mode.d.ts +13 -0
  289. package/dist/dispatch/unknown-flag-refusal-mode.d.ts.map +1 -0
  290. package/dist/dispatch/unknown-flag-refusal-mode.js +13 -0
  291. package/dist/dispatch/unknown-flag-refusal.d.ts +22 -0
  292. package/dist/dispatch/unknown-flag-refusal.d.ts.map +1 -0
  293. package/dist/dispatch/unknown-flag-refusal.js +57 -0
  294. package/dist/dispatch/unread-flags.d.ts +152 -0
  295. package/dist/dispatch/unread-flags.d.ts.map +1 -0
  296. package/dist/dispatch/unread-flags.js +295 -0
  297. package/dist/dispatch/verdict-valve-flag.d.ts +21 -0
  298. package/dist/dispatch/verdict-valve-flag.d.ts.map +1 -0
  299. package/dist/dispatch/verdict-valve-flag.js +21 -0
  300. package/dist/dispatch/walk-declared-subverbs.d.ts +14 -0
  301. package/dist/dispatch/walk-declared-subverbs.d.ts.map +1 -0
  302. package/dist/dispatch/walk-declared-subverbs.js +28 -0
  303. package/dist/exit-codes.d.ts +41 -3
  304. package/dist/exit-codes.d.ts.map +1 -1
  305. package/dist/exit-codes.js +153 -14
  306. package/dist/finish/run-finish.d.ts +72 -5
  307. package/dist/finish/run-finish.d.ts.map +1 -1
  308. package/dist/finish/run-finish.js +453 -104
  309. package/dist/gates/accepted-empty-note.d.ts +10 -0
  310. package/dist/gates/accepted-empty-note.d.ts.map +1 -0
  311. package/dist/gates/accepted-empty-note.js +11 -0
  312. package/dist/gates/allow-empty.d.ts +25 -0
  313. package/dist/gates/allow-empty.d.ts.map +1 -0
  314. package/dist/gates/allow-empty.js +27 -0
  315. package/dist/gates/asset-doctor-failing-units.d.ts +16 -0
  316. package/dist/gates/asset-doctor-failing-units.d.ts.map +1 -0
  317. package/dist/gates/asset-doctor-failing-units.js +14 -0
  318. package/dist/gates/asset-doctor-failure-line.d.ts +16 -0
  319. package/dist/gates/asset-doctor-failure-line.d.ts.map +1 -0
  320. package/dist/gates/asset-doctor-failure-line.js +28 -0
  321. package/dist/gates/empty-rule-advice-lines.d.ts +15 -0
  322. package/dist/gates/empty-rule-advice-lines.d.ts.map +1 -0
  323. package/dist/gates/empty-rule-advice-lines.js +20 -0
  324. package/dist/gates/gate-envelope.d.ts +85 -8
  325. package/dist/gates/gate-envelope.d.ts.map +1 -1
  326. package/dist/gates/gate-envelope.js +56 -7
  327. package/dist/gates/gate-rule-globs.d.ts +67 -0
  328. package/dist/gates/gate-rule-globs.d.ts.map +1 -0
  329. package/dist/gates/gate-rule-globs.js +139 -0
  330. package/dist/gates/gate-rule-view.d.ts +25 -6
  331. package/dist/gates/gate-rule-view.d.ts.map +1 -1
  332. package/dist/gates/gate-rule-view.js +105 -77
  333. package/dist/gates/i-coverage-dead-glob.d.ts +19 -0
  334. package/dist/gates/i-coverage-dead-glob.d.ts.map +1 -0
  335. package/dist/gates/i-coverage-dead-glob.js +1 -0
  336. package/dist/gates/i-coverage-negation.d.ts +13 -0
  337. package/dist/gates/i-coverage-negation.d.ts.map +1 -0
  338. package/dist/gates/i-coverage-negation.js +1 -0
  339. package/dist/gates/i-registration-query-settle-input.d.ts +18 -0
  340. package/dist/gates/i-registration-query-settle-input.d.ts.map +1 -0
  341. package/dist/gates/i-registration-query-settle-input.js +1 -0
  342. package/dist/gates/i-unit-state-note-row.d.ts +16 -0
  343. package/dist/gates/i-unit-state-note-row.d.ts.map +1 -0
  344. package/dist/gates/i-unit-state-note-row.js +1 -0
  345. package/dist/gates/i-unit-state-notes.d.ts +32 -0
  346. package/dist/gates/i-unit-state-notes.d.ts.map +1 -0
  347. package/dist/gates/i-unit-state-notes.js +1 -0
  348. package/dist/gates/incomplete-registry-inventory.d.ts +19 -0
  349. package/dist/gates/incomplete-registry-inventory.d.ts.map +1 -0
  350. package/dist/gates/incomplete-registry-inventory.js +63 -0
  351. package/dist/gates/measure-registration-roles.d.ts +10 -0
  352. package/dist/gates/measure-registration-roles.d.ts.map +1 -0
  353. package/dist/gates/measure-registration-roles.js +9 -0
  354. package/dist/gates/plane-verdict.d.ts +14 -0
  355. package/dist/gates/plane-verdict.d.ts.map +1 -0
  356. package/dist/gates/plane-verdict.js +22 -0
  357. package/dist/gates/qualify-clean-for-units.d.ts +12 -0
  358. package/dist/gates/qualify-clean-for-units.d.ts.map +1 -0
  359. package/dist/gates/qualify-clean-for-units.js +18 -0
  360. package/dist/gates/registration-graph-verdict.d.ts +34 -0
  361. package/dist/gates/registration-graph-verdict.d.ts.map +1 -0
  362. package/dist/gates/registration-graph-verdict.js +70 -0
  363. package/dist/gates/registration-roles.d.ts +9 -0
  364. package/dist/gates/registration-roles.d.ts.map +1 -0
  365. package/dist/gates/registration-roles.js +1 -0
  366. package/dist/gates/registry-liveness.d.ts +11 -0
  367. package/dist/gates/registry-liveness.d.ts.map +1 -0
  368. package/dist/gates/registry-liveness.js +13 -0
  369. package/dist/gates/rule-coverage.d.ts +225 -4
  370. package/dist/gates/rule-coverage.d.ts.map +1 -1
  371. package/dist/gates/rule-coverage.js +610 -43
  372. package/dist/gates/run-gate-planes.d.ts +58 -0
  373. package/dist/gates/run-gate-planes.d.ts.map +1 -0
  374. package/dist/gates/run-gate-planes.js +403 -0
  375. package/dist/gates/scaffold-selftest.d.ts +56 -0
  376. package/dist/gates/scaffold-selftest.d.ts.map +1 -0
  377. package/dist/gates/scaffold-selftest.js +119 -0
  378. package/dist/gates/seam-rejected-rules.d.ts +28 -0
  379. package/dist/gates/seam-rejected-rules.d.ts.map +1 -0
  380. package/dist/gates/seam-rejected-rules.js +95 -0
  381. package/dist/gates/self-test-check.d.ts +29 -0
  382. package/dist/gates/self-test-check.d.ts.map +1 -0
  383. package/dist/gates/self-test-check.js +1 -0
  384. package/dist/gates/self-test-subject.d.ts +28 -0
  385. package/dist/gates/self-test-subject.d.ts.map +1 -0
  386. package/dist/gates/self-test-subject.js +1 -0
  387. package/dist/gates/self-test.d.ts +20 -0
  388. package/dist/gates/self-test.d.ts.map +1 -0
  389. package/dist/gates/self-test.js +82 -0
  390. package/dist/gates/settle-verdict.d.ts +14 -0
  391. package/dist/gates/settle-verdict.d.ts.map +1 -0
  392. package/dist/gates/settle-verdict.js +13 -0
  393. package/dist/gates/settled-verdict.d.ts +8 -0
  394. package/dist/gates/settled-verdict.d.ts.map +1 -0
  395. package/dist/gates/settled-verdict.js +1 -0
  396. package/dist/gates/template-registry-coverage.d.ts +18 -0
  397. package/dist/gates/template-registry-coverage.d.ts.map +1 -0
  398. package/dist/gates/template-registry-coverage.js +24 -0
  399. package/dist/gates/unit-state-notes.d.ts +40 -0
  400. package/dist/gates/unit-state-notes.d.ts.map +1 -0
  401. package/dist/gates/unit-state-notes.js +71 -0
  402. package/dist/gates/verdict-line.d.ts +18 -0
  403. package/dist/gates/verdict-line.d.ts.map +1 -0
  404. package/dist/gates/verdict-line.js +40 -0
  405. package/dist/graph/graph-reuse-lookup.d.ts +10 -0
  406. package/dist/graph/graph-reuse-lookup.d.ts.map +1 -0
  407. package/dist/graph/graph-reuse-lookup.js +41 -0
  408. package/dist/graph/index-behind-hint.d.ts +11 -0
  409. package/dist/graph/index-behind-hint.d.ts.map +1 -0
  410. package/dist/graph/index-behind-hint.js +27 -0
  411. package/dist/knowledge/knowledge-stale-gate-flags.d.ts +3 -0
  412. package/dist/knowledge/knowledge-stale-gate-flags.d.ts.map +1 -0
  413. package/dist/knowledge/knowledge-stale-gate-flags.js +1 -0
  414. package/dist/knowledge/knowledge-stale-gate-input.d.ts +3 -0
  415. package/dist/knowledge/knowledge-stale-gate-input.d.ts.map +1 -0
  416. package/dist/knowledge/knowledge-stale-gate-input.js +1 -0
  417. package/dist/knowledge/knowledge-stale-gate-result.d.ts +3 -0
  418. package/dist/knowledge/knowledge-stale-gate-result.d.ts.map +1 -0
  419. package/dist/knowledge/knowledge-stale-gate-result.js +1 -0
  420. package/dist/knowledge/knowledge-stale-gate.d.ts +10 -0
  421. package/dist/knowledge/knowledge-stale-gate.d.ts.map +1 -0
  422. package/dist/knowledge/knowledge-stale-gate.js +9 -0
  423. package/dist/main.d.ts.map +1 -1
  424. package/dist/main.js +204 -83
  425. package/dist/output/failure-hints.d.ts.map +1 -1
  426. package/dist/output/failure-hints.js +11 -9
  427. package/dist/output/rejected-entries-note.d.ts +57 -0
  428. package/dist/output/rejected-entries-note.d.ts.map +1 -0
  429. package/dist/output/rejected-entries-note.js +103 -0
  430. package/dist/output/stdout-is-pipe.d.ts +13 -0
  431. package/dist/output/stdout-is-pipe.d.ts.map +1 -0
  432. package/dist/output/stdout-is-pipe.js +21 -0
  433. package/dist/packs/stale-build-warning.d.ts +11 -0
  434. package/dist/packs/stale-build-warning.d.ts.map +1 -0
  435. package/dist/packs/stale-build-warning.js +23 -0
  436. package/dist/quality/run-quality.d.ts +123 -0
  437. package/dist/quality/run-quality.d.ts.map +1 -0
  438. package/dist/quality/run-quality.js +442 -0
  439. package/dist/schemas/json-schemas.d.ts +184 -3
  440. package/dist/schemas/json-schemas.d.ts.map +1 -1
  441. package/dist/schemas/json-schemas.js +113 -2
  442. package/dist/shrk.d.ts +3 -0
  443. package/dist/shrk.d.ts.map +1 -0
  444. package/dist/shrk.js +30 -0
  445. package/dist/surface/audience-applicability.d.ts +26 -0
  446. package/dist/surface/audience-applicability.d.ts.map +1 -0
  447. package/dist/surface/audience-applicability.js +60 -0
  448. package/dist/surface/catalog-command-safety.d.ts +18 -0
  449. package/dist/surface/catalog-command-safety.d.ts.map +1 -0
  450. package/dist/surface/catalog-command-safety.js +62 -0
  451. package/dist/surface/cli-command-resolver.d.ts +21 -0
  452. package/dist/surface/cli-command-resolver.d.ts.map +1 -0
  453. package/dist/surface/cli-command-resolver.js +87 -0
  454. package/dist/surface/command-dispatch-kind.d.ts +20 -0
  455. package/dist/surface/command-dispatch-kind.d.ts.map +1 -0
  456. package/dist/surface/command-dispatch-kind.js +20 -0
  457. package/dist/surface/command-index-entry.d.ts +40 -0
  458. package/dist/surface/command-index-entry.d.ts.map +1 -0
  459. package/dist/surface/command-index-entry.js +1 -0
  460. package/dist/surface/command-index.d.ts +77 -0
  461. package/dist/surface/command-index.d.ts.map +1 -0
  462. package/dist/surface/command-index.js +323 -0
  463. package/dist/surface/i-command-index.d.ts +22 -0
  464. package/dist/surface/i-command-index.d.ts.map +1 -0
  465. package/dist/surface/i-command-index.js +1 -0
  466. package/dist/surface/i-command-string-context.d.ts +20 -0
  467. package/dist/surface/i-command-string-context.d.ts.map +1 -0
  468. package/dist/surface/i-command-string-context.js +1 -0
  469. package/dist/surface/i-surface-denial.d.ts +13 -0
  470. package/dist/surface/i-surface-denial.d.ts.map +1 -0
  471. package/dist/surface/i-surface-denial.js +1 -0
  472. package/dist/surface/load-surface-context.d.ts +17 -1
  473. package/dist/surface/load-surface-context.d.ts.map +1 -1
  474. package/dist/surface/load-surface-context.js +48 -10
  475. package/dist/surface/not-enabled-error.d.ts +38 -4
  476. package/dist/surface/not-enabled-error.d.ts.map +1 -1
  477. package/dist/surface/not-enabled-error.js +96 -6
  478. package/dist/surface/profiles.d.ts +7 -0
  479. package/dist/surface/profiles.d.ts.map +1 -1
  480. package/dist/surface/resolve-command-string.d.ts +10 -0
  481. package/dist/surface/resolve-command-string.d.ts.map +1 -0
  482. package/dist/surface/resolve-command-string.js +572 -0
  483. package/dist/surface/surface-config-writer.d.ts +7 -2
  484. package/dist/surface/surface-config-writer.d.ts.map +1 -1
  485. package/dist/surface/surface-config-writer.js +25 -6
  486. package/dist/surface/surface-layer.d.ts +11 -0
  487. package/dist/surface/surface-layer.d.ts.map +1 -0
  488. package/dist/surface/surface-layer.js +11 -0
  489. package/dist/surface/surface-refusal-reason.d.ts +16 -0
  490. package/dist/surface/surface-refusal-reason.d.ts.map +1 -0
  491. package/dist/surface/surface-refusal-reason.js +16 -0
  492. package/dist/surface/surface-selector.d.ts +26 -0
  493. package/dist/surface/surface-selector.d.ts.map +1 -0
  494. package/dist/surface/surface-selector.js +42 -0
  495. package/dist/surface/surface-summary.d.ts +61 -18
  496. package/dist/surface/surface-summary.d.ts.map +1 -1
  497. package/dist/surface/surface-summary.js +147 -60
  498. package/dist/surface/tier.d.ts +85 -22
  499. package/dist/surface/tier.d.ts.map +1 -1
  500. package/dist/surface/tier.js +132 -30
  501. package/dist/usage/usage-log.d.ts +6 -1
  502. package/dist/usage/usage-log.d.ts.map +1 -1
  503. package/dist/usage/usage-log.js +7 -2
  504. package/dist/validation/run-validation-loop.d.ts +9 -0
  505. package/dist/validation/run-validation-loop.d.ts.map +1 -1
  506. package/dist/validation/run-validation-loop.js +14 -11
  507. package/dist/validation/typecheck-emitted.d.ts +8 -9
  508. package/dist/validation/typecheck-emitted.d.ts.map +1 -1
  509. package/dist/validation/typecheck-emitted.js +17 -97
  510. package/package.json +34 -34
@@ -14,22 +14,46 @@
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, writeFileSync } from 'node:fs';
17
18
  import * as nodePath from 'node:path';
18
- import { explainWiring, inspectSource, scanRegistry } from '@shrkcrft/boundaries';
19
- import { resolveProjectConfig } from '@shrkcrft/inspector';
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';
20
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";
21
28
  import { ExitCode } from "../exit-codes.js";
22
29
  import { asJson, header, kv } from "../output/format-output.js";
23
30
  import { collectGateRules, GATE_PLANES, } from "../gates/gate-rule-view.js";
24
- 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";
25
35
  import { buildGateEnvelope } from "../gates/gate-envelope.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";
43
+ import { runGatePlanes } from "../gates/run-gate-planes.js";
26
44
  import { baselineExplainCommand } from "./baseline.command.js";
27
45
  import { generatedExplainCommand } from "./generated.command.js";
46
+ import { docsReferencesExplainCommand } from "./docs-references.command.js";
28
47
  import { renderPolicyExplain, runPolicyExplain } from "./policy-lint.command.js";
29
48
  import { renderWiringExplain } from "./wiring.command.js";
30
49
  const SCHEMA = 'sharkcraft.gates/v1';
31
- async function prepare(args) {
50
+ export async function prepare(args) {
32
51
  const cwd = resolveCwd(args);
52
+ // Coverage memoizes its tree reads inside one call (see `withFileReadCache`).
53
+ // `--no-cache` drops anything already memoized so a suspected caching bug can
54
+ // be ruled out without a code change.
55
+ if (flagBool(args, 'no-cache'))
56
+ clearFileReadCache();
33
57
  const json = flagBool(args, 'json');
34
58
  const loaded = await resolveProjectConfig(cwd);
35
59
  if (!loaded.ok) {
@@ -40,14 +64,17 @@ async function prepare(args) {
40
64
  process.stderr.write(`Could not load config: ${msg}\n Run \`shrk doctor\` for details.\n`);
41
65
  return { ok: false, code: ExitCode.UsageError };
42
66
  }
43
- const rel = nodePath.relative(cwd, loaded.value.sharkcraftDir).split(nodePath.sep).join('/');
44
67
  return {
45
68
  ok: true,
46
69
  value: {
47
70
  cwd,
48
71
  rules: collectGateRules(loaded.value.config),
49
- 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),
50
74
  planeDiagnostics: loaded.value.planeDiagnostics,
75
+ rejectedRules: seamRejectedRules(loaded.value),
76
+ extractors: loaded.value.config.extractors ?? {},
77
+ configFile: loaded.value.configFile,
51
78
  },
52
79
  };
53
80
  }
@@ -79,6 +106,136 @@ function writeNoRules(json) {
79
106
  ' generatedArtifacts[] generated files that must not be hand-edited\n');
80
107
  return ExitCode.NotVerified;
81
108
  }
109
+ /**
110
+ * Resolve `--changed-only` / `--since <ref>` (`--base` is a synonym) into the
111
+ * changed-file set, or `undefined` for a whole-tree run.
112
+ *
113
+ * `error` is distinct from "no files changed": an unresolvable ref must not
114
+ * silently degrade to an empty diff, which would narrow every rule out of
115
+ * scope and produce a green run that checked nothing.
116
+ */
117
+ export function resolveScope(args, cwd) {
118
+ const changedOnly = flagBool(args, 'changed-only');
119
+ const since = flagString(args, 'since') ?? flagString(args, 'base');
120
+ if (!changedOnly && !since)
121
+ return {};
122
+ // The SAME helper `check wiring --changed-only` uses, so bare
123
+ // `--changed-only` means the working tree here too. Two surfaces disagreeing
124
+ // about what "changed" means is how a pre-commit hook silently checks a
125
+ // different set than the CI step it is supposed to mirror.
126
+ if (since !== undefined && !refExists(cwd, since)) {
127
+ return { error: `cannot resolve ref '${since}' — not a valid commit/branch` };
128
+ }
129
+ const changed = resolveChangedFiles({
130
+ projectRoot: cwd,
131
+ ...(since ? { since } : { includeWorktree: true }),
132
+ });
133
+ return { files: changed.files };
134
+ }
135
+ /**
136
+ * Narrow rules to those whose FOOTPRINT intersects the change.
137
+ *
138
+ * Returns the surviving rules plus how many were dropped, because a scoped run
139
+ * must never print the same headline as a full one — "0 violations across 2 of
140
+ * 9 rules" and "0 violations across 9 of 9" are different facts.
141
+ */
142
+ export function narrowToScope(rules, files) {
143
+ if (files === undefined)
144
+ return { selected: rules, skippedByScope: 0 };
145
+ const selected = rules.filter((r) => ruleTouchedBy(r, files));
146
+ return { selected, skippedByScope: rules.length - selected.length };
147
+ }
148
+ /** Flags every `gates` verb accepts; anything else is a typo, not an opt-in. */
149
+ const GATES_FLAGS = new Set([
150
+ 'json',
151
+ 'margin',
152
+ 'write',
153
+ 'strict',
154
+ 'plane',
155
+ 'only',
156
+ 'changed-only',
157
+ 'since',
158
+ 'base',
159
+ 'no-spawn',
160
+ 'no-cache',
161
+ 'full',
162
+ 'limit',
163
+ 'id',
164
+ 'rule-file',
165
+ 'wiring',
166
+ // Every dispatcher global (THE list) — a direct handler call may carry them.
167
+ ...GLOBAL_FLAGS,
168
+ ]);
169
+ /**
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.
175
+ */
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).
189
+ /**
190
+ * Apply `--plane` and `--only` narrowing.
191
+ *
192
+ * An unknown `--only` id is REFUSED rather than silently matching nothing: a
193
+ * typo'd rule id would otherwise narrow the run to zero rules and report a
194
+ * confident pass over an empty set.
195
+ */
196
+ function filterRules(all, planes, only, rejectedAll = []) {
197
+ let rules = planes ? all.filter((r) => planes.has(r.plane)) : all;
198
+ let rejected = planes ? rejectedAll.filter((r) => planes.has(r.type)) : rejectedAll;
199
+ if (!only)
200
+ return { ok: true, rules, rejected };
201
+ const wanted = only.split(',').map((x) => x.trim()).filter(Boolean);
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)]);
205
+ const unknown = wanted.filter((w) => !known.has(w));
206
+ if (unknown.length > 0) {
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`);
208
+ return { ok: false };
209
+ }
210
+ rules = rules.filter((r) => wanted.includes(r.id));
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`);
218
+ }
219
+ /**
220
+ * Build the registries the doc-reference plane resolves against — only when a
221
+ * rule of that plane is actually in scope.
222
+ *
223
+ * Every other plane reads files; this one reads shrk's own registries, which
224
+ * cost a workspace inspection. Paying that on every `gates coverage` in a repo
225
+ * with no doc-reference rules would be a tax on the common case.
226
+ */
227
+ async function inspectionIfNeeded(cwd, rules) {
228
+ if (!rules.some((r) => r.plane === 'doc-reference'))
229
+ return undefined;
230
+ // Playbook / construct ids come from a cache an ASYNC load populates; the
231
+ // resolver is sync. Warming here is what makes a correct pack playbook cited
232
+ // in prose actually resolve.
233
+ const inspection = await inspectSharkcraft({ cwd });
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);
237
+ return inspection;
238
+ }
82
239
  export const gatesListCommand = {
83
240
  name: 'list',
84
241
  description: 'Every data-defined rule across every plane, with its severity and empty-match policy.',
@@ -95,7 +252,10 @@ export const gatesListCommand = {
95
252
  const rules = planes.planes
96
253
  ? prep.value.rules.filter((r) => planes.planes.has(r.plane))
97
254
  : prep.value.rules;
98
- 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)
99
259
  return writeNoRules(json);
100
260
  if (json) {
101
261
  process.stdout.write(asJson({
@@ -109,6 +269,8 @@ export const gatesListCommand = {
109
269
  failOnEmpty: r.failOnEmpty,
110
270
  selfTest: r.selfTest ?? null,
111
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 })),
112
274
  diagnostics: prep.value.planeDiagnostics,
113
275
  }) + '\n');
114
276
  return ExitCode.VerifiedPass;
@@ -130,6 +292,10 @@ export const gatesListCommand = {
130
292
  process.stdout.write(` ${r.description}\n`);
131
293
  }
132
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
+ }
133
299
  for (const d of prep.value.planeDiagnostics)
134
300
  process.stdout.write(` ! ${d}\n`);
135
301
  process.stdout.write('\nRun `shrk gates coverage` to see what each one actually matches.\n');
@@ -139,8 +305,9 @@ export const gatesListCommand = {
139
305
  export const gatesCoverageCommand = {
140
306
  name: 'coverage',
141
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.',
142
- usage: 'shrk gates coverage [--plane <p>] [--strict] [--json]',
143
- booleanFlags: new Set(['json', 'strict']),
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]),
144
311
  async run(args) {
145
312
  const prep = await prepare(args);
146
313
  if (!prep.ok)
@@ -149,85 +316,1025 @@ export const gatesCoverageCommand = {
149
316
  if (!planes.ok)
150
317
  return ExitCode.UsageError;
151
318
  const json = flagBool(args, 'json');
152
- const rules = planes.planes
153
- ? prep.value.rules.filter((r) => planes.planes.has(r.plane))
154
- : prep.value.rules;
155
- if (rules.length === 0)
156
- return writeNoRules(json);
157
- const report = buildGateCoverage(prep.value.cwd, rules, prep.value.excludeDirs);
319
+ const filtered = filterRules(prep.value.rules, planes.planes, flagString(args, 'only'), prep.value.rejectedRules);
320
+ if (!filtered.ok)
321
+ return ExitCode.UsageError;
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
+ }
328
+ // Scoping the STALE-SELECTOR check to the diff is what moves it from a
329
+ // CI-only report to something you can afford on every save — a stale glob
330
+ // is then caught the moment you cause it, not the next morning.
331
+ const scope = resolveScope(args, prep.value.cwd);
332
+ if (scope.error) {
333
+ process.stderr.write(`Cannot scope to the changeset: ${scope.error}\n`);
334
+ return ExitCode.UsageError;
335
+ }
336
+ const { selected, skippedByScope } = narrowToScope(filtered.rules, scope.files);
337
+ if (selected.length === 0 && rejected.length === 0) {
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
+ });
345
+ if (json) {
346
+ process.stdout.write(asJson({
347
+ schema: 'sharkcraft.gate-coverage/v1',
348
+ rules: [],
349
+ total: 0,
350
+ scoped: true,
351
+ skippedByScope,
352
+ exitCode: emptyEnv.exit,
353
+ gate: emptyEnv,
354
+ }) + '\n');
355
+ }
356
+ else {
357
+ process.stdout.write(header('Gate-rule coverage'));
358
+ process.stdout.write(` No rule's footprint intersects the changeset (${skippedByScope} skipped by scope).\n` +
359
+ ' Nothing was checked — this is NOT a pass.\n');
360
+ }
361
+ return emptyEnv.exit;
362
+ }
363
+ const rules = selected;
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);
158
366
  // A rule that matched nothing is NOT-VERIFIED (2) by default — it neither
159
367
  // passed nor failed, it never ran. `failOnEmpty` on the rule (or the global
160
- // --strict promotion) turns that into a hard failure.
161
- 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;
162
375
  const softEmpty = report.rules.filter((r) => r.status === 'empty' && !r.failOnEmpty);
163
- const exit = hardFailures.length > 0
164
- ? ExitCode.Failure
165
- : softEmpty.length > 0
166
- ? ExitCode.NotVerified
167
- : ExitCode.VerifiedPass;
168
376
  if (json) {
169
377
  process.stdout.write(asJson({
170
378
  ...report,
171
- 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,
383
+ ...(scope.files ? { scoped: true, skippedByScope } : {}),
172
384
  exitCode: exit,
173
- gate: buildGateEnvelope('gates coverage', exit, report.rules.map((r) => ({
174
- id: r.id,
175
- type: r.plane,
176
- status: r.status === 'ok'
177
- ? 'passed'
178
- : r.status === 'empty'
179
- ? r.failOnEmpty
180
- ? 'failed'
181
- : 'skipped'
182
- : r.status === 'error'
183
- ? 'error'
184
- : 'failed',
185
- severity: r.failOnEmpty ? 'error' : 'warning',
186
- counts: { files: r.filesMatched, units: r.unitsMatched },
187
- violations: r.expectationFailures.map((f) => ({ id: r.id, message: f })),
188
- ...(r.status === 'empty' ? { skipReason: `matched 0 ${r.unitLabel}` } : {}),
189
- ...(r.error ? { error: r.error } : {}),
190
- }))),
385
+ gate: env,
191
386
  }) + '\n');
192
387
  return exit;
193
388
  }
194
389
  process.stdout.write(header('Gate-rule coverage'));
195
- process.stdout.write(kv('rules', String(report.total)) + '\n');
196
- process.stdout.write(kv('matched nothing', `${report.empty}${report.empty > 0 ? ' ← stale selector suspects' : ''}`) + '\n');
390
+ process.stdout.write(kv('rules', `${report.total}${skippedByScope > 0 ? ` (${skippedByScope} skipped by scope)` : ''}`) + '\n');
391
+ if (scope.files) {
392
+ process.stdout.write(kv('scope', `changed-only (${scope.files.length} file(s))`) + '\n');
393
+ }
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');
197
406
  if (report.errored > 0)
198
407
  process.stdout.write(kv('misconfigured', String(report.errored)) + '\n');
199
408
  if (report.expectationFailures > 0) {
200
409
  process.stdout.write(kv('broken selfTest', String(report.expectationFailures)) + '\n');
201
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
+ }
415
+ // The shared extractors, ONCE. Their whole value is that N consumers cannot
416
+ // disagree about which set they check — so one line proving the shared set
417
+ // is live and non-empty covers all N.
418
+ if (report.extractors.length > 0) {
419
+ process.stdout.write('\n shared extractors\n');
420
+ for (const e of report.extractors) {
421
+ const mark = e.error ? '!' : e.idsMatched === 0 ? '✗' : '✓';
422
+ process.stdout.write(` ${mark} $use:${e.id} — ${e.idsMatched} ids across ${e.filesMatched} file(s), ` +
423
+ `shared by ${e.consumers.length} rule(s): ${e.consumers.join(', ')}\n`);
424
+ if (e.sampleIds.length > 0)
425
+ process.stdout.write(` e.g. ${e.sampleIds.join(', ')}\n`);
426
+ if (e.error)
427
+ process.stdout.write(` ! ${e.error}\n`);
428
+ if (!e.error && e.idsMatched === 0) {
429
+ process.stdout.write(' matched nothing — every consumer of this extractor is checking an empty set\n');
430
+ }
431
+ }
432
+ }
202
433
  process.stdout.write('\n');
203
- for (const r of report.rules) {
204
- const mark = r.status === 'ok' ? '✓' : r.status === 'empty' ? (r.failOnEmpty ? '✗' : '–') : '✗';
205
- process.stdout.write(` ${mark} [${r.plane}] ${r.id} — ${r.unitsMatched} ${r.unitLabel} across ${r.filesMatched} file(s)\n`);
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
+ : '✗';
450
+ process.stdout.write(` ${mark} [${r.plane}] ${r.id}${r.viaExtractor ? ` (via $use:${r.viaExtractor})` : ''}` +
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`);
206
481
  if (r.sampleIds.length > 0) {
207
482
  process.stdout.write(` e.g. ${r.sampleIds.join(', ')}\n`);
208
483
  }
209
484
  if (r.status === 'empty') {
210
- 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`);
495
+ // When the engine knows WHY a correct zero-match is probably not what
496
+ // the author meant, say so here rather than leaving them to rediscover
497
+ // it — this is the one dead end `import-edges` reliably produces.
498
+ if (r.hint)
499
+ process.stdout.write(` → ${r.hint}\n`);
211
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`);
212
505
  if (r.error)
213
506
  process.stdout.write(` ! ${r.error}\n`);
214
507
  for (const f of r.expectationFailures)
215
508
  process.stdout.write(` ! selfTest: ${f}\n`);
216
509
  }
217
- if (exit === ExitCode.VerifiedPass) {
218
- 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`);
219
524
  }
220
- else if (exit === ExitCode.NotVerified) {
221
- process.stdout.write(`\n${softEmpty.length} rule(s) matched nothing NOT a pass. Fix the selector, or set \`failOnEmpty: true\`\n` +
222
- '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`);
223
550
  }
224
551
  return exit;
225
552
  },
226
553
  };
554
+ export const gatesCheckCommand = {
555
+ name: 'check',
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).',
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]),
560
+ async run(args) {
561
+ const prep = await prepare(args);
562
+ if (!prep.ok)
563
+ return prep.code;
564
+ const planes = parsePlanes(args);
565
+ if (!planes.ok)
566
+ return ExitCode.UsageError;
567
+ const json = flagBool(args, 'json');
568
+ const filtered = filterRules(prep.value.rules, planes.planes, flagString(args, 'only'), prep.value.rejectedRules);
569
+ if (!filtered.ok)
570
+ return ExitCode.UsageError;
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
+ }
580
+ const scope = resolveScope(args, prep.value.cwd);
581
+ if (scope.error) {
582
+ process.stderr.write(`Cannot scope to the changeset: ${scope.error}\n`);
583
+ return ExitCode.UsageError;
584
+ }
585
+ const { selected, skippedByScope } = narrowToScope(filtered.rules, scope.files);
586
+ const noSpawn = flagBool(args, 'no-spawn');
587
+ const planeRun = selected.length === 0
588
+ ? { results: [], diagnostics: [] }
589
+ : runGatePlanes(selected, {
590
+ cwd: prep.value.cwd,
591
+ excludeDirs: prep.value.excludeDirs,
592
+ ...(scope.files ? { changedFiles: scope.files } : {}),
593
+ ...(noSpawn ? { noSpawn: true } : {}),
594
+ ...(await inspectionIfNeeded(prep.value.cwd, selected).then((i) => i ? { inspection: i } : {})),
595
+ });
596
+ const run = { ...planeRun, results: [...planeRun.results, ...rejected] };
597
+ const inScope = selected.length + rejected.length;
598
+ // Only an ERROR-severity failure blocks. A warning-severity rule reports
599
+ // without failing, exactly as its own plane's verb does.
600
+ //
601
+ // `--strict` promotes those warnings to blocking — the documented switch for
602
+ // a zero-warning CI. It reuses the established local meaning of `--strict`
603
+ // (`shrk check --strict` already does exactly this) rather than inventing a
604
+ // second severity model; the GLOBAL `--strict` promotion of not-verified
605
+ // (`2` → `1`) is applied once in `runCli` and composes with this.
606
+ const strict = flagBool(args, 'strict');
607
+ const failedRules = run.results.filter((r) => r.status === 'failed' || r.status === 'error');
608
+ const blocking = failedRules.filter((r) => strict || r.severity === 'error');
609
+ const failedWarnings = failedRules.filter((r) => r.severity !== 'error');
610
+ // An ERRORED rule proved nothing — it never reached a subject. It is
611
+ // therefore not `evaluated`, whatever its severity; otherwise a
612
+ // warning-severity rule that could not run exits 0 with a green banner,
613
+ // which is the silent-green this engine exists to prevent.
614
+ const evaluated = run.results.filter((r) => r.status !== 'skipped' && r.status !== 'error').length;
615
+ const skipped = run.results.length - evaluated;
616
+ // Exit must match the banner. Two kinds of "didn't run" are NOT the same
617
+ // thing, and conflating them breaks the surface either way:
618
+ // • skipped BY SCOPE — the user asked for the narrowing (`--changed-only`),
619
+ // so the rules outside it are deliberately out of the question. Failing
620
+ // here would make a pre-commit hook exit non-zero on every commit.
621
+ // • skipped BY ACCIDENT — a selector matched nothing, or --no-spawn
622
+ // dropped the drift half. Nobody asked for that, and it is exactly the
623
+ // silent-green this engine exists to prevent, so it is `2`.
624
+ // Matches `check wiring --changed-only`, which draws the same line.
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
631
+ ? ExitCode.Failure
632
+ : (evaluated === 0 && selected.length > 0) || skipped > 0
633
+ ? ExitCode.NotVerified
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;
654
+ const diagnostics = [...run.diagnostics, ...prep.value.planeDiagnostics];
655
+ if (json) {
656
+ process.stdout.write(asJson({
657
+ schema: SCHEMA,
658
+ configured: filtered.rules.length + rejected.length,
659
+ // Pack rules the merge seam refused — errored rows in `gate.rules`.
660
+ rejected: rejected.length,
661
+ selected: selected.length,
662
+ evaluated,
663
+ skipped,
664
+ skippedByScope,
665
+ ...(scope.files ? { scoped: true, changedFiles: scope.files.length } : {}),
666
+ noSpawn,
667
+ strict,
668
+ failed: blocking.length,
669
+ failedWarnings: failedWarnings.length,
670
+ verdict: exit === ExitCode.Failure ? 'errors' : exit === ExitCode.VerifiedPass ? 'pass' : 'not-verified',
671
+ diagnostics,
672
+ exitCode: exit,
673
+ gate: env,
674
+ }) + '\n');
675
+ return exit;
676
+ }
677
+ process.stdout.write(header('Gate check — every rule plane'));
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');
686
+ if (scope.files) {
687
+ process.stdout.write(kv('scope', `changed-only (${scope.files.length} file(s))`) + '\n');
688
+ }
689
+ if (noSpawn)
690
+ process.stdout.write(kv('mode', '--no-spawn — shell-executing checks skipped') + '\n');
691
+ process.stdout.write(kv('violations', `${blocking.length} blocking rule(s), ${failedWarnings.length} warning rule(s)` +
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`.
700
+ for (const plane of GATE_PLANES) {
701
+ const inPlane = env.rules.filter((r) => r.type === plane);
702
+ if (inPlane.length === 0)
703
+ continue;
704
+ for (const r of inPlane) {
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
+ : '!';
716
+ const counts = Object.entries(r.counts).map(([k, v]) => `${k} ${v}`).join(', ');
717
+ process.stdout.write(` ${mark} [${plane}] ${r.id}${counts ? ` (${counts})` : ''}\n`);
718
+ if (r.status === 'skipped' && r.skipReason) {
719
+ process.stdout.write(` SKIPPED — ${r.skipReason}\n`);
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
+ }
729
+ if (r.error)
730
+ process.stdout.write(` ! ${r.error}\n`);
731
+ for (const v of r.violations.slice(0, 10)) {
732
+ const at = v.file ? ` (${v.file}${v.line !== undefined ? `:${v.line}` : ''})` : '';
733
+ process.stdout.write(` • ${v.id}${at}${v.message ? ` — ${v.message}` : ''}\n`);
734
+ }
735
+ if (r.violations.length > 10) {
736
+ process.stdout.write(` … (${r.violations.length - 10} more)\n`);
737
+ }
738
+ const hint = r.violations.find((v) => v.hint)?.hint;
739
+ if (hint && r.violations.length > 0)
740
+ process.stdout.write(` → ${hint}\n`);
741
+ }
742
+ }
743
+ for (const d of diagnostics)
744
+ process.stdout.write(` ! ${d}\n`);
745
+ process.stdout.write(unitNotes.text);
746
+ if (strict && failedWarnings.length > 0 && exit === ExitCode.Failure) {
747
+ process.stdout.write(`\n${failedWarnings.length} warning rule(s) reported findings and --strict promoted them to failures.\n`);
748
+ }
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');
756
+ }
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`);
782
+ }
783
+ return exit;
784
+ },
785
+ };
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) {
823
+ const sides = new Map();
824
+ // Split on whitespace that precedes a `<side>=`, so a glob may contain none.
825
+ for (const part of spec.trim().split(/\s+(?=(?:declared|registered)=)/)) {
826
+ const eq = part.indexOf('=');
827
+ if (eq === -1)
828
+ return { error: `"${part}" is not <side>=<glob>:<pattern>` };
829
+ const side = part.slice(0, eq).trim();
830
+ if (side !== 'declared' && side !== 'registered') {
831
+ return { error: `unknown side "${side}" — use declared= or registered=` };
832
+ }
833
+ const rest = part.slice(eq + 1);
834
+ const colon = rest.indexOf(':');
835
+ if (colon <= 0)
836
+ return { error: `"${side}=" needs <glob>:<pattern>` };
837
+ sides.set(side, { files: [rest.slice(0, colon)], match: rest.slice(colon + 1) });
838
+ }
839
+ const declared = sides.get('declared');
840
+ const registered = sides.get('registered');
841
+ if (!declared || !registered) {
842
+ return { error: 'both declared=<glob>:<pattern> and registered=<glob>:<pattern> are required' };
843
+ }
844
+ const toSource = (side) => ({
845
+ files: side.files,
846
+ extract: 'regex-capture',
847
+ pattern: side.match,
848
+ ...(flags !== undefined && flags.length > 0 ? { flags } : {}),
849
+ });
850
+ return {
851
+ rule: { id: '(try)', declared: toSource(declared), registered: toSource(registered) },
852
+ };
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
+ }
987
+ export const gatesTryCommand = {
988
+ name: 'try',
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]",
991
+ booleanFlags: new Set(['json', 'full']),
992
+ flags: new Set([...GATES_FLAGS, ...TRY_ONLY_FLAGS]),
993
+ async run(args) {
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
+ }
1001
+ const ruleFile = flagString(args, 'rule-file') ?? args.positional[0];
1002
+ const inline = flagString(args, 'wiring');
1003
+ const inlineFlags = flagString(args, 'flags');
1004
+ if (!ruleFile && !inline) {
1005
+ process.stderr.write('Usage: shrk gates try --rule-file <rule.json> [--plane <p>] [--full | --limit N]\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' +
1008
+ ' --full dump the WHOLE extracted set (default: first 5)\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");
1017
+ return ExitCode.UsageError;
1018
+ }
1019
+ const cwd = resolveCwd(args);
1020
+ const json = flagBool(args, 'json');
1021
+ // A candidate may `$use` the project's shared extractors, so the real
1022
+ // config is loaded for its `extractors` map (and for nothing else — no
1023
+ // declared rule is read, and nothing is written).
1024
+ const loaded = await resolveProjectConfig(cwd);
1025
+ const extractors = loaded.ok ? (loaded.value.config.extractors ?? {}) : {};
1026
+ const excludeDirs = loaded.ok ? planeScanExcludeDirs(cwd, loaded.value.sharkcraftDir) : [];
1027
+ let raw;
1028
+ let plane = 'wiring';
1029
+ if (inline) {
1030
+ const parsed = parseInlineWiring(inline, inlineFlags);
1031
+ if (parsed.error) {
1032
+ process.stderr.write(`Invalid --wiring spec: ${parsed.error}\n`);
1033
+ return ExitCode.UsageError;
1034
+ }
1035
+ raw = parsed.rule;
1036
+ }
1037
+ else {
1038
+ if (!existsSync(ruleFile)) {
1039
+ process.stderr.write(`Rule file not found: ${ruleFile}\n`);
1040
+ return ExitCode.UsageError;
1041
+ }
1042
+ try {
1043
+ raw = JSON.parse(readFileSync(ruleFile, 'utf8'));
1044
+ }
1045
+ catch (e) {
1046
+ process.stderr.write(`${ruleFile} is not valid JSON: ${e.message}\n`);
1047
+ return ExitCode.UsageError;
1048
+ }
1049
+ const planes = parsePlanes(args);
1050
+ if (!planes.ok)
1051
+ return ExitCode.UsageError;
1052
+ const explicit = planes.planes ? [...planes.planes][0] : undefined;
1053
+ const inferred = explicit ?? inferPlane(raw);
1054
+ if (!inferred) {
1055
+ process.stderr.write('Could not infer the plane from the rule shape. Pass --plane wiring|policy|registry|registration|baseline|generated.\n');
1056
+ return ExitCode.UsageError;
1057
+ }
1058
+ plane = inferred;
1059
+ }
1060
+ // Validate with the SAME schema the loader uses, so a spec that passes here
1061
+ // is a spec that will load — the point of the REPL is that what you see is
1062
+ // what you get once you paste it in.
1063
+ const schema = PLANE_SCHEMAS[plane];
1064
+ const parsed = schema.safeParse(raw);
1065
+ if (!parsed.success) {
1066
+ const summary = (parsed.error?.issues ?? [])
1067
+ .map((iss) => `${iss.path.join('.') || '<root>'}: ${iss.message}`)
1068
+ .join('; ');
1069
+ if (json)
1070
+ process.stdout.write(asJson({ schema: SCHEMA, plane, valid: false, error: summary }) + '\n');
1071
+ else
1072
+ process.stderr.write(`Invalid ${plane} rule: ${summary}\n`);
1073
+ return ExitCode.UsageError;
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;
1089
+ // Resolve `$use` exactly as the loader would, so a candidate referencing a
1090
+ // shared extractor is tried against the real shared definition.
1091
+ const planeKey = PLANE_CONFIG_KEY[plane];
1092
+ const resolved = resolvePlaneExtractors({ [planeKey]: [candidate] }, extractors);
1093
+ if (resolved.errors.length > 0) {
1094
+ const summary = resolved.errors.map((e) => e.message).join('; ');
1095
+ if (json)
1096
+ process.stdout.write(asJson({ schema: SCHEMA, plane, valid: false, error: summary }) + '\n');
1097
+ else
1098
+ process.stderr.write(`Unresolvable extractor reference: ${summary}\n`);
1099
+ return ExitCode.UsageError;
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
+ }
1113
+ const resolvedByKey = resolved;
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';
1133
+ // The wiring plane already has a BOTH-SIDES explainer (resolved declared
1134
+ // set, resolved registered set, and the set-difference between them) — the
1135
+ // exact view an author tightening a selector needs. Reuse it rather than
1136
+ // printing a second, thinner one that could disagree with `gates explain`.
1137
+ if (plane === 'wiring') {
1138
+ const explain = explainWiring(cwd, rule, { excludeDirs });
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;
1162
+ }
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;
1169
+ }
1170
+ // `noSpawn` is not a performance choice here: a `--rule-file` is arbitrary
1171
+ // JSON, and honouring a `regen` / `compute.run` from it would turn a
1172
+ // read-only preview into shell execution from an untrusted file.
1173
+ const run = runGatePlanes([view], {
1174
+ cwd,
1175
+ excludeDirs,
1176
+ noSpawn: true,
1177
+ ...(inspection ? { inspection } : {}),
1178
+ });
1179
+ const result = run.results[0];
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
+ }));
1206
+ if (json) {
1207
+ process.stdout.write(asJson({
1208
+ schema: 'sharkcraft.gates-try/v1',
1209
+ plane,
1210
+ valid: true,
1211
+ coverage: cov,
1212
+ ...(cov.hint ? { hint: cov.hint } : {}),
1213
+ result: result ?? null,
1214
+ selfTest: selfTestPayload(cov, declared),
1215
+ ...(notes.length > 0 ? { notes } : {}),
1216
+ note: 'nothing was written — paste the rule into sharkcraft.config.ts to keep it',
1217
+ exitCode: settled.exit,
1218
+ settled,
1219
+ }) + '\n');
1220
+ return settled.exit;
1221
+ }
1222
+ process.stdout.write(header(`gates try — candidate ${plane} rule "${view.id}"`));
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');
1226
+ process.stdout.write(kv(cov.unitLabel, String(cov.unitsMatched)) + '\n');
1227
+ process.stdout.write(kv('coverage', formatCoverage(cov.coverage)) + '\n');
1228
+ if (cov.viaExtractor)
1229
+ process.stdout.write(kv('via extractor', `$use:${cov.viaExtractor}`) + '\n');
1230
+ // Tuning a selector against a large tree needs the WHOLE candidate set —
1231
+ // a five-item sample cannot tell you whether the tail is right.
1232
+ const full = flagBool(args, 'full');
1233
+ const limitRaw = flagString(args, 'limit');
1234
+ const limit = limitRaw !== undefined ? Number.parseInt(limitRaw, 10) : undefined;
1235
+ if (limitRaw !== undefined && (!Number.isFinite(limit) || (limit ?? 0) < 1)) {
1236
+ process.stderr.write(`--limit must be a positive integer, got "${limitRaw}".\n`);
1237
+ return ExitCode.UsageError;
1238
+ }
1239
+ const shown = full ? allIds : allIds.slice(0, limit ?? 5);
1240
+ if (shown.length > 0) {
1241
+ const label = shown.length === allIds.length ? `all ${shown.length}` : `first ${shown.length} of ${allIds.length}`;
1242
+ process.stdout.write(`\n extracted (${label}):\n`);
1243
+ for (const id of shown)
1244
+ process.stdout.write(` ${id}\n`);
1245
+ if (shown.length < allIds.length) {
1246
+ process.stdout.write(` … (${allIds.length - shown.length} more — re-run with --full)\n`);
1247
+ }
1248
+ }
1249
+ if (cov.error)
1250
+ process.stdout.write(`\n ! ${cov.error}\n`);
1251
+ if (result) {
1252
+ process.stdout.write(`\n would be: ${result.status}${result.violations.length > 0 ? ` (${result.violations.length} violation(s))` : ''}\n`);
1253
+ for (const v of result.violations.slice(0, 20)) {
1254
+ const at = v.file ? ` (${v.file}${v.line !== undefined ? `:${v.line}` : ''})` : '';
1255
+ process.stdout.write(` • ${v.id}${at}\n`);
1256
+ }
1257
+ if (result.violations.length > 20) {
1258
+ process.stdout.write(` … (${result.violations.length - 20} more)\n`);
1259
+ }
1260
+ }
1261
+ writeSelfTestBlock(view.id, cov, declared);
1262
+ writeNotes(notes);
1263
+ if (cov.status === 'empty') {
1264
+ process.stdout.write('\n This selector matched NOTHING — tighten it before adding it to config.\n');
1265
+ if (cov.hint)
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`);
1269
+ }
1270
+ process.stdout.write('\n Nothing was written. Paste the rule into sharkcraft.config.ts to keep it.\n');
1271
+ writeTryVerdict(settled, cov, cov.status === 'empty' ? 'The candidate matched nothing — it proved nothing.' : undefined);
1272
+ return settled.exit;
1273
+ },
1274
+ };
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;
1280
+ /** The loader's own schema for each plane — one validation, not a second one. */
1281
+ const PLANE_SCHEMAS = {
1282
+ wiring: WiringRuleSchema,
1283
+ policy: PolicyRuleSchema,
1284
+ registry: RegistryDeclarationSchema,
1285
+ registration: RegistrationIdiomSchema,
1286
+ baseline: BaselineRuleSchema,
1287
+ generated: GeneratedArtifactRuleSchema,
1288
+ 'doc-reference': DocReferenceRuleSchema,
1289
+ };
1290
+ /**
1291
+ * Infer a candidate's plane from its shape.
1292
+ *
1293
+ * Each plane has a field no other plane uses, so inference is exact rather than
1294
+ * a guess — and an ambiguous shape returns `undefined` so the author is asked
1295
+ * with `--plane` instead of being silently run on the wrong engine.
1296
+ */
1297
+ function inferPlane(raw) {
1298
+ if (typeof raw !== 'object' || raw === null)
1299
+ return undefined;
1300
+ const o = raw;
1301
+ if ('generatedGlob' in o)
1302
+ return 'generated';
1303
+ if ('tokenPattern' in o && 'resolvesAs' in o)
1304
+ return 'doc-reference';
1305
+ if ('compute' in o)
1306
+ return 'baseline';
1307
+ if ('declared' in o && 'provided' in o && 'consumed' in o)
1308
+ return 'registration';
1309
+ if ('declared' in o || 'registered' in o || 'chain' in o)
1310
+ return 'wiring';
1311
+ if ('surface' in o && 'pattern' in o)
1312
+ return 'policy';
1313
+ if ('source' in o && 'name' in o)
1314
+ return 'registry';
1315
+ return undefined;
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
+ }
227
1331
  /** Render a registry inventory as the trust-layer explain view. */
228
- function explainRegistry(cwd, decl, excludeDirs) {
1332
+ function explainRegistry(cwd, view, excludeDirs) {
1333
+ const decl = view.raw;
229
1334
  const inventory = scanRegistry(cwd, decl, { excludeDirs });
230
1335
  const insp = inspectSource(cwd, decl.source, excludeDirs);
1336
+ if (decl.source.$use)
1337
+ process.stdout.write(kv('via extractor', `$use:${decl.source.$use}`) + '\n');
231
1338
  process.stdout.write(kv('files scanned', String(insp.filesScanned)) + '\n');
232
1339
  process.stdout.write(kv('ids', String(inventory.entries.length)) + '\n');
233
1340
  for (const e of inventory.entries.slice(0, 60)) {
@@ -238,16 +1345,19 @@ function explainRegistry(cwd, decl, excludeDirs) {
238
1345
  }
239
1346
  for (const d of inventory.diagnostics)
240
1347
  process.stdout.write(` ! ${d}\n`);
1348
+ writeExplainUnitStates(cwd, view, excludeDirs);
241
1349
  }
242
1350
  /** Render the three sides of a registration idiom. */
243
- function explainRegistration(cwd, idiom, excludeDirs) {
1351
+ function explainRegistration(cwd, view, excludeDirs) {
1352
+ const idiom = view.raw;
244
1353
  for (const [label, source] of [
245
1354
  ['declared', idiom.declared],
246
1355
  ['provided', idiom.provided],
247
1356
  ['consumed', idiom.consumed],
248
1357
  ]) {
249
1358
  const insp = inspectSource(cwd, source, excludeDirs);
250
- process.stdout.write(kv(label, `${insp.ids.length} token(s) across ${insp.filesScanned} file(s)`) + '\n');
1359
+ process.stdout.write(kv(label, `${insp.ids.length} token(s) across ${insp.filesScanned} file(s)` +
1360
+ (source.$use ? ` (via $use:${source.$use})` : '')) + '\n');
251
1361
  if (insp.error)
252
1362
  process.stdout.write(` ! ${insp.error}\n`);
253
1363
  for (const s of insp.sites.slice(0, 20)) {
@@ -256,6 +1366,7 @@ function explainRegistration(cwd, idiom, excludeDirs) {
256
1366
  if (insp.sites.length > 20)
257
1367
  process.stdout.write(` … (${insp.sites.length - 20} more)\n`);
258
1368
  }
1369
+ writeExplainUnitStates(cwd, view, excludeDirs);
259
1370
  process.stdout.write(`\n Query one token's chain with \`shrk wiring chain <token>\`.\n`);
260
1371
  }
261
1372
  export const gatesExplainCommand = {
@@ -306,6 +1417,13 @@ export const gatesExplainCommand = {
306
1417
  args.flags.set('id', view.id);
307
1418
  return generatedExplainCommand.run(args);
308
1419
  }
1420
+ // The doc-reference plane owns its explain view too — it resolves against
1421
+ // registries, not file globs, so the generic source-based renderer below
1422
+ // has nothing to show for it.
1423
+ if (view.plane === 'doc-reference') {
1424
+ args.flags.set('id', view.id);
1425
+ return docsReferencesExplainCommand.run(args);
1426
+ }
309
1427
  if (view.plane === 'wiring') {
310
1428
  const explain = explainWiring(prep.value.cwd, view.raw, {
311
1429
  excludeDirs: prep.value.excludeDirs,
@@ -334,10 +1452,10 @@ export const gatesExplainCommand = {
334
1452
  if (view.description)
335
1453
  process.stdout.write(` ${view.description}\n`);
336
1454
  if (view.plane === 'registry') {
337
- explainRegistry(prep.value.cwd, view.raw, prep.value.excludeDirs);
1455
+ explainRegistry(prep.value.cwd, view, prep.value.excludeDirs);
338
1456
  }
339
1457
  else {
340
- explainRegistration(prep.value.cwd, view.raw, prep.value.excludeDirs);
1458
+ explainRegistration(prep.value.cwd, view, prep.value.excludeDirs);
341
1459
  }
342
1460
  return ExitCode.VerifiedPass;
343
1461
  },
@@ -362,15 +1480,147 @@ export async function tryExplainGateRule(args, id) {
362
1480
  const forwarded = { ...args, positional: [id] };
363
1481
  return gatesExplainCommand.run(forwarded);
364
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
+ };
365
1603
  export const gatesCommand = {
366
1604
  name: 'gates',
367
- description: 'Rule-authoring trust layer: list every data-defined rule, show what each one MATCHED (the stale-selector detector), and explain any one of them. Read-only. Not `shrk gate`, which runs the quality-gate pipeline.',
368
- usage: 'shrk gates list | coverage [--strict] | explain <id>',
369
- booleanFlags: new Set(['json', 'strict']),
370
- async run(args) {
371
- const sub = args.positional[0];
372
- process.stderr.write((sub ? `Unknown subcommand "${sub}". ` : '') +
373
- 'Usage: shrk gates list | coverage [--plane <p>] [--strict] | explain <id>\n' +
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,
1611
+ booleanFlags: new Set(['json', 'strict', 'changed-only', 'no-spawn']),
1612
+ async run() {
1613
+ process.stderr.write('Usage: shrk gates <subcommand>\n' +
1614
+ ' check [--plane <p>] [--only <ids>] [--changed-only|--since <ref>] [--no-spawn]\n' +
1615
+ ' run EVERY plane\'s violation check — one exit code (the CI / pre-commit primitive)\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' +
1618
+ ' list [--plane <p>] every declared rule, with severity + empty-match policy\n' +
1619
+ ' explain <id> the concrete inputs one rule resolved\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" +
374
1624
  '(`shrk gate`, singular, runs the quality-gate pipeline — a different verb.)\n');
375
1625
  return ExitCode.UsageError;
376
1626
  },