@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
@@ -43,6 +43,7 @@
43
43
  * ./scripts-run src/scripts/measure_skill_ranker_baseline --corpus routing-matrix
44
44
  * ./scripts-run src/scripts/measure_skill_ranker_baseline --corpus all
45
45
  * ./scripts-run src/scripts/measure_skill_ranker_baseline --ranker keyword-v2
46
+ * ./scripts-run src/scripts/measure_skill_ranker_baseline --corpus routing-matrix --slice sealed
46
47
  */
47
48
 
48
49
  import * as fs from 'node:fs';
@@ -50,6 +51,7 @@ import * as path from 'node:path';
50
51
  import { fileURLToPath } from 'node:url';
51
52
 
52
53
  import { wilsonInterval } from './_lib/capture_rate.js';
54
+ import { fnv1a } from './_lib/fnv.js';
53
55
  import { rank } from './skill_tools/score_skill_relevance.js';
54
56
  import type { RankOptions } from '../shared/skillRanking.js';
55
57
 
@@ -204,6 +206,84 @@ export function promptsForCorpus(corpus: CorpusName, repo = REPO): LabelledPromp
204
206
  return [...readLabelledPrompts(repo), ...readMatrixLabelledPrompts(repo)];
205
207
  }
206
208
 
209
+ /**
210
+ * Which half of the corpus a row belongs to.
211
+ *
212
+ * `road-to-a-ranker-that-routes` 1.2. Tuning and reading on one corpus of 390
213
+ * overfits it: a signal chosen because it lifted the number on the rows it was
214
+ * chosen from will lift the number on exactly those rows, and the interval
215
+ * printed beside it then describes the fitting rather than the ranker. Every
216
+ * lift claim in Phases 2 and 3 is read on `sealed` only.
217
+ */
218
+ export type SliceName = 'all' | 'tuning' | 'sealed';
219
+ export const SLICE_NAMES: readonly SliceName[] = ['all', 'tuning', 'sealed'];
220
+
221
+ /**
222
+ * One row in five — nominally 20 %, measured 19.2 % (75 of 390) on the live
223
+ * corpus, because a hash is not a shuffle and the exact count is a property of
224
+ * the ids.
225
+ *
226
+ * Stated as a modulus rather than a fraction because the partition has to be a
227
+ * pure function of the id: a share applied to a shuffled list would move every
228
+ * row the moment a prompt is added, and the sealed slice would stop being the
229
+ * same slice between two runs of the same measurement.
230
+ */
231
+ export const SEALED_MODULUS = 5;
232
+
233
+ /**
234
+ * The slice one case id falls in — deterministic and order-independent.
235
+ *
236
+ * STABILITY UNDER CORPUS CHANGE IS PARTIAL, and an earlier version of this block
237
+ * claimed otherwise. The matrix id is `rule#section[ordinal]` and the ordinal is
238
+ * POSITIONAL — it counts `- prompt:` entries within a section. So:
239
+ *
240
+ * - APPENDING a prompt to the end of a section is stable: every existing row
241
+ * keeps its ordinal, its id, and its side of the seal. This is the workflow
242
+ * the matrix README documents, which is why the partition survives the
243
+ * release-to-release growth the `history` array is for.
244
+ * - INSERTING or DELETING mid-section renumbers every later row in THAT
245
+ * section, which re-hashes them and moves roughly one in five across the
246
+ * seal.
247
+ *
248
+ * A content key (`rule|section|prompt`) would close the second case and is the
249
+ * right shape for the next corpus version. It is deliberately NOT applied here:
250
+ * the signals in Phase 2 were chosen by reading the TUNING rows of this
251
+ * partition, so re-drawing the boundary now would move ~20 % of those rows into
252
+ * the sealed slice and silently contaminate the very claims the seal exists to
253
+ * protect. Changing a partition after choosing on it is worse than a partially
254
+ * stable partition, so the key moves when the corpus does, not before.
255
+ */
256
+ export function sliceForId(id: string): Exclude<SliceName, 'all'> {
257
+ return fnv1a(id) % SEALED_MODULUS === 0 ? 'sealed' : 'tuning';
258
+ }
259
+
260
+ /** The rows of `prompts` that fall in `slice`; `all` is the identity. */
261
+ export function partitionBySlice<T extends { id: string }>(
262
+ prompts: readonly T[],
263
+ slice: SliceName,
264
+ ): T[] {
265
+ if (slice === 'all') return [...prompts];
266
+ return prompts.filter((p) => sliceForId(p.id) === slice);
267
+ }
268
+
269
+ /** How the corpus divides, printed on every run so a reader never assumes it. */
270
+ export interface SliceSizes {
271
+ all: number;
272
+ tuning: number;
273
+ sealed: number;
274
+ sealed_modulus: number;
275
+ }
276
+
277
+ export function sliceSizes(prompts: readonly { id: string }[]): SliceSizes {
278
+ const sealed = prompts.filter((p) => sliceForId(p.id) === 'sealed').length;
279
+ return {
280
+ all: prompts.length,
281
+ tuning: prompts.length - sealed,
282
+ sealed,
283
+ sealed_modulus: SEALED_MODULUS,
284
+ };
285
+ }
286
+
207
287
  /**
208
288
  * The packs a skill declares, read from its own `packs:` frontmatter block.
209
289
  *
@@ -278,6 +358,10 @@ export interface Interval {
278
358
 
279
359
  export interface AccuracyArm {
280
360
  corpus: CorpusName;
361
+ /** Which half of the corpus this arm read. `all` is the whole of it. */
362
+ slice: SliceName;
363
+ /** The division of the FULL corpus, printed whichever slice was read. */
364
+ slice_sizes: SliceSizes;
281
365
  corpus_prompts: number;
282
366
  top1: number;
283
367
  top1_ci95: Interval;
@@ -350,8 +434,11 @@ export function measureAccuracy(opts: {
350
434
  repo: string;
351
435
  skillsDir: string;
352
436
  rankOpts: RankOptions;
437
+ slice?: SliceName;
353
438
  }): AccuracyArm {
354
- const labelled = promptsForCorpus(opts.corpus, opts.repo);
439
+ const slice = opts.slice ?? 'all';
440
+ const whole = promptsForCorpus(opts.corpus, opts.repo);
441
+ const labelled = partitionBySlice(whole, slice);
355
442
  let top1 = 0;
356
443
  let top3 = 0;
357
444
  const misses: string[] = [];
@@ -365,6 +452,8 @@ export function measureAccuracy(opts: {
365
452
  const census = packCensus(labelled, opts.skillsDir);
366
453
  return {
367
454
  corpus: opts.corpus,
455
+ slice,
456
+ slice_sizes: sliceSizes(whole),
368
457
  corpus_prompts: n,
369
458
  top1: n ? round3(top1 / n) : 0,
370
459
  top1_ci95: ci(top1, n),
@@ -381,20 +470,80 @@ export function measureAccuracy(opts: {
381
470
  };
382
471
  }
383
472
 
473
+ /**
474
+ * The ranker label → the options it means.
475
+ *
476
+ * One table rather than a conditional at each call site, because a measurement
477
+ * harness that resolves `--ranker` differently from the report that quotes it
478
+ * is a comparison of two things under one name. Every entry past `keyword-v1`
479
+ * is a candidate configuration under `road-to-a-ranker-that-routes` Phase 2,
480
+ * each a single flag so it can be measured alone, plus the combinations the
481
+ * same phase names.
482
+ *
483
+ * AN UNKNOWN LABEL THROWS. It used to resolve to `keyword-v1` — the baseline —
484
+ * which is the exact failure this table exists to prevent: every consumer
485
+ * echoes the requested label back into its output, so a typo published a
486
+ * BASELINE number under another configuration's name, in the report's own title
487
+ * and in the regenerate command it prints beside it. The two sibling parsers in
488
+ * this file refuse an unknown value and so does the sweep; the lenient branch
489
+ * was the inconsistent one.
490
+ */
491
+ export const RANKER_LABELS: Readonly<Record<string, RankOptions>> = {
492
+ 'keyword-v1': {},
493
+ 'keyword-v2': { includeTriggers: true },
494
+ 'when-to-use': { includeWhenToUse: true },
495
+ headings: { includeHeadings: true },
496
+ idf: { idfWeighting: true },
497
+ 'idf+when-to-use': { idfWeighting: true, includeWhenToUse: true },
498
+ };
499
+
500
+ /**
501
+ * The message carries NO tool name.
502
+ *
503
+ * Three scripts resolve `--ranker` through this module now, so a hardcoded
504
+ * prefix would print one tool's name when another was the failing command. The
505
+ * caller's own `process.stderr` line is where the command is identified.
506
+ */
507
+ export class UnknownRankerLabel extends Error {
508
+ constructor(label: string) {
509
+ super(
510
+ `unknown --ranker ${label || '(nothing)'}; ` +
511
+ `known: ${Object.keys(RANKER_LABELS).join(' | ')}`,
512
+ );
513
+ this.name = 'UnknownRankerLabel';
514
+ }
515
+ }
516
+
517
+ /** Whether `label` is one of the table's OWN keys — never an inherited one. */
518
+ export function isKnownRanker(label: string): boolean {
519
+ return Object.hasOwn(RANKER_LABELS, label);
520
+ }
521
+
522
+ export function rankOptionsFor(ranker: string): RankOptions {
523
+ // `Object.hasOwn`, not a truthiness check on the lookup: `RANKER_LABELS`
524
+ // is an object literal, so `constructor`, `toString` and `valueOf` resolve
525
+ // to inherited FUNCTIONS rather than to undefined. A plain `=== undefined`
526
+ // guard lets those three through, scores them as the baseline, and echoes
527
+ // the label into the report title — which is the whole defect this guard
528
+ // was added to close, surviving in its own fix.
529
+ if (!isKnownRanker(ranker)) throw new UnknownRankerLabel(ranker);
530
+ return RANKER_LABELS[ranker] as RankOptions;
531
+ }
532
+
384
533
  export function measure(opts: {
385
534
  ranker: string;
386
535
  commit: string;
387
536
  corpus?: CorpusName;
537
+ slice?: SliceName;
388
538
  skillsDir?: string;
389
539
  repo?: string;
390
540
  }): RankerBaseline {
391
541
  const repo = opts.repo ?? REPO;
392
542
  const skillsDir = opts.skillsDir ?? SKILLS_DIR;
393
- // `keyword-v2` is Phase 3.1: the same formula with `triggers:` prose folded
394
- // into each skill's term source. Any other label measures v1.
395
- const rankOpts: RankOptions = opts.ranker === 'keyword-v2' ? { includeTriggers: true } : {};
543
+ // Resolved through the one table, which throws on a label it does not know.
544
+ const rankOpts: RankOptions = rankOptionsFor(opts.ranker);
396
545
  const corpus = opts.corpus ?? 'labelled';
397
- const accuracy = measureAccuracy({ corpus, repo, skillsDir, rankOpts });
546
+ const accuracy = measureAccuracy({ corpus, repo, skillsDir, rankOpts, slice: opts.slice ?? 'all' });
398
547
 
399
548
  const cases = readMatrixCases(repo);
400
549
  let withResult = 0;
@@ -446,17 +595,76 @@ function parseCorpus(argv: readonly string[]): CorpusName {
446
595
  return raw as CorpusName;
447
596
  }
448
597
 
598
+ /**
599
+ * The `--ranker` value, or the baseline when the flag is absent.
600
+ *
601
+ * A PRESENT flag with no value after it is an error, not an omission — the
602
+ * sibling `--slice` has always refused that shape, and substituting the
603
+ * baseline for a malformed flag is the same silent-fallback defect one level
604
+ * up from the label table.
605
+ */
606
+ export function parseRanker(argv: readonly string[]): string {
607
+ const i = argv.indexOf('--ranker');
608
+ if (i === -1) return 'keyword-v1';
609
+ const raw = argv[i + 1];
610
+ if (!raw || raw.startsWith('--')) {
611
+ throw new Error(
612
+ `--ranker expects one of ${Object.keys(RANKER_LABELS).join(' | ')}, got ${raw ?? '(nothing)'}`,
613
+ );
614
+ }
615
+ return raw;
616
+ }
617
+
618
+ /**
619
+ * A flag's value, or `fallback` when the flag is absent.
620
+ *
621
+ * A PRESENT flag followed by nothing — or by the next flag — is an error. Both
622
+ * `--commit` and `--date` are echoed verbatim into published artifacts (the
623
+ * baseline's provenance field, the confusion report's own header), so
624
+ * `--commit --ranker idf` silently publishing `--ranker` as the commit is the
625
+ * same silent-substitution class `parseRanker` was written to close, two flags
626
+ * over in the same two functions.
627
+ */
628
+ export function parseFlagValue(argv: readonly string[], flag: string, fallback: string): string {
629
+ const i = argv.indexOf(flag);
630
+ if (i === -1) return fallback;
631
+ const raw = argv[i + 1];
632
+ if (!raw || raw.startsWith('--')) {
633
+ throw new Error(`${flag} expects a value, got ${raw ?? '(nothing)'}`);
634
+ }
635
+ return raw;
636
+ }
637
+
638
+ export function parseSlice(argv: readonly string[]): SliceName {
639
+ const i = argv.indexOf('--slice');
640
+ if (i === -1) return 'all';
641
+ const raw = argv[i + 1];
642
+ if (!raw || !SLICE_NAMES.includes(raw as SliceName)) {
643
+ throw new Error(
644
+ `measure_skill_ranker_baseline: --slice expects one of ${SLICE_NAMES.join(' | ')}, got ${raw ?? '(nothing)'}`,
645
+ );
646
+ }
647
+ return raw as SliceName;
648
+ }
649
+
449
650
  export function main(argv: readonly string[]): number {
450
- const ranker = argv.includes('--ranker') ? (argv[argv.indexOf('--ranker') + 1] ?? 'keyword-v1') : 'keyword-v1';
451
- const commit = argv.includes('--commit') ? (argv[argv.indexOf('--commit') + 1] ?? 'unknown') : 'unknown';
452
651
  let corpus: CorpusName;
652
+ let slice: SliceName;
653
+ let ranker: string;
654
+ let commit: string;
655
+ let out: RankerBaseline;
453
656
  try {
454
657
  corpus = parseCorpus(argv);
658
+ slice = parseSlice(argv);
659
+ ranker = parseRanker(argv);
660
+ commit = parseFlagValue(argv, '--commit', 'unknown');
661
+ // Inside the same guard as the two parsers above: an unknown --ranker is
662
+ // the same class of mistake and now gets the same exit code.
663
+ out = measure({ ranker, commit, corpus, slice });
455
664
  } catch (err) {
456
665
  process.stderr.write(`${(err as Error).message}\n`);
457
666
  return 2;
458
667
  }
459
- const out = measure({ ranker, commit, corpus });
460
668
  process.stdout.write(`${JSON.stringify(out, null, 2)}\n`);
461
669
  return 0;
462
670
  }
@@ -22,6 +22,11 @@
22
22
  * PROMOTED lesson is surfaced as a one-line visibility marker pointing
23
23
  * at the existing /memory:propose flow. Nothing auto-writes curated
24
24
  * `agents/memory/*.yml`.
25
+ * - Measured: every run appends one line to the gitignored dogfood ledger
26
+ * (`agents/runtime/state/learning-dogfood.jsonl`) BEFORE the early
27
+ * returns, so the 30-day window the default flip is gated on can read a
28
+ * zero rather than a silence. See `runLearn` for why that distinction is
29
+ * the whole point.
25
30
  *
26
31
  * Exit codes (dispatcher contract): 0 always (allow).
27
32
  */
@@ -44,8 +49,9 @@ import type { SettingsClass } from '../shared/settingsClasses.js';
44
49
  import { consentVerdict, type ConsentVerdict } from '../shared/settingsConsent.js';
45
50
 
46
51
  import {
47
- buildSidecar,
52
+ buildSidecarFromSignals,
48
53
  LESSONS_NAME,
54
+ readSignals,
49
55
  renderLessonsMd,
50
56
  SIDECAR_NAME,
51
57
  } from './learning_sidecar.js';
@@ -175,23 +181,102 @@ export function enabled(root: string): boolean {
175
181
  return learnConsent(root) === 'granted';
176
182
  }
177
183
 
178
- /** Run the aggregation. Returns the one-line marker to emit, or null. */
184
+ /**
185
+ * The dogfood ledger: repo-relative POSIX path, one JSON object per line.
186
+ *
187
+ * Under `agents/runtime/` — gitignored wholesale — because it is one machine's
188
+ * timings and counts, not a shared artefact. Its continuity-surface row records
189
+ * it as `excluded`: nothing restores a session from it.
190
+ */
191
+ export const DOGFOOD_LEDGER_POSIX = 'agents/runtime/state/learning-dogfood.jsonl';
192
+
193
+ /** One session end, as the ledger records it. */
194
+ export interface DogfoodLine {
195
+ /** The `now` the aggregation ran against. */
196
+ at: string;
197
+ /** Wall-clock ms for read + aggregate — the portion the 2 s budget governs. */
198
+ wall_ms: number;
199
+ /** Well-formed signal records read from the intake. */
200
+ signals_in: number;
201
+ /** Lessons the aggregation produced. */
202
+ lessons_out: number;
203
+ /** Of those, how many carry the `preferred` verdict. */
204
+ preferred: number;
205
+ }
206
+
207
+ /**
208
+ * Append one measurement line. Never throws: the ledger is a measurement, and
209
+ * a measurement that can break the thing it measures is worse than no ledger.
210
+ */
211
+ export function appendDogfoodLine(root: string, line: DogfoodLine): void {
212
+ try {
213
+ const file = path.join(root, ...DOGFOOD_LEDGER_POSIX.split('/'));
214
+ fs.mkdirSync(path.dirname(file), { recursive: true });
215
+ fs.appendFileSync(file, `${JSON.stringify(line)}\n`, 'utf8');
216
+ } catch {
217
+ // fail-open, same contract as the aggregation itself.
218
+ }
219
+ }
220
+
221
+ /** Read the ledger back. Malformed lines are skipped, an absent file is empty. */
222
+ export function readDogfoodLines(root: string): DogfoodLine[] {
223
+ let text: string;
224
+ try {
225
+ text = fs.readFileSync(path.join(root, ...DOGFOOD_LEDGER_POSIX.split('/')), 'utf8');
226
+ } catch {
227
+ return [];
228
+ }
229
+ const out: DogfoodLine[] = [];
230
+ for (const raw of text.split('\n')) {
231
+ const line = raw.trim();
232
+ if (!line) continue;
233
+ try {
234
+ out.push(JSON.parse(line) as DogfoodLine);
235
+ } catch {
236
+ continue;
237
+ }
238
+ }
239
+ return out;
240
+ }
241
+
242
+ /**
243
+ * Run the aggregation. Returns the one-line marker to emit, or null.
244
+ *
245
+ * MEASURE FIRST, THEN DECIDE. The flip condition this hook's default is gated
246
+ * on (`src/config/agent-settings.template.yml:1376-1378`) names two numbers —
247
+ * non-trivial signal AND session-end p95 < 2 s — and until the ledger below
248
+ * existed, neither was recorded: every early return here fired before any
249
+ * measurement point, so a checkout with an empty intake produced an unbroken
250
+ * silence. A silence and a measured zero read identically to whoever opens the
251
+ * window, which is the one failure a 30-day window cannot recover from. So the
252
+ * read, the aggregation and the ledger line all happen before the first return.
253
+ */
179
254
  export function runLearn(root: string, nowIso: string, budgetMs: number = BUDGET_MS): string | null {
180
255
  const started = Date.now();
181
256
  const intakeDir = path.join(root, 'agents', 'memory', 'intake');
182
257
  const outDir = path.join(root, 'agents', 'memory');
183
- if (!fs.existsSync(intakeDir)) return null;
184
- const sidecar = buildSidecar(intakeDir, nowIso);
258
+ // An absent intake directory is a zero-signal session, not an absent one.
259
+ const haveIntake = fs.existsSync(intakeDir);
260
+ const signals = haveIntake ? readSignals(intakeDir) : [];
261
+ const sidecar = buildSidecarFromSignals(signals, nowIso);
262
+ const preferred = sidecar.lessons.filter((l) => l.verdict === 'preferred').length;
263
+ appendDogfoodLine(root, {
264
+ at: nowIso,
265
+ wall_ms: Date.now() - started,
266
+ signals_in: signals.length,
267
+ lessons_out: sidecar.lessons.length,
268
+ preferred,
269
+ });
270
+ if (!haveIntake) return null;
185
271
  if (Date.now() - started > budgetMs) return null; // over budget — skip the write
186
272
  if (sidecar.lessons.length === 0) return null;
187
273
  fs.writeFileSync(path.join(outDir, SIDECAR_NAME), `${JSON.stringify(sidecar, null, 2)}\n`, 'utf8');
188
274
  fs.writeFileSync(path.join(outDir, LESSONS_NAME), renderLessonsMd(sidecar), 'utf8');
189
- const promoted = sidecar.lessons.filter((l) => l.verdict === 'preferred').length;
190
275
  const deadEnds = sidecar.lessons.filter((l) => l.verdict === 'dead_end').length;
191
276
  // Memory-visibility contract shape: one line, ids/counters only, no bodies.
192
277
  return (
193
278
  `🧠 Memory: sidecar refreshed — ${sidecar.lessons.length} lesson(s) ` +
194
- `(${promoted} preferred · ${deadEnds} dead-end); promote via /memory:propose`
279
+ `(${preferred} preferred · ${deadEnds} dead-end); promote via /memory:propose`
195
280
  );
196
281
  }
197
282
 
@@ -65,6 +65,7 @@ import {
65
65
  triggerlessRuleIds,
66
66
  type Router,
67
67
  } from './_lib/rule_injection.js';
68
+ import { ruleBody } from './_lib/rule_law_section.js';
68
69
  import {
69
70
  renderArmExperiment,
70
71
  runArmExperiment,
@@ -682,6 +683,34 @@ export function runEndpoints(corpusDir: string): EndpointResult[] {
682
683
  ]);
683
684
  const thinnedAndPathOnly = pathOnlyReach.filter((id) => !alwaysEager.has(id));
684
685
 
686
+ // PROMPT-LEVEL beside RULE-LEVEL — step 0.4 of
687
+ // `road-to-a-rule-carrier-that-works-outside-the-repo`.
688
+ //
689
+ // The two numbers above are per RULE: a rule counts as reachable when ANY
690
+ // of its labelled positives matches. That is the right unit for the bar —
691
+ // "no labelled rule left unreachable" — and it is the wrong unit for the
692
+ // question a reader actually asks next, which is how much of the corpus the
693
+ // shipped binding delivers. A rule reachable on one of twelve prompts and a
694
+ // rule reachable on all twelve are the same row in `byRule`.
695
+ //
696
+ // So the pair is reported in both units. The SUMMARY view has computed the
697
+ // prompt-level figures since the two-readings split landed; what was missing
698
+ // is that `--endpoints`, which is the view a claim cites, printed only the
699
+ // rule-level half. A reader comparing the two views saw 99/102 and 305/335
700
+ // and had no way to tell they measure different things.
701
+ const promptHonoured = positives.filter((c) => reach(c, true)).length;
702
+ const promptIgnored = positives.filter((c) => reach(c, false)).length;
703
+
704
+ // The SUMMARY view prints a LOWER pair over the same corpus, and the gap is
705
+ // not noise. `scoreExact` calls `matchTierRules(router, c.prompt, of)` with
706
+ // three arguments and drops the case's `command`, so a positive whose rule
707
+ // is reached by a `command:` trigger scores as a miss there and as a hit
708
+ // here. Recorded rather than repaired: this phase changes no behaviour, and
709
+ // the SUMMARY figures are cited in a roadmap and in the prereg. Both
710
+ // readings are printed so neither view has to be trusted over the other.
711
+ const summaryHonoured = scoreExact(router, cases, true);
712
+ const summaryIgnored = scoreExact(router, cases, false);
713
+
685
714
  // (c) A near-miss must never deliver its labelled rule.
686
715
  const exact = scoreExact(router, cases, true);
687
716
 
@@ -716,7 +745,15 @@ export function runEndpoints(corpusDir: string): EndpointResult[] {
716
745
  `${byRule.size - pathOnlyReach.length}/${byRule.size}; reachable only via a ` +
717
746
  `path trigger: ${pathOnlyReach.length === 0 ? 'none' : pathOnlyReach.join(', ')}` +
718
747
  ` (of which thinned, i.e. at no scope on that route: ` +
719
- `${thinnedAndPathOnly.length === 0 ? 'none' : thinnedAndPathOnly.join(', ')})`,
748
+ `${thinnedAndPathOnly.length === 0 ? 'none' : thinnedAndPathOnly.join(', ')})` +
749
+ ` | per-prompt reach: ${promptIgnored}/${positives.length} with open_files ` +
750
+ `IGNORED against ${promptHonoured}/${positives.length} honoured — the same ` +
751
+ `corpus in prompts rather than in rules, because a rule reachable on one of ` +
752
+ `twelve positives and one reachable on all twelve are one row above` +
753
+ ` | SUMMARY prints ${summaryIgnored.hits}/${summaryIgnored.positives} and ` +
754
+ `${summaryHonoured.hits}/${summaryHonoured.positives} for the same corpus: ` +
755
+ `scoreExact drops the case's command trigger, so a command-reached positive ` +
756
+ `misses there and hits here`,
720
757
  bar: 'unreachable == 0 (a rule with zero matched positives is a rule the mode removed)',
721
758
  passed: unreachable.length === 0,
722
759
  },
@@ -781,13 +818,23 @@ export function runSelftest(corpusDir: string): SelftestCase[] {
781
818
  });
782
819
  } else {
783
820
  const id = injection.rules[0] as string;
784
- const projected = loadRuleBody(REPO_ROOT, id) ?? '';
785
- const mutated = `${projected.trim().slice(0, -1)}~MUTATED~`;
821
+ // THE DELIVERED FORM, not the raw file. Since step 1.4 the concern emits
822
+ // `ruleBody` of the projection — frontmatter and HTML comments stripped
823
+ // by the same parser `project_thin_rules` uses — so comparing against
824
+ // the raw file asserts a substring the payload no longer contains. The
825
+ // endpoint's claim is unchanged: the payload carries the body verbatim
826
+ // and rejects a mutation of it. What "the body" means moved, and this
827
+ // moved with it.
828
+ const projected = ruleBody(loadRuleBody(REPO_ROOT, id) ?? '');
829
+ const mutated = `${projected.slice(0, -1)}~MUTATED~`;
786
830
  out.push({
787
831
  endpoint: 'a-delivery',
788
832
  name: 'one-byte mutation rejected',
789
- passed: injection.body.includes(projected.trim()) && !injection.body.includes(mutated),
790
- detail: `${id}: the concern's payload carries the projected body verbatim and rejects a mutation of it`,
833
+ passed:
834
+ projected !== ''
835
+ && injection.body.includes(projected)
836
+ && !injection.body.includes(mutated),
837
+ detail: `${id}: the concern's payload carries the delivered body verbatim and rejects a mutation of it`,
791
838
  });
792
839
  }
793
840