@event4u/agent-config 16.2.0 → 16.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (332) hide show
  1. package/.augment-plugin/marketplace.json +3 -3
  2. package/.augment-plugin/plugin.json +1 -1
  3. package/.claude-plugin/marketplace.json +1 -1
  4. package/CHANGELOG.md +89 -237
  5. package/README.md +14 -12
  6. package/dist/agent-src/commands/analyze/inbox.md +1 -1
  7. package/dist/agent-src/commands/analyze/roadmap-repos.md +1 -1
  8. package/dist/agent-src/commands/bug/fix.md +17 -2
  9. package/dist/agent-src/commands/feature/plan.md +1 -1
  10. package/dist/agent-src/commands/feature/roadmap.md +1 -1
  11. package/dist/agent-src/commands/fix/ci.md +1 -1
  12. package/dist/agent-src/commands/implement-ticket.md +1 -1
  13. package/dist/agent-src/commands/jira-ticket.md +1 -1
  14. package/dist/agent-src/commands/roadmap/create.md +1 -1
  15. package/dist/agent-src/commands/roadmap/materialize.md +1 -1
  16. package/dist/agent-src/commands/roadmap/process-full.md +4 -4
  17. package/dist/agent-src/commands/roadmap/resolve-blockers.md +361 -0
  18. package/dist/agent-src/commands/roadmap/triage-parked.md +325 -0
  19. package/dist/agent-src/commands/roadmap.md +18 -4
  20. package/dist/agent-src/commands/tests/create.md +17 -3
  21. package/dist/agent-src/commands/tests/execute.md +22 -5
  22. package/dist/agent-src/contexts/authority/decision-revisit-gate-enforcement.md +43 -0
  23. package/dist/agent-src/contexts/communication/rules-auto/design-fidelity-enforcement.md +24 -0
  24. package/dist/agent-src/contexts/execution/auto-dispatch-classification.md +6 -6
  25. package/dist/agent-src/contexts/execution/roadmap-process-loop.md +32 -2
  26. package/dist/agent-src/contexts/execution/session-canary-enforcement-history.md +67 -0
  27. package/dist/agent-src/contexts/execution/toolchain-resolver.md +101 -7
  28. package/dist/agent-src/rules/active-remediation.md +1 -1
  29. package/dist/agent-src/rules/autonomous-execution.md +1 -0
  30. package/dist/agent-src/rules/broken-access-control.md +1 -0
  31. package/dist/agent-src/rules/command-suggestion-policy.md +1 -0
  32. package/dist/agent-src/rules/context-hygiene.md +1 -1
  33. package/dist/agent-src/rules/decision-revisit-gate.md +16 -33
  34. package/dist/agent-src/rules/design-fidelity.md +13 -16
  35. package/dist/agent-src/rules/doc-screenshot-hygiene.md +1 -0
  36. package/dist/agent-src/rules/domain-safety-disclaimer.md +1 -0
  37. package/dist/agent-src/rules/domain-safety-pii.md +1 -0
  38. package/dist/agent-src/rules/domain-safety-retention.md +1 -0
  39. package/dist/agent-src/rules/engineering-safety-floor.md +1 -0
  40. package/dist/agent-src/rules/evaluator-independence.md +3 -2
  41. package/dist/agent-src/rules/fast-path-marker-visibility.md +1 -0
  42. package/dist/agent-src/rules/finance-safety-floor.md +1 -0
  43. package/dist/agent-src/rules/git-history-discipline.md +1 -0
  44. package/dist/agent-src/rules/image-likeness-and-rights.md +1 -0
  45. package/dist/agent-src/rules/legal-safety-floor.md +3 -2
  46. package/dist/agent-src/rules/lethal-trifecta-guard.md +1 -0
  47. package/dist/agent-src/rules/low-impact-corpus-privacy-floor.md +1 -0
  48. package/dist/agent-src/rules/media-governance-routing.md +14 -17
  49. package/dist/agent-src/rules/persona-governance.md +2 -2
  50. package/dist/agent-src/rules/provider-lifecycle-discipline.md +1 -1
  51. package/dist/agent-src/rules/question-not-instruction.md +1 -0
  52. package/dist/agent-src/rules/roadmap-progress-sync.md +1 -1
  53. package/dist/agent-src/rules/runtime-safety.md +1 -0
  54. package/dist/agent-src/rules/secret-vcs-guard.md +1 -0
  55. package/dist/agent-src/rules/security-sensitive-stop.md +3 -2
  56. package/dist/agent-src/rules/session-canary.md +8 -62
  57. package/dist/agent-src/rules/settings-ask-protocol.md +1 -0
  58. package/dist/agent-src/rules/source-confidentiality.md +1 -0
  59. package/dist/agent-src/rules/strategy-safety-floor.md +1 -0
  60. package/dist/agent-src/rules/tool-safety.md +1 -0
  61. package/dist/agent-src/rules/untrusted-input-defense.md +1 -0
  62. package/dist/agent-src/rules/user-interrupt-priority.md +1 -0
  63. package/dist/agent-src/scripts/archive_completed_roadmaps.ts +115 -20
  64. package/dist/agent-src/scripts/deferral_carry.ts +339 -0
  65. package/dist/agent-src/scripts/update_roadmap_progress.ts +4 -1
  66. package/dist/agent-src/skills/api-design/data/manifest.json +2 -2
  67. package/dist/agent-src/skills/api-testing/SKILL.md +17 -0
  68. package/dist/agent-src/skills/code-intelligence/SKILL.md +14 -2
  69. package/dist/agent-src/skills/design-system-capture/references/import-procedure.md +1 -1
  70. package/dist/agent-src/skills/existing-ui-audit/SKILL.md +2 -2
  71. package/dist/agent-src/skills/fe-design/SKILL.md +1 -1
  72. package/dist/agent-src/skills/iconography/SKILL.md +1 -1
  73. package/dist/agent-src/skills/js-library-packaging/SKILL.md +2 -2
  74. package/dist/agent-src/skills/monorepo-workspace/SKILL.md +1 -1
  75. package/dist/agent-src/skills/quality-tools/SKILL.md +11 -1
  76. package/dist/agent-src/skills/quality-tools/references/go-tools.md +76 -0
  77. package/dist/agent-src/skills/quality-tools/references/python-tools.md +104 -0
  78. package/dist/agent-src/skills/review-routing/SKILL.md +2 -2
  79. package/dist/agent-src/skills/skill-improvement-pipeline/SKILL.md +1 -1
  80. package/dist/agent-src/skills/storybook-workshop/SKILL.md +3 -3
  81. package/dist/agent-src/skills/tailwind-engineer/SKILL.md +1 -1
  82. package/dist/agent-src/skills/test-case-discovery/SKILL.md +7 -0
  83. package/dist/agent-src/skills/test-driven-development/SKILL.md +17 -0
  84. package/dist/agent-src/skills/test-performance/SKILL.md +19 -1
  85. package/dist/agent-src/skills/ui-component-architect/SKILL.md +2 -2
  86. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  87. package/dist/agent-src/templates/scripts/work_engine/directives/ui/apply.ts +48 -6
  88. package/dist/agent-src/templates/scripts/work_engine/stack/runner.ts +774 -109
  89. package/dist/cli/registry.js +1 -1
  90. package/dist/cli-delegate/archive_completed_roadmaps.js +92 -12
  91. package/dist/cli-delegate/check_update_banner.js +12 -1
  92. package/dist/cli-delegate/chunk-523HCFYO.js +90 -0
  93. package/dist/cli-delegate/{chunk-M2DPGFZ4.js → chunk-6V6TA25H.js} +22 -152
  94. package/dist/cli-delegate/chunk-7IQE3D5S.js +191 -0
  95. package/dist/cli-delegate/{chunk-6KFEC3G7.js → chunk-CQWX336L.js} +1 -1
  96. package/dist/cli-delegate/chunk-DEIV7XP7.js +108 -0
  97. package/dist/cli-delegate/chunk-EEHDXSYD.js +2040 -0
  98. package/dist/cli-delegate/{chunk-P6ZHP5W2.js → chunk-GKE7ZVFA.js} +4 -4
  99. package/dist/cli-delegate/chunk-JQOM7TTM.js +1201 -0
  100. package/dist/cli-delegate/{chunk-IGD6ERX6.js → chunk-JQVYKIZA.js} +10 -0
  101. package/dist/cli-delegate/{chunk-FC75PGJE.js → chunk-JY2I2DXV.js} +12 -1
  102. package/dist/cli-delegate/chunk-KB5FZ655.js +208 -0
  103. package/dist/cli-delegate/{chunk-7RQP4UDH.js → chunk-KR6EM23A.js} +235 -1455
  104. package/dist/cli-delegate/{chunk-LTYVIY76.js → chunk-LIYTLH6O.js} +1 -1
  105. package/dist/cli-delegate/{chunk-YJ72UIAK.js → chunk-NQ7WT43P.js} +1186 -244
  106. package/dist/cli-delegate/chunk-NXEILF2K.js +2160 -0
  107. package/dist/cli-delegate/{chunk-5R7HOLAF.js → chunk-NYX3L2PZ.js} +1 -1
  108. package/dist/cli-delegate/{chunk-4E2W63DA.js → chunk-OKJUIAPS.js} +62 -1
  109. package/dist/cli-delegate/{chunk-QAJS5YWY.js → chunk-PHD7OOLE.js} +2 -1
  110. package/dist/cli-delegate/{chunk-RGNIWTEA.js → chunk-QLDHIPWR.js} +1 -1
  111. package/dist/cli-delegate/{chunk-7XGBX6ZH.js → chunk-RQNNW7IF.js} +1314 -560
  112. package/dist/cli-delegate/{chunk-4XFP2Y3E.js → chunk-S4UMZNES.js} +1 -1
  113. package/dist/cli-delegate/{chunk-6KM6ORRY.js → chunk-S4YWNRKO.js} +24 -8
  114. package/dist/cli-delegate/{chunk-FP36SWJD.js → chunk-TRI6DQZ6.js} +2781 -340
  115. package/dist/cli-delegate/chunk-ULQRUR23.js +208 -0
  116. package/dist/cli-delegate/{chunk-NUHLI2RX.js → chunk-YCU3TIXH.js} +2 -0
  117. package/dist/cli-delegate/chunk-ZBZV2G5R.js +210 -0
  118. package/dist/cli-delegate/{chunk-WGXTVKCQ.js → chunk-ZUZKZYCN.js} +5 -3
  119. package/dist/cli-delegate/cmd_analyze_session.js +1 -1
  120. package/dist/cli-delegate/cmd_brand_status.js +1 -1
  121. package/dist/cli-delegate/cmd_conformance.js +1167 -234
  122. package/dist/cli-delegate/cmd_doctor.js +13 -6
  123. package/dist/cli-delegate/cmd_doctor_neighbours.js +23 -0
  124. package/dist/cli-delegate/cmd_explain.js +5 -5
  125. package/dist/cli-delegate/cmd_export.js +11 -8
  126. package/dist/cli-delegate/cmd_fleet.js +3 -3
  127. package/dist/cli-delegate/cmd_handoff.js +10 -7
  128. package/dist/cli-delegate/cmd_mcp_available.js +1 -1
  129. package/dist/cli-delegate/cmd_memory_get.js +3 -1
  130. package/dist/cli-delegate/cmd_migrate.js +2 -2
  131. package/dist/cli-delegate/cmd_packs_active.js +3 -3
  132. package/dist/cli-delegate/cmd_preflight.js +6 -6
  133. package/dist/cli-delegate/cmd_prune.js +11 -8
  134. package/dist/cli-delegate/cmd_refresh.js +2 -1
  135. package/dist/cli-delegate/cmd_route_audit.js +8 -6
  136. package/dist/cli-delegate/cmd_route_explain.js +4 -4
  137. package/dist/cli-delegate/cmd_session_recycle.js +4 -3
  138. package/dist/cli-delegate/cmd_settings_get.js +1 -1
  139. package/dist/cli-delegate/cmd_settings_migrate.js +11 -8
  140. package/dist/cli-delegate/cmd_settings_set.js +4 -4
  141. package/dist/cli-delegate/cmd_sync.js +11 -8
  142. package/dist/cli-delegate/cmd_uninstall.js +11 -8
  143. package/dist/cli-delegate/cmd_update.js +1 -1
  144. package/dist/cli-delegate/cmd_upgrade.js +3 -2
  145. package/dist/cli-delegate/cmd_validate.js +11 -8
  146. package/dist/cli-delegate/cmd_versions.js +1 -1
  147. package/dist/cli-delegate/deferral_carry.js +16 -0
  148. package/dist/cli-delegate/gate_execute.js +2 -2
  149. package/dist/cli-delegate/roadmap_gates.js +5 -5
  150. package/dist/cli-delegate/update_roadmap_progress.js +1 -1
  151. package/dist/discovery/deprecation-report.md +1 -1
  152. package/dist/discovery/discovery-manifest.json +127 -75
  153. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  154. package/dist/discovery/discovery-manifest.summary.md +3 -3
  155. package/dist/discovery/dormancy-report.md +1 -1
  156. package/dist/discovery/orphan-report.md +1 -1
  157. package/dist/discovery/packs.json +7 -5
  158. package/dist/discovery/trust-report.md +2 -2
  159. package/dist/discovery/workspaces.json +6 -4
  160. package/dist/hooks/dispatch.js +5357 -6656
  161. package/dist/hooks/dispatch.sha256 +1 -1
  162. package/dist/install/claudePathsPlan.js +47 -6
  163. package/dist/install/claudeRuleRewrite.js +7 -0
  164. package/dist/install/emit_host_rules_cli.js +39 -36
  165. package/dist/install/globalRuleScope.js +87 -0
  166. package/dist/install/host_rule_ownership.js +160 -0
  167. package/dist/install/install.mjs +2080 -1024
  168. package/dist/install/installThinLayer.js +261 -0
  169. package/dist/install/installedRuleLinks.js +176 -0
  170. package/dist/install/wizard-plan.js +24 -0
  171. package/dist/mcp/registry-manifest.json +3 -3
  172. package/dist/mcp/server.json +2 -2
  173. package/dist/mcp/server.mjs +31965 -27154
  174. package/dist/scripts/_lib/agent_settings.js +11 -0
  175. package/dist/scripts/_lib/claude_settings_hooks.js +10 -8
  176. package/dist/scripts/_lib/installed_layer.js +378 -0
  177. package/dist/scripts/_lib/lean_projection_mode.js +218 -15
  178. package/dist/scripts/_lib/rule_consequence_class.js +45 -0
  179. package/dist/scripts/_lib/rule_injection.js +67 -10
  180. package/dist/scripts/_lib/rule_law_section.js +122 -0
  181. package/dist/scripts/_lib/thin_rules.js +458 -0
  182. package/dist/scripts/_lib/windsurf_render.js +60 -0
  183. package/dist/scripts/condense.js +14 -26
  184. package/dist/scripts/project_thin_rules.js +14 -294
  185. package/dist/server/schemas/settings.js +6 -0
  186. package/dist/shared/skillRanking.js +74 -13
  187. package/docs/contracts/settings-classes.md +7 -5
  188. package/package.json +1 -1
  189. package/src/agent-src/scripts/archive_completed_roadmaps.ts +115 -20
  190. package/src/agent-src/scripts/deferral_carry.ts +339 -0
  191. package/src/agent-src/scripts/update_roadmap_progress.ts +4 -1
  192. package/src/agent-src/templates/scripts/work_engine/directives/ui/apply.ts +48 -6
  193. package/src/agent-src/templates/scripts/work_engine/stack/runner.ts +774 -109
  194. package/src/config/agent-settings.template.yml +62 -22
  195. package/src/config/continuity-surface.json +7 -0
  196. package/src/config/evaluator-budgets.json +4 -4
  197. package/src/config/gate-coverage.yml +96 -3
  198. package/src/config/gate-violation-baselines.json +17 -9
  199. package/src/config/hook-bundle-budget.json +25 -0
  200. package/src/config/hook-token-budget.json +6 -1
  201. package/src/config/host-instruction-limits.json +33 -0
  202. package/src/config/routing-coverage-seed.json +24 -3
  203. package/src/config/rule-activation-census.json +7 -3
  204. package/src/config/rule-consequence-class.json +161 -0
  205. package/src/config/rule-law-ceilings.json +99 -0
  206. package/src/install/claudePathsPlan.ts +46 -4
  207. package/src/install/claudeRuleRewrite.ts +10 -1
  208. package/src/install/emit_host_rules_cli.ts +43 -34
  209. package/src/install/globalRuleScope.ts +109 -0
  210. package/src/install/host_rule_ownership.ts +170 -0
  211. package/src/install/installThinLayer.ts +312 -0
  212. package/src/install/installedRuleLinks.ts +220 -0
  213. package/src/install/wizard-plan.ts +24 -0
  214. package/src/scripts/_cli/cmd_doctor.ts +23 -71
  215. package/src/scripts/_cli/cmd_doctor_neighbours.ts +293 -0
  216. package/src/scripts/_dispatch.bash +16 -2
  217. package/src/scripts/_lib/agent_settings.ts +11 -0
  218. package/src/scripts/_lib/claude_settings_hooks.ts +10 -8
  219. package/src/scripts/_lib/estate_offsets.ts +241 -0
  220. package/src/scripts/_lib/exemption_shape.ts +143 -0
  221. package/src/scripts/_lib/fnv.ts +23 -0
  222. package/src/scripts/_lib/foreign_scan.ts +106 -0
  223. package/src/scripts/_lib/gitignore_block.ts +314 -0
  224. package/src/scripts/_lib/global_deploy_inventory.ts +32 -0
  225. package/src/scripts/_lib/graph_feeder_record.ts +214 -0
  226. package/src/scripts/_lib/hook_settings.ts +73 -0
  227. package/src/scripts/_lib/host_capability.ts +40 -19
  228. package/src/scripts/_lib/host_hook_merge.ts +165 -0
  229. package/src/scripts/_lib/installed_layer.ts +538 -0
  230. package/src/scripts/_lib/json_merge.ts +82 -0
  231. package/src/scripts/_lib/json_pointers.ts +93 -11
  232. package/src/scripts/_lib/lean_projection_mode.ts +275 -15
  233. package/src/scripts/_lib/machine_wake.ts +16 -3
  234. package/src/scripts/_lib/neighbour_census.ts +760 -0
  235. package/src/scripts/_lib/neighbour_scan.ts +122 -0
  236. package/src/scripts/_lib/neighbour_tool_use.ts +189 -0
  237. package/src/scripts/_lib/prompt_shape.ts +46 -0
  238. package/src/scripts/_lib/release_migration_gate.ts +19 -3
  239. package/src/scripts/_lib/reserved_name_sweep.ts +73 -0
  240. package/src/scripts/_lib/review_coverage.ts +134 -0
  241. package/src/scripts/_lib/rule_consequence_class.ts +71 -0
  242. package/src/scripts/_lib/rule_injection.ts +130 -10
  243. package/src/scripts/_lib/rule_law_section.ts +147 -0
  244. package/src/scripts/_lib/rule_layer_overlap.ts +58 -0
  245. package/src/scripts/_lib/skill_origin.ts +254 -0
  246. package/src/scripts/_lib/structured_ask.ts +64 -21
  247. package/src/scripts/_lib/thin_rules.ts +540 -0
  248. package/src/scripts/_lib/touched_file_quality.ts +434 -0
  249. package/src/scripts/_lib/turn_end_refusals.ts +77 -2
  250. package/src/scripts/_lib/windsurf_render.ts +63 -0
  251. package/src/scripts/ai_council/cli_call_budget.ts +13 -5
  252. package/src/scripts/ai_council/clients.ts +26 -57
  253. package/src/scripts/ai_council/config.ts +1 -1
  254. package/src/scripts/ai_council/vendor_defaults.ts +45 -0
  255. package/src/scripts/audit_skill_overlap.ts +261 -0
  256. package/src/scripts/before_complete_hook.ts +243 -12
  257. package/src/scripts/bench_touched_file_quality.ts +238 -0
  258. package/src/scripts/check_enforcement_matrix.ts +377 -20
  259. package/src/scripts/check_estate_count.ts +130 -153
  260. package/src/scripts/check_finding_dispositions.ts +116 -4
  261. package/src/scripts/check_hook_bundle_composition.ts +370 -0
  262. package/src/scripts/check_kernel_edit_ratified.ts +49 -11
  263. package/src/scripts/check_memory.ts +7 -1
  264. package/src/scripts/check_release_highlights.ts +10 -0
  265. package/src/scripts/check_release_pr_shape.ts +38 -15
  266. package/src/scripts/check_standing_rule_delivery.ts +33 -13
  267. package/src/scripts/check_verify_expectation_delta.ts +521 -0
  268. package/src/scripts/code_graph/detect.ts +127 -7
  269. package/src/scripts/code_graph/foreign.ts +194 -0
  270. package/src/scripts/code_graph/query.ts +217 -4
  271. package/src/scripts/code_graph/types.ts +26 -2
  272. package/src/scripts/code_graph/validate.ts +3 -1
  273. package/src/scripts/code_graph/verbs.ts +103 -0
  274. package/src/scripts/compile_hook_manifest.ts +33 -1
  275. package/src/scripts/condense.ts +14 -27
  276. package/src/scripts/estate_exemption_shape.ts +202 -0
  277. package/src/scripts/generate_host_cost_table.ts +1 -1
  278. package/src/scripts/hook_manifest.json +1 -1
  279. package/src/scripts/hook_manifest.yaml +150 -9
  280. package/src/scripts/hooks/block_config_weakening.ts +67 -2
  281. package/src/scripts/hooks/block_plumbing_writes.ts +29 -1
  282. package/src/scripts/hooks/code_graph_context_hook.ts +252 -23
  283. package/src/scripts/hooks/concern_failure_policy.ts +280 -0
  284. package/src/scripts/hooks/dispatch_hook.ts +52 -52
  285. package/src/scripts/hooks/exit_codes.ts +2 -1
  286. package/src/scripts/hooks/host_lowering.json +1 -1
  287. package/src/scripts/hooks/host_lowering.ts +19 -1
  288. package/src/scripts/hooks/one_question_per_ask_hook.ts +41 -25
  289. package/src/scripts/hooks/payload_stub.ts +10 -1
  290. package/src/scripts/hooks/rule_inject_hook.ts +757 -63
  291. package/src/scripts/hooks/table_fingerprint.ts +21 -0
  292. package/src/scripts/hooks/telemetry_usage_hook.ts +64 -8
  293. package/src/scripts/hooks/turn_end_gate_hook.ts +84 -0
  294. package/src/scripts/hooks_status.ts +112 -0
  295. package/src/scripts/injection_scan_hook.ts +7 -0
  296. package/src/scripts/install.ts +72 -185
  297. package/src/scripts/installed_layer_report.ts +103 -0
  298. package/src/scripts/language_mirror_hook.ts +15 -6
  299. package/src/scripts/learning_sidecar.ts +119 -8
  300. package/src/scripts/lint_hook_manifest.ts +53 -0
  301. package/src/scripts/lint_major_migration_sections.ts +156 -26
  302. package/src/scripts/lint_pack_boundaries.ts +63 -5
  303. package/src/scripts/lint_rule_law_section.ts +517 -0
  304. package/src/scripts/lint_workflow_security.ts +161 -5
  305. package/src/scripts/mcp_server/consumer_tool_catalog.json +53 -0
  306. package/src/scripts/mcp_server/graph_tools.ts +72 -3
  307. package/src/scripts/measure_routing_signal.ts +54 -16
  308. package/src/scripts/measure_skill_ranker_baseline.ts +216 -8
  309. package/src/scripts/memory_learn_hook.ts +91 -6
  310. package/src/scripts/model_rule_injection.ts +52 -5
  311. package/src/scripts/project_thin_rules.ts +34 -327
  312. package/src/scripts/report_evidence_temperature.ts +657 -0
  313. package/src/scripts/report_installed_rule_links.ts +157 -0
  314. package/src/scripts/report_label_agreement.ts +233 -0
  315. package/src/scripts/report_obligation_mechanism.ts +230 -0
  316. package/src/scripts/report_single_token_triggers.ts +277 -0
  317. package/src/scripts/report_skill_ranker_confusion.ts +442 -0
  318. package/src/scripts/report_standing_payload_by_host.ts +188 -15
  319. package/src/scripts/report_verification_record_coverage.ts +306 -0
  320. package/src/scripts/rule_activation_census.ts +5 -2
  321. package/src/scripts/rule_trigger_eval.ts +9 -9
  322. package/src/scripts/schemas/review-findings.schema.json +3 -2
  323. package/src/scripts/schemas/rule.schema.json +5 -0
  324. package/src/scripts/self_review_gate.ts +62 -7
  325. package/src/scripts/skill_tools/score_skill_relevance.ts +290 -25
  326. package/src/scripts/sweep_skill_ranker_signals.ts +129 -0
  327. package/src/scripts/sync_gitignore.ts +29 -235
  328. package/src/scripts/trigger_eval_grandfather.json +0 -5
  329. package/src/server/schemas/settings.ts +10 -0
  330. package/src/shared/skillRanking.ts +117 -11
  331. package/dist/cli-delegate/chunk-JNMVLWLR.js +0 -4457
  332. package/dist/cli-delegate/chunk-YDPILCOL.js +0 -152
@@ -0,0 +1,157 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Which links inside an installed rule file do NOT resolve, per host.
4
+ *
5
+ * A rule body is authored against the package tree, where every sibling
6
+ * directory is present. The installer copies a subset
7
+ * (`GLOBAL_DEPLOY_SOURCES`), and nothing joined the two, so a rule could ship
8
+ * a link into a directory no consumer has.
9
+ *
10
+ * This reporter reads the real deploy plan and the real rule bodies, so its
11
+ * numbers are the installer's rather than a restatement of it. It also prices
12
+ * the two repairs step 1.2 of `road-to-rule-triggers-and-links-that-hold`
13
+ * weighs against each other:
14
+ *
15
+ * · **deploy** — add the directory to the host's plan. Nothing is written
16
+ * into a rule body, so the standing text a host loads as instructions does
17
+ * not move; the cost is files on disk.
18
+ * · **rewrite** — leave the plan alone and make each link an absolute
19
+ * package path. Every character lands INSIDE a rule body, which is exactly
20
+ * the text the host loads.
21
+ *
22
+ * The rewrite price depends on the installed package root's own length, which
23
+ * is a property of the machine, so `--prefix-chars` is explicit and the report
24
+ * states the value it used.
25
+ */
26
+ import * as fs from 'node:fs';
27
+ import * as path from 'node:path';
28
+ import { fileURLToPath, pathToFileURL } from 'node:url';
29
+
30
+ import {
31
+ type LinkAuditReport,
32
+ auditInstalledRuleLinks,
33
+ deployPlanFrom,
34
+ rewriteOptionCost,
35
+ } from '../install/installedRuleLinks.js';
36
+ import { GLOBAL_DEPLOY_SOURCES } from './install.js';
37
+
38
+ // ledger-exempt: a REPORTER, not a gate. It has no verdict for a per-target
39
+ // ledger to account for; it prints what the deploy plan and the rule bodies
40
+ // already say.
41
+
42
+ const _HERE = fileURLToPath(import.meta.url);
43
+
44
+ /** Hosts whose plan carries a rules directory — the only ones with an answer. */
45
+ function ruleHosts(): [string, ReadonlyArray<readonly [string, string]>][] {
46
+ return Object.entries(GLOBAL_DEPLOY_SOURCES).filter(([, pairs]) =>
47
+ pairs.some(([source]) => source.endsWith('/rules')),
48
+ );
49
+ }
50
+
51
+ export interface HostLinkReading {
52
+ host: string;
53
+ links: number;
54
+ resolved: number;
55
+ unresolved: number;
56
+ rewrite_cost_chars: number;
57
+ by_directory: LinkAuditReport['by_directory'];
58
+ }
59
+
60
+ export function readings(packageRoot: string, prefixChars: number): HostLinkReading[] {
61
+ const out: HostLinkReading[] = [];
62
+ for (const [host, pairs] of ruleHosts()) {
63
+ const report = auditInstalledRuleLinks(deployPlanFrom(pairs), packageRoot);
64
+ out.push({
65
+ host,
66
+ links: report.audits.length,
67
+ resolved: report.counts.resolved,
68
+ unresolved: report.audits.length - report.counts.resolved,
69
+ rewrite_cost_chars: rewriteOptionCost(report, prefixChars),
70
+ by_directory: report.by_directory,
71
+ });
72
+ }
73
+ return out;
74
+ }
75
+
76
+ function flagValue(flag: string): string | null {
77
+ const argv = process.argv.slice(2);
78
+ const i = argv.indexOf(flag);
79
+ return i >= 0 && i + 1 < argv.length ? (argv[i + 1] as string) : null;
80
+ }
81
+
82
+ const USAGE =
83
+ 'usage: report_installed_rule_links [--root DIR] [--prefix-chars N] [--json]\n' +
84
+ ' --root package checkout the installer copies FROM (default: cwd)\n' +
85
+ ' --prefix-chars length of the installed package root, for the rewrite price\n' +
86
+ ' (default 48, a typical `~/.claude/agent-config` expansion)\n' +
87
+ ' --json machine-readable readings\n';
88
+
89
+ export function main(): number {
90
+ const argv = process.argv.slice(2);
91
+ if (argv.includes('--help') || argv.includes('-h')) {
92
+ process.stdout.write(USAGE);
93
+ return 0;
94
+ }
95
+ for (const a of argv) {
96
+ if (a.startsWith('-') && !['--root', '--prefix-chars', '--json'].includes(a)) {
97
+ process.stderr.write(`unknown argument: ${a}\n${USAGE}`);
98
+ return 2;
99
+ }
100
+ }
101
+
102
+ const root = flagValue('--root') ?? process.cwd();
103
+ const rawPrefix = flagValue('--prefix-chars');
104
+ const prefixChars = rawPrefix === null ? 48 : Number.parseInt(rawPrefix, 10);
105
+ if (!Number.isInteger(prefixChars) || prefixChars <= 0) {
106
+ process.stderr.write(`--prefix-chars must be a positive integer\n`);
107
+ return 2;
108
+ }
109
+ if (!fs.existsSync(path.join(root, 'dist', 'agent-src', 'rules'))) {
110
+ process.stderr.write(`no dist/agent-src/rules under ${root} — nothing to read\n`);
111
+ return 1;
112
+ }
113
+
114
+ const rows = readings(root, prefixChars);
115
+
116
+ if (argv.includes('--json')) {
117
+ process.stdout.write(`${JSON.stringify({ prefix_chars: prefixChars, hosts: rows }, null, 2)}\n`);
118
+ return 0;
119
+ }
120
+
121
+ process.stdout.write(`installed rule links, per host (rewrite priced at ${String(prefixChars)} prefix chars)\n\n`);
122
+ for (const r of rows) {
123
+ process.stdout.write(
124
+ ` ${r.host}: ${String(r.links)} link(s), ${String(r.resolved)} resolved, ${String(r.unresolved)} unresolved\n`,
125
+ );
126
+ if (r.unresolved > 0) {
127
+ // Priced over EVERY unresolved link, including the ones a rewrite
128
+ // to an absolute package path cannot reach either (a target the
129
+ // projection does not ship). Comparing it against a deploy option
130
+ // that repairs only a subset overstates the rewrite; the caveat is
131
+ // printed rather than left for the reader to rediscover.
132
+ process.stdout.write(
133
+ ` rewrite option: +${String(r.rewrite_cost_chars)} standing characters ` +
134
+ `(all ${String(r.unresolved)} unresolved; a like-for-like figure prices only ` +
135
+ `the subset the option being compared against repairs)\n`,
136
+ );
137
+ for (const d of r.by_directory) {
138
+ process.stdout.write(` ${String(d.count).padStart(4)} ${d.directory} (${d.verdict})\n`);
139
+ }
140
+ }
141
+ }
142
+ return 0;
143
+ }
144
+
145
+ function _isCliEntry(): boolean {
146
+ if (process.argv[1] === undefined) return false;
147
+ if (import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) return true;
148
+ try {
149
+ return fs.realpathSync(_HERE) === fs.realpathSync(path.resolve(process.argv[1]));
150
+ } catch {
151
+ return false;
152
+ }
153
+ }
154
+
155
+ if (_isCliEntry()) {
156
+ process.exit(main());
157
+ }
@@ -0,0 +1,233 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * report_label_agreement — ranker error, separated from label noise.
4
+ *
5
+ * `road-to-a-ranker-that-routes` 1.4. A top-1 of 0.21 is a statement about two
6
+ * things at once: how often the ranker is wrong, and how often "wrong" is one
7
+ * reasonable person disagreeing with another about which skill a prompt wants.
8
+ * The second is bounded by how far two independent labelling seats agree, and
9
+ * that bound was published as a single aggregate (39 / 55 exact-set agreement)
10
+ * with no per-row record — so nobody could ask the only question that matters
11
+ * here: what does the ranker score on the rows the seats actually AGREE on?
12
+ *
13
+ * This script answers it, and it needs a second seat's labels per row to do so.
14
+ * The store it reads is {@link SECOND_SEAT_FILE}: a flat `{ case id: [skill
15
+ * ids] }` map written by seats that had not read this ranker's scoring, under
16
+ * the protocol in `tests/eval/routing-matrix/README.md`. The first seat's labels
17
+ * are the `expected_skills` lists in the matrix itself.
18
+ *
19
+ * THE DISAGREEMENTS ARE NOT RECONCILED, and that is the point. Overwriting one
20
+ * seat with the other would replace the honest width of the ground truth with a
21
+ * narrower number that looks better and means less.
22
+ *
23
+ * THREE AGREEMENT DEFINITIONS, reported side by side because they answer
24
+ * different questions and the gap between them IS the finding:
25
+ *
26
+ * - `exact` — the two label sets are identical. The strictest, and the one
27
+ * the published 70.9 % figure used.
28
+ * - `overlap` — the two sets share at least one skill, or are both empty. A
29
+ * hit is scored when ANY labelled skill appears in the top-k,
30
+ * so this is the definition the MEASUREMENT actually cares
31
+ * about: on an overlap row the two seats would score the same
32
+ * top-1 answer the same way.
33
+ * - `polarity` — the two seats agree only on whether a skill answer exists at
34
+ * all. The weakest, and the floor under the other two.
35
+ *
36
+ * **`polarity` IS ONE-SIDED ON THIS DENOMINATOR and must not be compared with a
37
+ * two-sided figure.** The first seat's labels arrive through
38
+ * `readMatrixLabelledPrompts`, which keeps only NON-EMPTY lists, so "seat one
39
+ * thinks a skill answer exists" is true for every row here by construction and
40
+ * the `agrees` both-empty branch is unreachable from this caller. The number
41
+ * therefore reads "the second seat also wrote a non-empty label", not "the two
42
+ * seats agree about whether an answer exists". It is still worth reporting — it
43
+ * counts the rows the second seat declined — and it is NOT the published
44
+ * two-sided agreement figure wearing the same name.
45
+ *
46
+ * `second_seat_rows` and `relabelled_and_labelled` differ by exactly the rows
47
+ * the second seat relabelled that the first seat had left empty: those are
48
+ * absent from `first` and so cannot be paired.
49
+ *
50
+ * Usage:
51
+ * ./scripts-run src/scripts/report_label_agreement
52
+ * ./scripts-run src/scripts/report_label_agreement --ranker idf
53
+ *
54
+ * Output is JSON on stdout, always — there is no second format and therefore no
55
+ * flag to pick one.
56
+ */
57
+
58
+ import * as fs from 'node:fs';
59
+ import * as path from 'node:path';
60
+ import { fileURLToPath } from 'node:url';
61
+
62
+ import { wilsonInterval } from './_lib/capture_rate.js';
63
+ import {
64
+ REPO,
65
+ SKILLS_DIR,
66
+ type LabelledPrompt,
67
+ parseRanker,
68
+ rankOptionsFor,
69
+ readMatrixLabelledPrompts,
70
+ sliceForId,
71
+ } from './measure_skill_ranker_baseline.js';
72
+ import { rank } from './skill_tools/score_skill_relevance.js';
73
+
74
+ /** Where the second seat's per-row labels live, relative to the repo root. */
75
+ export const SECOND_SEAT_FILE = path.join('tests', 'eval', 'routing-matrix', 'second-seat-2026-10-01.json');
76
+
77
+ export type AgreementKind = 'exact' | 'overlap' | 'polarity';
78
+ export const AGREEMENT_KINDS: readonly AgreementKind[] = ['exact', 'overlap', 'polarity'];
79
+
80
+ /**
81
+ * The store, or `{}` when nobody has relabelled.
82
+ *
83
+ * A MALFORMED store is a third state and must not look like either of the other
84
+ * two: an empty object would read as "nobody has relabelled" and a raw
85
+ * `SyntaxError` out of `main` would bypass the message that distinguishes them.
86
+ */
87
+ export function readSecondSeat(repo = REPO): Record<string, string[]> {
88
+ const file = path.join(repo, SECOND_SEAT_FILE);
89
+ if (!fs.existsSync(file)) return {};
90
+ const raw = fs.readFileSync(file, 'utf8');
91
+ try {
92
+ return JSON.parse(raw) as Record<string, string[]>;
93
+ } catch (err) {
94
+ throw new Error(
95
+ `report_label_agreement: ${SECOND_SEAT_FILE} is not valid JSON (${(err as Error).message}). ` +
96
+ 'That is a corrupt store, NOT an empty one — the two must not be confused.',
97
+ );
98
+ }
99
+ }
100
+
101
+ export function agrees(a: readonly string[], b: readonly string[], kind: AgreementKind): boolean {
102
+ if (kind === 'polarity') return (a.length > 0) === (b.length > 0);
103
+ if (kind === 'overlap') {
104
+ if (a.length === 0 || b.length === 0) return a.length === b.length;
105
+ return a.some((s) => b.includes(s));
106
+ }
107
+ if (a.length !== b.length) return false;
108
+ const sa = [...a].sort();
109
+ const sb = [...b].sort();
110
+ return sa.every((s, i) => s === sb[i]);
111
+ }
112
+
113
+ export interface AgreementArm {
114
+ kind: AgreementKind | 'all-relabelled';
115
+ n: number;
116
+ top1: number;
117
+ top1_ci95: { lower: number; upper: number };
118
+ top3: number;
119
+ top3_ci95: { lower: number; upper: number };
120
+ }
121
+
122
+ function round3(n: number): number {
123
+ return Math.round(n * 1000) / 1000;
124
+ }
125
+
126
+ function arm(
127
+ kind: AgreementArm['kind'],
128
+ rows: readonly { prompt: LabelledPrompt; ranked: string[] }[],
129
+ ): AgreementArm {
130
+ let t1 = 0;
131
+ let t3 = 0;
132
+ for (const r of rows) {
133
+ if (r.prompt.expected.some((e) => r.ranked.slice(0, 1).includes(e))) t1 += 1;
134
+ if (r.prompt.expected.some((e) => r.ranked.slice(0, 3).includes(e))) t3 += 1;
135
+ }
136
+ const n = rows.length;
137
+ const ci = (s: number): { lower: number; upper: number } => {
138
+ const raw = wilsonInterval(s, n);
139
+ return { lower: round3(raw.lower), upper: round3(raw.upper) };
140
+ };
141
+ return {
142
+ kind,
143
+ n,
144
+ top1: n ? round3(t1 / n) : 0,
145
+ top1_ci95: ci(t1),
146
+ top3: n ? round3(t3 / n) : 0,
147
+ top3_ci95: ci(t3),
148
+ };
149
+ }
150
+
151
+ export interface AgreementReport {
152
+ ranker: string;
153
+ second_seat_rows: number;
154
+ relabelled_and_labelled: number;
155
+ agreement: Record<AgreementKind, { agreed: number; share: number }>;
156
+ arms: AgreementArm[];
157
+ /** Rows where the two seats share no skill at all — the honest width. */
158
+ disjoint: { id: string; first: string[]; second: string[] }[];
159
+ }
160
+
161
+ export function measureAgreement(opts: {
162
+ repo?: string;
163
+ skillsDir?: string;
164
+ ranker: string;
165
+ }): AgreementReport {
166
+ const repo = opts.repo ?? REPO;
167
+ const skillsDir = opts.skillsDir ?? SKILLS_DIR;
168
+ const second = readSecondSeat(repo);
169
+ const first = readMatrixLabelledPrompts(repo);
170
+ const rankOpts = rankOptionsFor(opts.ranker);
171
+
172
+ const paired = first.filter((p) => second[p.id] !== undefined);
173
+ const ranked = new Map<string, string[]>();
174
+ for (const p of paired) ranked.set(p.id, rank(p.prompt, skillsDir, rankOpts).map((r) => r[0]));
175
+
176
+ const rows = paired.map((prompt) => ({ prompt, ranked: ranked.get(prompt.id) ?? [] }));
177
+ const agreement = {} as Record<AgreementKind, { agreed: number; share: number }>;
178
+ const arms: AgreementArm[] = [arm('all-relabelled', rows)];
179
+ for (const kind of AGREEMENT_KINDS) {
180
+ const kept = rows.filter((r) => agrees(r.prompt.expected, second[r.prompt.id] ?? [], kind));
181
+ agreement[kind] = {
182
+ agreed: kept.length,
183
+ share: rows.length ? round3(kept.length / rows.length) : 0,
184
+ };
185
+ arms.push(arm(kind, kept));
186
+ }
187
+ const disjoint = paired
188
+ .filter((p) => !agrees(p.expected, second[p.id] ?? [], 'overlap'))
189
+ .map((p) => ({ id: p.id, first: p.expected, second: second[p.id] ?? [] }));
190
+
191
+ return {
192
+ ranker: opts.ranker,
193
+ second_seat_rows: Object.keys(second).length,
194
+ relabelled_and_labelled: paired.length,
195
+ agreement,
196
+ arms,
197
+ disjoint,
198
+ };
199
+ }
200
+
201
+ /** Every relabelled row is expected to sit in the tuning slice; this says so. */
202
+ export function relabelledSliceCensus(repo = REPO): Record<string, number> {
203
+ const out: Record<string, number> = { tuning: 0, sealed: 0 };
204
+ for (const id of Object.keys(readSecondSeat(repo))) out[sliceForId(id)] = (out[sliceForId(id)] ?? 0) + 1;
205
+ return out;
206
+ }
207
+
208
+ export function main(argv: readonly string[]): number {
209
+ let report: AgreementReport;
210
+ try {
211
+ // Same check as `--slice` elsewhere: a present flag with no value is an
212
+ // error, never a silent substitution of the baseline.
213
+ report = measureAgreement({ ranker: parseRanker(argv) });
214
+ } catch (err) {
215
+ process.stderr.write(`${(err as Error).message}\n`);
216
+ return 2;
217
+ }
218
+ if (report.second_seat_rows === 0) {
219
+ process.stderr.write(
220
+ `report_label_agreement: no second-seat labels at ${SECOND_SEAT_FILE} — ` +
221
+ 'this is "nobody has relabelled", not "the seats disagree about nothing".\n',
222
+ );
223
+ return 2;
224
+ }
225
+ process.stdout.write(
226
+ `${JSON.stringify({ ...report, relabelled_slice_census: relabelledSliceCensus() }, null, 2)}\n`,
227
+ );
228
+ return 0;
229
+ }
230
+
231
+ if (process.argv[1] && fileURLToPath(import.meta.url) === path.resolve(process.argv[1])) {
232
+ process.exit(main(process.argv.slice(2)));
233
+ }
@@ -0,0 +1,230 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Per rule: is the obligation already carried by a mechanism, and is there a
4
+ * slot that could carry it?
5
+ *
6
+ * Step 1.5 of `road-to-rule-triggers-and-links-that-hold`. It joins three
7
+ * instruments that already exist and had never been read together:
8
+ *
9
+ * · `check_enforcement_coverage --json` — the only place `enforced_by` and
10
+ * `obligation_frequency` are already resolved per rule, including what the
11
+ * declaration resolves TO (a wired validator, a bound hook, nothing).
12
+ * · `report_obligation_carriers` — how many artifacts restate the same
13
+ * obligation, which is a different question from whether one enforces it.
14
+ * · The `# obligation: line N` frontmatter marker — 107 of 121 rules carry
15
+ * one and nothing in the tree reads it, so this is its first reader.
16
+ *
17
+ * **Two columns, and conflating them would be the error worth avoiding.**
18
+ * `carried` is measured: a gate either refuses or it does not.
19
+ * `slot_exists` is NECESSARY, NEVER SUFFICIENT — it says a host event fires at
20
+ * the obligation's own frequency, so a carrier could at least be bound. It
21
+ * does not say the obligation is decidable from what that event sees, which is
22
+ * the actual question and is not answerable by a join. A rule whose body says
23
+ * `instruction-only` has usually already answered it in the other direction,
24
+ * and that answer is reproduced rather than overridden.
25
+ *
26
+ * Report only. Moving an obligation out of prose is a per-rule change with its
27
+ * own review, which this roadmap's scope section says in as many words.
28
+ */
29
+ import { execFileSync } from 'node:child_process';
30
+ import * as fs from 'node:fs';
31
+ import * as path from 'node:path';
32
+ import { fileURLToPath, pathToFileURL } from 'node:url';
33
+
34
+ // ledger-exempt: a REPORTER, not a gate. Its only non-zero exit says an input
35
+ // instrument could not be run, which is a statement about the walk.
36
+
37
+ const _HERE = fileURLToPath(import.meta.url);
38
+
39
+ /** Frequencies a host event is known to fire at — the `slot_exists` test. */
40
+ const EVENT_BOUND_FREQUENCIES: ReadonlySet<string> = new Set([
41
+ 'per-edit',
42
+ 'per-file-write',
43
+ 'per-commit',
44
+ 'per-turn',
45
+ 'per-session',
46
+ 'per-event',
47
+ 'per-task',
48
+ ]);
49
+
50
+ /** Resolutions where something can actually refuse. */
51
+ const REFUSING: ReadonlySet<string> = new Set(['validator', 'validator-local', 'test', 'hook']);
52
+
53
+ export interface MechanismRow {
54
+ rule: string;
55
+ tier: string;
56
+ marker_line: number | null;
57
+ frequency: string;
58
+ declared: number;
59
+ effective: string;
60
+ /** `gate` · `observer` · `declared-gap` · `undeclared`. */
61
+ carried: string;
62
+ /** A host event is known to fire at this frequency. Necessary, not sufficient. */
63
+ slot_exists: boolean;
64
+ /** Artifacts restating this rule's obligations, per the carrier census. */
65
+ restatements: number;
66
+ }
67
+
68
+ /** `# obligation: line N` inside the frontmatter block. */
69
+ export function markerLine(content: string): number | null {
70
+ if (!content.startsWith('---')) return null;
71
+ const end = content.indexOf('\n---', 3);
72
+ const block = end === -1 ? content : content.slice(0, end);
73
+ const m = /^#\s*obligation:\s*line\s+(\d+)\s*$/m.exec(block);
74
+ return m ? Number.parseInt(m[1] as string, 10) : null;
75
+ }
76
+
77
+ function run(root: string, script: string, args: readonly string[]): string {
78
+ return execFileSync(path.join(root, 'scripts-run'), [script, ...args], {
79
+ cwd: root,
80
+ encoding: 'utf8',
81
+ maxBuffer: 64 * 1024 * 1024,
82
+ stdio: ['ignore', 'pipe', 'ignore'],
83
+ });
84
+ }
85
+
86
+ interface CoverageRule {
87
+ id: string;
88
+ tier?: string;
89
+ declared?: string[];
90
+ effective?: string;
91
+ obligation_frequency?: string | null;
92
+ }
93
+
94
+ export function rows(root: string): MechanismRow[] {
95
+ const coverage = JSON.parse(run(root, 'src/scripts/check_enforcement_coverage', ['--json'])) as {
96
+ rules: CoverageRule[];
97
+ };
98
+
99
+ run(root, 'src/scripts/report_obligation_carriers', ['--top', '1']);
100
+ const carriersPath = path.join(root, 'agents', 'runtime', 'reports', 'obligation-carriers.json');
101
+ const restatements = new Map<string, number>();
102
+ if (!fs.existsSync(carriersPath)) {
103
+ // Not a degradation to absorb: `agents/runtime/` is gitignored, so a
104
+ // fresh checkout reaches this, and a silent fallback would publish a
105
+ // uniformly-zero Restatements column with nothing saying it is empty.
106
+ // Every other input failure in this script exits 1; so does this one.
107
+ throw new Error(
108
+ `report_obligation_carriers wrote no census at ${carriersPath} — the ` +
109
+ 'Restatements column would read 0 for every rule without saying so',
110
+ );
111
+ }
112
+ {
113
+ const census = JSON.parse(fs.readFileSync(carriersPath, 'utf8')) as {
114
+ rows: { carriers: { path: string; cls: string }[] }[];
115
+ };
116
+ for (const r of census.rows) {
117
+ // A row is one obligation; it counts for every RULE that carries it.
118
+ for (const c of r.carriers) {
119
+ if (c.cls !== 'rule') continue;
120
+ const id = path.basename(c.path, '.md');
121
+ restatements.set(id, (restatements.get(id) ?? 0) + r.carriers.length - 1);
122
+ }
123
+ }
124
+ }
125
+
126
+ const out: MechanismRow[] = [];
127
+ for (const r of coverage.rules) {
128
+ const file = path.join(root, 'src', 'rules', `${r.id}.md`);
129
+ const content = fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : '';
130
+ const declared = r.declared ?? [];
131
+ const effective = r.effective ?? 'none';
132
+ const frequency = r.obligation_frequency ?? '—';
133
+
134
+ let carried: string;
135
+ if (REFUSING.has(effective)) carried = 'gate';
136
+ else if (effective === 'observer') carried = 'observer';
137
+ else if (declared.length > 0) carried = 'declared-gap';
138
+ else carried = 'undeclared';
139
+
140
+ out.push({
141
+ rule: r.id,
142
+ tier: r.tier ?? '—',
143
+ marker_line: markerLine(content),
144
+ frequency,
145
+ declared: declared.length,
146
+ effective,
147
+ carried,
148
+ slot_exists: EVENT_BOUND_FREQUENCIES.has(frequency),
149
+ restatements: restatements.get(r.id) ?? 0,
150
+ });
151
+ }
152
+ out.sort((a, b) => a.rule.localeCompare(b.rule));
153
+ return out;
154
+ }
155
+
156
+ export function renderTable(data: readonly MechanismRow[]): string[] {
157
+ const L: string[] = [];
158
+ L.push('| Rule | Tier | Marker | Frequency | `enforced_by` | Resolves to | Carried | Slot exists | Restatements |');
159
+ L.push('|---|---|---:|---|---:|---|---|---|---:|');
160
+ for (const r of data) {
161
+ L.push(
162
+ `| \`${r.rule}\` | ${r.tier} | ${r.marker_line === null ? '—' : String(r.marker_line)} | ` +
163
+ `${r.frequency} | ${String(r.declared)} | \`${r.effective}\` | ${r.carried} | ` +
164
+ `${r.slot_exists ? 'yes' : 'no'} | ${String(r.restatements)} |`,
165
+ );
166
+ }
167
+ return L;
168
+ }
169
+
170
+ const USAGE = 'usage: report_obligation_mechanism [--root DIR] [--json | --table]\n';
171
+
172
+ export function main(): number {
173
+ const argv = process.argv.slice(2);
174
+ if (argv.includes('--help') || argv.includes('-h')) {
175
+ process.stdout.write(USAGE);
176
+ return 0;
177
+ }
178
+ for (const a of argv) {
179
+ if (a.startsWith('-') && !['--root', '--json', '--table'].includes(a)) {
180
+ process.stderr.write(`unknown argument: ${a}\n${USAGE}`);
181
+ return 2;
182
+ }
183
+ }
184
+ const i = argv.indexOf('--root');
185
+ const root = i >= 0 && i + 1 < argv.length ? (argv[i + 1] as string) : process.cwd();
186
+
187
+ let data: MechanismRow[];
188
+ try {
189
+ data = rows(root);
190
+ } catch (e) {
191
+ process.stderr.write(`could not read an input instrument: ${String(e)}\n`);
192
+ return 1;
193
+ }
194
+
195
+ if (argv.includes('--json')) {
196
+ process.stdout.write(`${JSON.stringify(data, null, 2)}\n`);
197
+ return 0;
198
+ }
199
+ if (argv.includes('--table')) {
200
+ process.stdout.write(`${renderTable(data).join('\n')}\n`);
201
+ return 0;
202
+ }
203
+
204
+ const by = (k: string): number => data.filter((r) => r.carried === k).length;
205
+ const slotNoGate = data.filter((r) => r.carried !== 'gate' && r.slot_exists).length;
206
+ process.stdout.write(
207
+ `${String(data.length)} rule(s)\n` +
208
+ ` carried by a gate that can refuse: ${String(by('gate'))}\n` +
209
+ ` carried by an observer (fires, never refuses): ${String(by('observer'))}\n` +
210
+ ` declared gap (the rule says instruction-only / none): ${String(by('declared-gap'))}\n` +
211
+ ` no enforcement declaration at all: ${String(by('undeclared'))}\n` +
212
+ ` not gated, but a host event fires at the obligation's frequency: ${String(slotNoGate)}\n` +
213
+ ` carry no \`# obligation: line N\` marker: ${String(data.filter((r) => r.marker_line === null).length)}\n`,
214
+ );
215
+ return 0;
216
+ }
217
+
218
+ function _isCliEntry(): boolean {
219
+ if (process.argv[1] === undefined) return false;
220
+ if (import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) return true;
221
+ try {
222
+ return fs.realpathSync(_HERE) === fs.realpathSync(path.resolve(process.argv[1]));
223
+ } catch {
224
+ return false;
225
+ }
226
+ }
227
+
228
+ if (_isCliEntry()) {
229
+ process.exit(main());
230
+ }