@event4u/agent-config 14.12.0 → 14.13.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 (466) 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 +234 -0
  5. package/CONTRIBUTING.md +38 -0
  6. package/CREDITS.md +135 -0
  7. package/NOTICE +13 -0
  8. package/README.md +8 -6
  9. package/dist/agent-src/commands/analyze/inbox.md +109 -2
  10. package/dist/agent-src/commands/feature/dev.md +7 -2
  11. package/dist/agent-src/commands/ghostwriter/write.md +5 -1
  12. package/dist/agent-src/commands/implement-ticket.md +1 -1
  13. package/dist/agent-src/commands/mission/upgrade.md +1 -1
  14. package/dist/agent-src/commands/orchestrate.md +2 -2
  15. package/dist/agent-src/commands/post-as/me.md +5 -1
  16. package/dist/agent-src/commands/pr/merge.md +19 -6
  17. package/dist/agent-src/commands/roadmap/process-full.md +19 -5
  18. package/dist/agent-src/commands/tdd/red.md +12 -2
  19. package/dist/agent-src/commands/work.md +1 -1
  20. package/dist/agent-src/contexts/communication/rules-auto/token-efficiency-mechanics.md +40 -0
  21. package/dist/agent-src/contexts/execution/auto-dispatch-classification.md +5 -2
  22. package/dist/agent-src/contexts/execution/autonomy-mechanics.md +21 -0
  23. package/dist/agent-src/contexts/execution/orchestration-telemetry.md +1 -0
  24. package/dist/agent-src/contexts/execution/subagent-response-contract.md +40 -0
  25. package/dist/agent-src/contexts/execution/subagent-steering.md +13 -1
  26. package/dist/agent-src/contexts/execution/terminal-states.md +96 -0
  27. package/dist/agent-src/rules/artifact-drafting-protocol.md +1 -1
  28. package/dist/agent-src/rules/broken-access-control.md +4 -0
  29. package/dist/agent-src/rules/code-provenance.md +4 -0
  30. package/dist/agent-src/rules/design-fidelity.md +1 -1
  31. package/dist/agent-src/rules/domain-safety-pii.md +4 -0
  32. package/dist/agent-src/rules/domain-safety-retention.md +4 -0
  33. package/dist/agent-src/rules/engineering-safety-floor.md +2 -0
  34. package/dist/agent-src/rules/external-code-graph-interop.md +14 -12
  35. package/dist/agent-src/rules/improve-before-implement.md +6 -8
  36. package/dist/agent-src/rules/laravel-routing.md +1 -1
  37. package/dist/agent-src/rules/lethal-trifecta-guard.md +4 -0
  38. package/dist/agent-src/rules/secret-vcs-guard.md +2 -0
  39. package/dist/agent-src/rules/security-sensitive-stop.md +2 -0
  40. package/dist/agent-src/rules/source-confidentiality.md +12 -34
  41. package/dist/agent-src/rules/symfony-routing.md +1 -1
  42. package/dist/agent-src/rules/token-efficiency.md +14 -0
  43. package/dist/agent-src/rules/tool-safety.md +4 -0
  44. package/dist/agent-src/rules/untrusted-input-defense.md +4 -0
  45. package/dist/agent-src/scripts/archive_completed_roadmaps.ts +247 -3
  46. package/dist/agent-src/scripts/guarded_baseline.ts +328 -0
  47. package/dist/agent-src/scripts/roadmap_set_step.ts +253 -0
  48. package/dist/agent-src/scripts/stub_queue.ts +82 -0
  49. package/dist/agent-src/scripts/stubs_due.ts +400 -0
  50. package/dist/agent-src/scripts/ticket_bundles.ts +84 -0
  51. package/dist/agent-src/scripts/update_roadmap_progress.ts +8 -4
  52. package/dist/agent-src/skills/brand-asset-generation/SKILL.md +17 -4
  53. package/dist/agent-src/skills/brand-audit/SKILL.md +33 -0
  54. package/dist/agent-src/skills/code-intelligence/SKILL.md +57 -0
  55. package/dist/agent-src/skills/database/SKILL.md +8 -0
  56. package/dist/agent-src/skills/dependency-upgrade/SKILL.md +27 -0
  57. package/dist/agent-src/skills/design-review/SKILL.md +1 -1
  58. package/dist/agent-src/skills/doc-coauthoring/SKILL.md +1 -1
  59. package/dist/agent-src/skills/existing-ui-audit/SKILL.md +17 -1
  60. package/dist/agent-src/skills/feature-planning/SKILL.md +9 -2
  61. package/dist/agent-src/skills/frontend-render-security/SKILL.md +44 -0
  62. package/dist/agent-src/skills/git-workflow/SKILL.md +1 -1
  63. package/dist/agent-src/skills/js-library-packaging/scripts/check_package_surface.ts +38 -4
  64. package/dist/agent-src/skills/judge-artifact-completeness/rubrics/ticket-quality-score.json +2 -2
  65. package/dist/agent-src/skills/laravel/SKILL.md +35 -0
  66. package/dist/agent-src/skills/laravel-migration/SKILL.md +57 -3
  67. package/dist/agent-src/skills/monorepo-workspace/SKILL.md +26 -0
  68. package/dist/agent-src/skills/playbook-authoring/SKILL.md +28 -1
  69. package/dist/agent-src/skills/react-shadcn-ui/SKILL.md +1 -1
  70. package/dist/agent-src/skills/skill-writing/references/procedure.md +5 -5
  71. package/dist/agent-src/skills/sql-writing/SKILL.md +7 -1
  72. package/dist/agent-src/skills/standards-from-config/SKILL.md +27 -4
  73. package/dist/agent-src/skills/supply-chain-intake/SKILL.md +11 -4
  74. package/dist/agent-src/skills/symfony-workflow/SKILL.md +35 -0
  75. package/dist/agent-src/skills/test-case-discovery/SKILL.md +85 -0
  76. package/dist/agent-src/skills/test-driven-development/SKILL.md +33 -9
  77. package/dist/agent-src/skills/testing-anti-patterns/SKILL.md +1 -1
  78. package/dist/agent-src/skills/testing-anti-patterns/process-anti-patterns.md +37 -0
  79. package/dist/agent-src/skills/ui-component-architect/SKILL.md +45 -2
  80. package/dist/agent-src/skills/upstream-contribute/SKILL.md +38 -1
  81. package/dist/agent-src/skills/verify-repair-loop/SKILL.md +9 -4
  82. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  83. package/dist/agent-src/templates/roadmaps.md +97 -3
  84. package/dist/agent-src/templates/scripts/check_memory.ts +258 -17
  85. package/dist/agent-src/templates/scripts/check_memory_proposal.ts +9 -3
  86. package/dist/agent-src/templates/scripts/memory_lookup.ts +86 -0
  87. package/dist/agent-src/templates/scripts/memory_signal.ts +44 -0
  88. package/dist/agent-src/templates/scripts/work_engine/directives/backend/implement.ts +90 -0
  89. package/dist/agent-src/templates/scripts/work_engine/directives/backend/index.ts +11 -0
  90. package/dist/agent-src/templates/scripts/work_engine/directives/backend/test.ts +23 -1
  91. package/dist/agent-src/templates/scripts/work_engine/directives/backend/verify.ts +10 -1
  92. package/dist/agent-src/templates/scripts/work_engine/directives/mixed/stitch.ts +5 -1
  93. package/dist/agent-src/templates/scripts/work_engine/stack/runner.ts +102 -14
  94. package/dist/cli/commands/uiAudit.js +124 -5
  95. package/dist/cli/registry.js +2 -0
  96. package/dist/cli-delegate/{chunk-HYWSRNUX.js → chunk-2DK2VJWG.js} +3 -3
  97. package/dist/cli-delegate/{chunk-JCW7ZLHK.js → chunk-4FS4WRXP.js} +5 -0
  98. package/dist/cli-delegate/{chunk-OMIVVCG2.js → chunk-A4QJ6ENT.js} +24 -0
  99. package/dist/cli-delegate/{chunk-JJA2QEWA.js → chunk-AKJGUQNO.js} +466 -285
  100. package/dist/cli-delegate/{chunk-E46KPN6K.js → chunk-BTWUYDGN.js} +424 -363
  101. package/dist/cli-delegate/{chunk-K4FFODTS.js → chunk-ILMUPLGW.js} +1227 -316
  102. package/dist/cli-delegate/{chunk-FF7WAFMR.js → chunk-KPBWFOPZ.js} +5 -1
  103. package/dist/cli-delegate/cmd_adr_effective.js +102 -0
  104. package/dist/cli-delegate/cmd_conformance.js +42 -10
  105. package/dist/cli-delegate/cmd_doctor.js +3 -3
  106. package/dist/cli-delegate/cmd_export.js +3 -2
  107. package/dist/cli-delegate/cmd_fleet.js +2 -2
  108. package/dist/cli-delegate/cmd_handoff.js +1 -1
  109. package/dist/cli-delegate/cmd_preflight.js +2 -2
  110. package/dist/cli-delegate/cmd_prune.js +3 -2
  111. package/dist/cli-delegate/cmd_settings_get.js +1 -1
  112. package/dist/cli-delegate/cmd_settings_migrate.js +3 -2
  113. package/dist/cli-delegate/cmd_settings_set.js +2 -2
  114. package/dist/cli-delegate/cmd_sync.js +3 -2
  115. package/dist/cli-delegate/cmd_uninstall.js +3 -2
  116. package/dist/cli-delegate/cmd_validate.js +3 -2
  117. package/dist/discovery/deprecation-report.md +1 -1
  118. package/dist/discovery/discovery-manifest.json +57 -57
  119. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  120. package/dist/discovery/discovery-manifest.summary.md +1 -1
  121. package/dist/discovery/dormancy-report.md +1 -1
  122. package/dist/discovery/orphan-report.md +1 -1
  123. package/dist/discovery/packs.json +2 -2
  124. package/dist/discovery/trust-report.md +1 -1
  125. package/dist/discovery/workspaces.json +2 -2
  126. package/dist/hooks/dispatch.js +1836 -712
  127. package/dist/install/claudeRuleRewrite.js +254 -0
  128. package/dist/install/detect.js +29 -16
  129. package/dist/install/detect_php_shape.js +159 -0
  130. package/dist/install/install.mjs +634 -428
  131. package/dist/mcp/awesome-mcp-servers.row.md +1 -1
  132. package/dist/mcp/mcp-cloudflare-catalogue.json +1 -1
  133. package/dist/mcp/registry-manifest.json +3 -3
  134. package/dist/mcp/server.json +3 -3
  135. package/dist/mcp/server.mjs +4818 -2645
  136. package/dist/server/schemas/settings.js +3 -0
  137. package/dist/ui/assets/index-D0jaCRPM.js +29 -0
  138. package/dist/ui/index.html +1 -1
  139. package/docs/MIGRATION.md +1 -1
  140. package/docs/THIRD-PARTY-NOTICES.md +25 -0
  141. package/docs/contracts/provider-lifecycle.md +1 -1
  142. package/docs/contracts/settings-classes.md +10 -4
  143. package/docs/guidelines/agent-infra/5w2h-analysis.md +3 -3
  144. package/docs/guidelines/agent-infra/agent-interaction-and-decision-quality.md +1 -0
  145. package/docs/guidelines/agent-infra/artifact-drafting-protocol-mechanics.md +58 -3
  146. package/docs/guidelines/agent-infra/ask-when-uncertain-demos.md +2 -2
  147. package/docs/guidelines/agent-infra/asking-and-brevity-examples.md +3 -3
  148. package/docs/guidelines/agent-infra/break-glass-usage.md +7 -7
  149. package/docs/guidelines/agent-infra/carve-out-predicates.md +3 -3
  150. package/docs/guidelines/agent-infra/comparison-matrix.md +1 -1
  151. package/docs/guidelines/agent-infra/critical-thinking.md +4 -4
  152. package/docs/guidelines/agent-infra/direct-answers-demos.md +2 -2
  153. package/docs/guidelines/agent-infra/domain-eval-anti-pattern.md +1 -1
  154. package/docs/guidelines/agent-infra/engineering-memory-data-format.md +3 -3
  155. package/docs/guidelines/agent-infra/existence-question-verdicts.md +80 -0
  156. package/docs/guidelines/agent-infra/first-principles.md +2 -2
  157. package/docs/guidelines/agent-infra/framework-neutrality-patterns.md +2 -2
  158. package/docs/guidelines/agent-infra/guarded-baseline.md +39 -0
  159. package/docs/guidelines/agent-infra/inversion-thinking.md +5 -5
  160. package/docs/guidelines/agent-infra/language-and-tone-examples.md +1 -1
  161. package/docs/guidelines/agent-infra/layered-settings.md +10 -10
  162. package/docs/guidelines/agent-infra/memory-access.md +1 -1
  163. package/docs/guidelines/agent-infra/mental-models.md +3 -3
  164. package/docs/guidelines/agent-infra/reuse-verdict-mechanics.md +71 -0
  165. package/docs/guidelines/agent-infra/review-routing-data-format.md +7 -7
  166. package/docs/guidelines/agent-infra/roadmap-progress-mechanics.md +13 -7
  167. package/docs/guidelines/agent-infra/role-contracts.md +2 -2
  168. package/docs/guidelines/agent-infra/role-mode-router.md +3 -3
  169. package/docs/guidelines/agent-infra/rule-type-governance.md +2 -2
  170. package/docs/guidelines/agent-infra/scqa-framework.md +5 -5
  171. package/docs/guidelines/agent-infra/self-improvement-pipeline.md +6 -6
  172. package/docs/guidelines/agent-infra/simplicity-and-goal-demos.md +2 -2
  173. package/docs/guidelines/agent-infra/six-hats.md +3 -3
  174. package/docs/guidelines/agent-infra/skill-quality-checklist.md +6 -6
  175. package/docs/guidelines/agent-infra/source-confidentiality-mechanics.md +109 -0
  176. package/docs/guidelines/agent-infra/systems-thinking.md +1 -1
  177. package/docs/guidelines/agent-infra/verify-before-complete-demos.md +2 -2
  178. package/docs/guidelines/augment-portability-patterns.md +6 -4
  179. package/docs/guidelines/component-oriented-and-oop-development.md +3 -2
  180. package/docs/guidelines/cross-role-handoff.md +2 -2
  181. package/docs/guidelines/design-fidelity-mechanics.md +1 -1
  182. package/docs/guidelines/docs/readme-size-and-splitting.md +3 -3
  183. package/docs/guidelines/php/database.md +90 -7
  184. package/docs/guidelines/prompt-templates.md +7 -7
  185. package/llms.txt +2 -2
  186. package/package.json +5 -2
  187. package/src/agent-src/scripts/archive_completed_roadmaps.ts +247 -3
  188. package/src/agent-src/scripts/guarded_baseline.ts +328 -0
  189. package/src/agent-src/scripts/roadmap_set_step.ts +253 -0
  190. package/src/agent-src/scripts/stub_queue.ts +82 -0
  191. package/src/agent-src/scripts/stubs_due.ts +400 -0
  192. package/src/agent-src/scripts/ticket_bundles.ts +84 -0
  193. package/src/agent-src/scripts/update_roadmap_progress.ts +8 -4
  194. package/src/agent-src/templates/scripts/check_memory.ts +258 -17
  195. package/src/agent-src/templates/scripts/check_memory_proposal.ts +9 -3
  196. package/src/agent-src/templates/scripts/memory_lookup.ts +86 -0
  197. package/src/agent-src/templates/scripts/memory_signal.ts +44 -0
  198. package/src/agent-src/templates/scripts/work_engine/directives/backend/implement.ts +90 -0
  199. package/src/agent-src/templates/scripts/work_engine/directives/backend/index.ts +11 -0
  200. package/src/agent-src/templates/scripts/work_engine/directives/backend/test.ts +23 -1
  201. package/src/agent-src/templates/scripts/work_engine/directives/backend/verify.ts +10 -1
  202. package/src/agent-src/templates/scripts/work_engine/directives/mixed/stitch.ts +5 -1
  203. package/src/agent-src/templates/scripts/work_engine/stack/runner.ts +102 -14
  204. package/src/config/agent-settings.template.yml +32 -3
  205. package/src/config/assurance-capability-registry.json +21 -9
  206. package/src/config/canonical-terms.yml +119 -0
  207. package/src/config/capability-native-outcome-bars.json +81 -0
  208. package/src/config/ci-local-parity.yml +80 -0
  209. package/src/config/drafting-channels.yml +43 -0
  210. package/src/config/estate-count-budget.json +38 -36
  211. package/src/config/evaluator-budgets.json +14 -14
  212. package/src/config/gate-coverage.yml +579 -63
  213. package/src/config/gate-reachability-exemptions.json +21 -0
  214. package/src/config/gate-violation-baselines.json +37 -19
  215. package/src/config/harness-evolution-budget.json +25 -0
  216. package/src/config/lapsed-beta-baseline.json +96 -0
  217. package/src/config/memory-twin-verdicts.yml +129 -0
  218. package/src/config/metric-registry.yml +124 -0
  219. package/src/config/pack-size-budget.json +16 -15
  220. package/src/config/preamble-payload-budget.json +8 -8
  221. package/src/config/profiles/balanced.ini +27 -0
  222. package/src/config/profiles/full.ini +27 -0
  223. package/src/config/profiles/minimal.ini +27 -0
  224. package/src/config/publish-surface.json +9 -2
  225. package/src/config/routing-coverage-seed.json +16 -2
  226. package/src/config/rule-stub-ceilings.json +126 -112
  227. package/src/config/turnaround-budget.json +66 -0
  228. package/src/install/claudeRuleRewrite.ts +288 -0
  229. package/src/install/detect.ts +60 -17
  230. package/src/install/detect_php_shape.ts +191 -0
  231. package/src/scripts/_cli/cmd_doctor.ts +27 -112
  232. package/src/scripts/_dispatch.bash +27 -0
  233. package/src/scripts/_lib/activation_ladder.ts +211 -0
  234. package/src/scripts/_lib/activation_states.ts +88 -0
  235. package/src/scripts/_lib/adherence_detectors.ts +143 -0
  236. package/src/scripts/_lib/arith_claims.ts +181 -0
  237. package/src/scripts/_lib/arm_ranking.ts +62 -0
  238. package/src/scripts/_lib/asset_delivery_ledger.ts +303 -0
  239. package/src/scripts/_lib/audit_field_provenance.ts +54 -0
  240. package/src/scripts/_lib/auto_dispatch.ts +32 -3
  241. package/src/scripts/_lib/bench_ab_size_claim.ts +98 -4
  242. package/src/scripts/_lib/benchmark_cache_fields.ts +158 -0
  243. package/src/scripts/_lib/candidate_proposer.ts +402 -0
  244. package/src/scripts/_lib/candidate_record.ts +601 -0
  245. package/src/scripts/_lib/capture_rate.ts +319 -0
  246. package/src/scripts/_lib/collector_denominator.ts +771 -0
  247. package/src/scripts/_lib/collector_record.ts +340 -0
  248. package/src/scripts/_lib/collector_store.ts +664 -0
  249. package/src/scripts/_lib/collector_supervision.ts +827 -0
  250. package/src/scripts/_lib/concern_estate.ts +77 -0
  251. package/src/scripts/_lib/config_chain.ts +477 -0
  252. package/src/scripts/_lib/config_cost.ts +209 -0
  253. package/src/scripts/_lib/council_settings_block.ts +6 -0
  254. package/src/scripts/_lib/curator_ops.ts +172 -0
  255. package/src/scripts/_lib/delivery_arm_experiment.ts +266 -0
  256. package/src/scripts/_lib/delivery_criticality.ts +126 -0
  257. package/src/scripts/_lib/doctor_runtime_checks.ts +70 -0
  258. package/src/scripts/_lib/empty_cycles.ts +88 -0
  259. package/src/scripts/_lib/eval_publication.ts +354 -0
  260. package/src/scripts/_lib/evaluation_vector.ts +283 -0
  261. package/src/scripts/_lib/evaluator_promotion.ts +225 -0
  262. package/src/scripts/_lib/evidence_basis.ts +118 -0
  263. package/src/scripts/_lib/exec_evidence.ts +8 -0
  264. package/src/scripts/_lib/experience_card.ts +272 -0
  265. package/src/scripts/_lib/experience_report.ts +151 -0
  266. package/src/scripts/_lib/experiment_freeze.ts +112 -0
  267. package/src/scripts/_lib/generated_by.ts +66 -0
  268. package/src/scripts/_lib/harness_evolution_guards.ts +229 -0
  269. package/src/scripts/_lib/hook_effect_probe.ts +141 -0
  270. package/src/scripts/_lib/host_denominator.ts +687 -0
  271. package/src/scripts/_lib/host_projection_reach.ts +157 -0
  272. package/src/scripts/_lib/ignored_blocker.ts +148 -0
  273. package/src/scripts/_lib/install_reach_checks.ts +495 -0
  274. package/src/scripts/_lib/json_duplicate_keys.ts +150 -0
  275. package/src/scripts/_lib/judge_hygiene.ts +228 -0
  276. package/src/scripts/_lib/legacy_boundary_map.ts +228 -0
  277. package/src/scripts/_lib/loop_guards.ts +299 -0
  278. package/src/scripts/_lib/loss_class.ts +129 -0
  279. package/src/scripts/_lib/machine_wake.ts +51 -0
  280. package/src/scripts/_lib/md_prose_lines.ts +177 -0
  281. package/src/scripts/_lib/minimality_tiebreak.ts +168 -0
  282. package/src/scripts/_lib/one_resolver_invariant.ts +449 -0
  283. package/src/scripts/_lib/orchestration_record.ts +253 -5
  284. package/src/scripts/_lib/outcome_envelope.ts +227 -0
  285. package/src/scripts/_lib/outcome_vocabularies.ts +125 -0
  286. package/src/scripts/_lib/paired_stats.ts +245 -0
  287. package/src/scripts/_lib/paired_verdict.ts +226 -0
  288. package/src/scripts/_lib/prefix_stable_surfaces.ts +123 -0
  289. package/src/scripts/_lib/privacy_class.ts +47 -0
  290. package/src/scripts/_lib/regression_neighbourhood.ts +259 -0
  291. package/src/scripts/_lib/repeated_failure.ts +105 -0
  292. package/src/scripts/_lib/repo_root.ts +111 -0
  293. package/src/scripts/_lib/review_skipped_record.ts +37 -0
  294. package/src/scripts/_lib/role_split.ts +338 -0
  295. package/src/scripts/_lib/runtime_journal.ts +1499 -0
  296. package/src/scripts/_lib/runtime_wiring_checks.ts +430 -0
  297. package/src/scripts/_lib/skill_catalogue.ts +11 -1
  298. package/src/scripts/_lib/source_digest.ts +200 -0
  299. package/src/scripts/_lib/source_redact.ts +260 -0
  300. package/src/scripts/_lib/source_shape.ts +502 -0
  301. package/src/scripts/_lib/source_snapshot_dedup.ts +199 -0
  302. package/src/scripts/_lib/strip_comments.d.mts +8 -0
  303. package/src/scripts/_lib/strip_comments.mjs +65 -0
  304. package/src/scripts/_lib/test_red_state.ts +154 -0
  305. package/src/scripts/_lib/trigger_shift.ts +144 -0
  306. package/src/scripts/_lib/untrusted_content.ts +82 -7
  307. package/src/scripts/adoption_report.ts +1 -1
  308. package/src/scripts/adr/evidence_census.ts +1 -1
  309. package/src/scripts/adr/regenerate_index.ts +26 -2
  310. package/src/scripts/adr_cite_check.ts +164 -1
  311. package/src/scripts/ai_council/blind_review.ts +11 -1
  312. package/src/scripts/ai_council/clients.ts +4 -0
  313. package/src/scripts/ai_council/config.ts +3 -0
  314. package/src/scripts/ai_council/consensus.ts +28 -8
  315. package/src/scripts/ai_council/consensus_round.ts +226 -0
  316. package/src/scripts/ai_council/disagreement_signal.ts +338 -0
  317. package/src/scripts/ai_council/inline_findings.ts +340 -0
  318. package/src/scripts/ai_council/judge_position_bias.ts +197 -0
  319. package/src/scripts/ai_council/orchestrator.ts +16 -203
  320. package/src/scripts/ai_council/orchestrator_results.ts +23 -3
  321. package/src/scripts/ai_council/pricing.ts +106 -0
  322. package/src/scripts/ai_council/prompts.ts +74 -7
  323. package/src/scripts/ai_council/provider_leakage_bench.ts +327 -0
  324. package/src/scripts/ai_council/py_format.ts +14 -3
  325. package/src/scripts/ai_council/recouncil_guard.ts +293 -0
  326. package/src/scripts/ai_council/response_record.ts +50 -0
  327. package/src/scripts/ai_council/topology_bench_manifest.ts +860 -0
  328. package/src/scripts/ai_council/topology_vocabulary.ts +175 -0
  329. package/src/scripts/asset_delivery_ledger.ts +157 -0
  330. package/src/scripts/attest_artifact.ts +193 -0
  331. package/src/scripts/audit_auto_rules.ts +2 -1
  332. package/src/scripts/audit_command_surface.ts +1 -1
  333. package/src/scripts/audit_mcp_tools.ts +2 -2
  334. package/src/scripts/audit_skill_overlap_allowlist.json +6 -1
  335. package/src/scripts/audit_user_type_axis.ts +2 -1
  336. package/src/scripts/bench_ab_clone.ts +215 -10
  337. package/src/scripts/bench_ab_integrity.ts +119 -5
  338. package/src/scripts/bench_ab_v2_stats.ts +40 -213
  339. package/src/scripts/build_archive_index.ts +1 -1
  340. package/src/scripts/build_linear_digest.ts +1 -1
  341. package/src/scripts/build_proof.ts +11 -4
  342. package/src/scripts/build_rule_trigger_matrix.ts +1 -1
  343. package/src/scripts/build_source_digests.ts +157 -0
  344. package/src/scripts/cache_realization_report.ts +103 -0
  345. package/src/scripts/check_adr_frontmatter.ts +126 -0
  346. package/src/scripts/check_benchmark_report_fields.ts +207 -0
  347. package/src/scripts/check_beta_review_markers.ts +235 -6
  348. package/src/scripts/check_bundle_path_leakage.ts +34 -1
  349. package/src/scripts/check_claims.ts +576 -6
  350. package/src/scripts/check_composite_arming.ts +35 -0
  351. package/src/scripts/check_concern_admissions.ts +365 -0
  352. package/src/scripts/check_enforcement_coverage.ts +28 -0
  353. package/src/scripts/check_estate_count.ts +142 -1
  354. package/src/scripts/check_gate_reachability.ts +383 -0
  355. package/src/scripts/check_host_loadability.ts +104 -0
  356. package/src/scripts/check_loss_class_declared.ts +308 -0
  357. package/src/scripts/check_md_language.ts +15 -98
  358. package/src/scripts/check_memory_proposal.ts +18 -11
  359. package/src/scripts/check_no_currency_in_cost_surfaces.ts +225 -0
  360. package/src/scripts/check_no_external_sources.ts +281 -6
  361. package/src/scripts/check_pack_conformance_fixture.ts +11 -1
  362. package/src/scripts/check_pack_size.ts +59 -2
  363. package/src/scripts/check_preamble_payload_budget.ts +114 -3
  364. package/src/scripts/check_prefix_stable_mutation.ts +372 -0
  365. package/src/scripts/check_publish_surface.ts +10 -1
  366. package/src/scripts/check_references.ts +24 -1
  367. package/src/scripts/check_rule_evidence_declaration.ts +316 -0
  368. package/src/scripts/check_rule_projection_integrity.ts +14 -4
  369. package/src/scripts/check_score_contract.ts +26 -3
  370. package/src/scripts/check_static_parity.ts +285 -0
  371. package/src/scripts/check_supervision_claim_atomicity.ts +498 -0
  372. package/src/scripts/check_suppression_hygiene.ts +15 -0
  373. package/src/scripts/ci_settle.ts +39 -2
  374. package/src/scripts/code_graph/build.ts +60 -3
  375. package/src/scripts/code_graph/extract.ts +44 -0
  376. package/src/scripts/code_graph/types.ts +24 -0
  377. package/src/scripts/collector_daemon.ts +714 -0
  378. package/src/scripts/config_cost_report.ts +302 -0
  379. package/src/scripts/consumer_matrix.ts +74 -1
  380. package/src/scripts/cost/budget.mjs +10 -3
  381. package/src/scripts/cost/track.mjs +10 -3
  382. package/src/scripts/council_cli.ts +37 -54
  383. package/src/scripts/dispatch_r2_reviewer.ts +46 -5
  384. package/src/scripts/evolution_lab.ts +980 -0
  385. package/src/scripts/external_sources_denylist.json +5 -18
  386. package/src/scripts/extract_audit_patterns.ts +21 -0
  387. package/src/scripts/fold_intake.ts +12 -1
  388. package/src/scripts/gen_discovery_baseline.ts +1 -1
  389. package/src/scripts/generate_capabilities_index.ts +12 -5
  390. package/src/scripts/generate_capability_matrix.ts +3 -3
  391. package/src/scripts/generate_command_flows.ts +2 -2
  392. package/src/scripts/generate_cookbook.ts +2 -2
  393. package/src/scripts/generate_index.ts +8 -1
  394. package/src/scripts/generate_knowledge_index.ts +2 -1
  395. package/src/scripts/generate_ownership_matrix.ts +2 -1
  396. package/src/scripts/generate_pack_manifests.ts +10 -2
  397. package/src/scripts/generate_role_experiences_catalog.ts +2 -2
  398. package/src/scripts/generate_settings_reference.ts +1 -1
  399. package/src/scripts/git_authorization_hook.ts +22 -1
  400. package/src/scripts/grade_target_readiness.ts +48 -5
  401. package/src/scripts/hook_effect_doctor.ts +261 -0
  402. package/src/scripts/hook_manifest.json +1 -1
  403. package/src/scripts/hook_manifest.yaml +98 -11
  404. package/src/scripts/hooks/block_speaking_inbox_dir.ts +271 -0
  405. package/src/scripts/hooks/block_unauthorized_git.ts +18 -0
  406. package/src/scripts/hooks/concern_registry.ts +4 -0
  407. package/src/scripts/hooks/dispatch_hook.ts +32 -0
  408. package/src/scripts/hooks/journal_record_hook.ts +345 -0
  409. package/src/scripts/hooks/prepush_metadata_sources.sh +110 -0
  410. package/src/scripts/hooks/run_continuation_hook.ts +44 -9
  411. package/src/scripts/hooks/shims/install.sh +61 -0
  412. package/src/scripts/hooks/shims/php +101 -0
  413. package/src/scripts/hooks/suggestion_capture_hook.ts +10 -0
  414. package/src/scripts/hot_context_hook.ts +16 -0
  415. package/src/scripts/install-hooks.sh +20 -5
  416. package/src/scripts/install.ts +17 -35
  417. package/src/scripts/inventory_frontmatter.ts +2 -1
  418. package/src/scripts/lint_adr_sweep_routing.ts +12 -0
  419. package/src/scripts/lint_canonical_terms.ts +604 -0
  420. package/src/scripts/lint_composition_review.ts +619 -0
  421. package/src/scripts/lint_consolidation_lineage.ts +666 -0
  422. package/src/scripts/lint_eval_specs.ts +593 -0
  423. package/src/scripts/lint_hook_manifest.ts +19 -0
  424. package/src/scripts/lint_host_portability.ts +373 -0
  425. package/src/scripts/lint_judge_prompt_expectation.ts +27 -1
  426. package/src/scripts/lint_memory_twin_parity.ts +335 -0
  427. package/src/scripts/lint_metric_consumers.ts +355 -0
  428. package/src/scripts/lint_skill_descriptions.ts +18 -1
  429. package/src/scripts/lint_skill_frontmatter_safety.ts +58 -0
  430. package/src/scripts/lint_skill_link_reach.ts +252 -0
  431. package/src/scripts/lint_skill_trigger_corpus.ts +426 -23
  432. package/src/scripts/lint_ticket_buildable.ts +17 -1
  433. package/src/scripts/mcp_server/consumer_tool_catalog.json +1 -1
  434. package/src/scripts/mcp_server/tool_catalog_source.ts +1 -1
  435. package/src/scripts/measure_docs_dead_links.ts +348 -0
  436. package/src/scripts/measure_host_capture.ts +461 -0
  437. package/src/scripts/migration_status.ts +3 -3
  438. package/src/scripts/model_rule_injection.ts +60 -0
  439. package/src/scripts/orchestration_record.ts +14 -0
  440. package/src/scripts/preamble_byte_census.ts +139 -10
  441. package/src/scripts/prepack-check.mjs +4 -1
  442. package/src/scripts/probe_turnaround.ts +384 -0
  443. package/src/scripts/probe_unblocked_ask.ts +279 -0
  444. package/src/scripts/render_benchmark_md.ts +1 -1
  445. package/src/scripts/rule_activation_census.ts +70 -9
  446. package/src/scripts/rule_backlinks.ts +1 -1
  447. package/src/scripts/run_lifecycle_suite.ts +208 -0
  448. package/src/scripts/run_skill_evals.ts +40 -0
  449. package/src/scripts/schemas/delivery-manifest.schema.json +43 -0
  450. package/src/scripts/schemas/drafting-channels.schema.json +43 -0
  451. package/src/scripts/schemas/evals.schema.json +24 -1
  452. package/src/scripts/schemas/rule.schema.json +52 -1
  453. package/src/scripts/schemas/skill.schema.json +17 -2
  454. package/src/scripts/skill_overlap.ts +7 -1
  455. package/src/scripts/skill_tools/run_block_d_eval.ts +8 -1
  456. package/src/scripts/skill_tools/score_skill_relevance.ts +84 -3
  457. package/src/scripts/skill_tools/suggest_skill_for_task.ts +8 -1
  458. package/src/scripts/skill_usage_report.ts +2 -1
  459. package/src/scripts/stubs_due.ts +211 -0
  460. package/src/scripts/sweep_source_surfaces.ts +678 -0
  461. package/src/scripts/sync_pr_branch.ts +3 -3
  462. package/src/scripts/trigger_eval_grandfather.json +202 -222
  463. package/src/scripts/utilization_report.ts +22 -2
  464. package/src/server/schemas/settings.ts +5 -0
  465. package/src/templates/marketing-copy.yml +1 -1
  466. package/dist/ui/assets/index-Cxgy63kz.js +0 -26
@@ -0,0 +1,335 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * Memory-twin parity — the copies may not drift apart again.
4
+ *
5
+ * `road-to-memory-twin-reconciliation` Phase 3. Seven scripts exist twice, once
6
+ * under `src/scripts/` and once under `src/agent-src/templates/scripts/`, and
7
+ * `package.json:files[]` ships BOTH. Until this gate there was no sync, no drift
8
+ * check and no parity check between them — so they diverged in both directions
9
+ * and the divergence was found by a redundancy audit rather than by CI.
10
+ *
11
+ * ── What it compares ────────────────────────────────────────────────────────
12
+ * NON-COMMENT lines only. Block comments, line comments and blank lines are
13
+ * stripped from both sides before diffing, because the docstring differences
14
+ * between the copies are known-correct: the template's header explains what a
15
+ * consumer installed, the dev copy's explains this repository's own tooling.
16
+ * A gate comparing whole diffs would fire on every unrelated PR — which is
17
+ * exactly the risk this roadmap's own register names at rank 4.
18
+ *
19
+ * ── Two modes, and why not one ──────────────────────────────────────────────
20
+ * `exact` — the non-comment diff must be EMPTY. This is the real check, and a
21
+ * twin is put here whenever it can be driven to zero.
22
+ *
23
+ * `bounded` — a recorded ceiling on non-comment changed lines, shrink-only. It
24
+ * exists for the twins that CANNOT reach zero, and the reason is structural
25
+ * rather than a preference: the installed consumer tree has no `scripts/_lib/`,
26
+ * so a template script may import node built-ins, `yaml` and its own installed
27
+ * siblings — nothing else. Every `keep-duplicated` verdict in the config names
28
+ * which non-installable import forces it.
29
+ *
30
+ * **What `bounded` does NOT claim**, stated because a ceiling reads stronger
31
+ * than it is: it catches GROWTH, not substitution. Replacing one 18-line
32
+ * divergence with a different 18-line divergence passes. That is the honest
33
+ * limit of a count, and it is why every twin that can be `exact` is `exact`.
34
+ *
35
+ * Exit codes: 0 within verdicts · 1 divergence beyond them · 2 config / IO error.
36
+ *
37
+ * Usage:
38
+ * ./scripts-run src/scripts/lint_memory_twin_parity
39
+ * ./scripts-run src/scripts/lint_memory_twin_parity --format json
40
+ * ./scripts-run src/scripts/lint_memory_twin_parity --root <fixture>
41
+ * ./scripts-run src/scripts/lint_memory_twin_parity --self-test
42
+ */
43
+ import * as fs from 'node:fs';
44
+ import * as os from 'node:os';
45
+ import * as path from 'node:path';
46
+ import process from 'node:process';
47
+ import { fileURLToPath, pathToFileURL } from 'node:url';
48
+
49
+ import { parse as parseYaml } from 'yaml';
50
+
51
+ import { GateLedger } from './_lib/gate_ledger.js';
52
+ import { runGateCli, runSelfTest } from './_lib/gate_self_test.js';
53
+ import { DeadScopeError, assertScanned } from './_lib/scan_scope.js';
54
+
55
+ const _HERE = fileURLToPath(import.meta.url);
56
+ const REPO_ROOT = path.resolve(path.dirname(_HERE), '..', '..');
57
+
58
+ export const CONFIG_REL = 'src/config/memory-twin-verdicts.yml';
59
+
60
+ export interface TwinVerdict {
61
+ file: string;
62
+ mode: 'exact' | 'bounded';
63
+ max_changed_lines?: number;
64
+ verdict: string;
65
+ reason: string;
66
+ }
67
+
68
+ export interface TwinConfig {
69
+ dev_root: string;
70
+ template_root: string;
71
+ twins: TwinVerdict[];
72
+ }
73
+
74
+ export function loadConfig(repoRoot: string): TwinConfig {
75
+ const raw = fs.readFileSync(path.join(repoRoot, CONFIG_REL), 'utf8');
76
+ const doc = parseYaml(raw) as Partial<TwinConfig>;
77
+ if (typeof doc.dev_root !== 'string' || typeof doc.template_root !== 'string' || !Array.isArray(doc.twins)) {
78
+ throw new Error(`${CONFIG_REL}: dev_root, template_root and twins are all required`);
79
+ }
80
+ return { dev_root: doc.dev_root, template_root: doc.template_root, twins: doc.twins };
81
+ }
82
+
83
+ /**
84
+ * Strip comments and blank lines.
85
+ *
86
+ * Deliberately textual rather than a parse: the gate must not need a TypeScript
87
+ * compiler to answer "did these two files diverge", and a stripper that is
88
+ * slightly conservative (a `*` continuation line inside a string would be
89
+ * dropped) errs toward reporting FEWER differences, which cannot manufacture a
90
+ * failure. It is the same shape the roadmap's own Phase 1 measurements used.
91
+ */
92
+ export function stripComments(src: string): string[] {
93
+ const noBlocks = src.replace(/\/\*[\s\S]*?\*\//g, '');
94
+ const out: string[] = [];
95
+ for (const line of noBlocks.split('\n')) {
96
+ const t = line.trim();
97
+ if (t === '' || t.startsWith('//') || t.startsWith('*')) continue;
98
+ out.push(line.replace(/\s+$/, ''));
99
+ }
100
+ return out;
101
+ }
102
+
103
+ /**
104
+ * Count changed lines between two stripped files.
105
+ *
106
+ * A multiset difference, not an LCS diff: a line present N times on one side and
107
+ * M on the other contributes |N-M|. That makes the number invariant to
108
+ * reordering, which is what we want — moving a function is not a divergence in
109
+ * behaviour, and a real divergence changes the line CONTENT.
110
+ */
111
+ export function changedLineCount(devLines: readonly string[], tplLines: readonly string[]): number {
112
+ const count = (ls: readonly string[]): Map<string, number> => {
113
+ const m = new Map<string, number>();
114
+ for (const l of ls) m.set(l, (m.get(l) ?? 0) + 1);
115
+ return m;
116
+ };
117
+ const a = count(devLines);
118
+ const b = count(tplLines);
119
+ let delta = 0;
120
+ for (const k of new Set([...a.keys(), ...b.keys()])) {
121
+ delta += Math.abs((a.get(k) ?? 0) - (b.get(k) ?? 0));
122
+ }
123
+ return delta;
124
+ }
125
+
126
+ export interface Finding {
127
+ file: string;
128
+ measured: number;
129
+ allowed: number | 'exact';
130
+ detail: string;
131
+ }
132
+
133
+ export interface Report {
134
+ findings: Finding[];
135
+ scanned: number;
136
+ measurements: { file: string; measured: number; mode: string }[];
137
+ }
138
+
139
+ export function evaluate(repoRoot: string, ledger?: GateLedger): Report {
140
+ const cfg = loadConfig(repoRoot);
141
+ const findings: Finding[] = [];
142
+ const measurements: Report['measurements'] = [];
143
+ ledger?.plan(cfg.twins.map((t) => t.file));
144
+
145
+ for (const twin of cfg.twins) {
146
+ const dev = path.join(repoRoot, cfg.dev_root, twin.file);
147
+ const tpl = path.join(repoRoot, cfg.template_root, twin.file);
148
+ if (!fs.existsSync(dev) || !fs.existsSync(tpl)) {
149
+ // A declared twin that no longer exists on both sides is a CONFIG
150
+ // defect, not a silent skip: the verdict is now describing nothing.
151
+ findings.push({
152
+ file: twin.file,
153
+ measured: -1,
154
+ allowed: twin.mode === 'exact' ? 'exact' : (twin.max_changed_lines ?? 0),
155
+ detail: `declared twin is missing on one side (dev=${fs.existsSync(dev)}, template=${fs.existsSync(tpl)}) — the verdict describes nothing`,
156
+ });
157
+ ledger?.fail(twin.file, 'declared twin missing on one side');
158
+ continue;
159
+ }
160
+ const measured = changedLineCount(
161
+ stripComments(fs.readFileSync(dev, 'utf8')),
162
+ stripComments(fs.readFileSync(tpl, 'utf8')),
163
+ );
164
+ measurements.push({ file: twin.file, measured, mode: twin.mode });
165
+
166
+ if (twin.mode === 'exact') {
167
+ if (measured !== 0) {
168
+ findings.push({
169
+ file: twin.file,
170
+ measured,
171
+ allowed: 'exact',
172
+ detail: `declared EXACT parity but the copies differ by ${measured} non-comment line(s)`,
173
+ });
174
+ ledger?.fail(twin.file, `${measured} line(s) against exact parity`);
175
+ } else {
176
+ ledger?.complete(twin.file);
177
+ }
178
+ continue;
179
+ }
180
+ const cap = twin.max_changed_lines ?? 0;
181
+ if (measured > cap) {
182
+ findings.push({
183
+ file: twin.file,
184
+ measured,
185
+ allowed: cap,
186
+ detail:
187
+ `${measured} non-comment changed line(s) against a recorded ceiling of ${cap} — ` +
188
+ `${measured - cap} new. The ceiling is shrink-only: reconcile the divergence, or ` +
189
+ `record a verdict for it in ${CONFIG_REL}. Raising the number is a defect, not a fix.`,
190
+ });
191
+ ledger?.fail(twin.file, `${measured} > ${cap}`);
192
+ } else {
193
+ ledger?.complete(twin.file);
194
+ }
195
+ }
196
+ return { findings, scanned: cfg.twins.length, measurements };
197
+ }
198
+
199
+ export function selfTest(): number {
200
+ const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'twin-selftest-'));
201
+ const build = (name: string, devBody: string, tplBody: string, cfgTwin: string): string => {
202
+ const root = path.join(tmp, name);
203
+ fs.mkdirSync(path.join(root, 'src', 'scripts'), { recursive: true });
204
+ fs.mkdirSync(path.join(root, 'src', 'agent-src', 'templates', 'scripts'), { recursive: true });
205
+ fs.mkdirSync(path.join(root, 'src', 'config'), { recursive: true });
206
+ fs.writeFileSync(path.join(root, 'src', 'scripts', 'twin.ts'), devBody, 'utf8');
207
+ fs.writeFileSync(path.join(root, 'src', 'agent-src', 'templates', 'scripts', 'twin.ts'), tplBody, 'utf8');
208
+ fs.writeFileSync(
209
+ path.join(root, 'src', 'config', 'memory-twin-verdicts.yml'),
210
+ `schema_version: memory-twin-verdicts-v1\ndev_root: src/scripts\ntemplate_root: src/agent-src/templates/scripts\ntwins:\n${cfgTwin}\n`,
211
+ 'utf8',
212
+ );
213
+ return root;
214
+ };
215
+ const exactTwin = " - file: twin.ts\n mode: exact\n verdict: dev-side-correct\n reason: self-test";
216
+ const run = (root: string): number =>
217
+ runGateCli(REPO_ROOT, 'src/scripts/lint_memory_twin_parity.ts', ['--root', root, '--quiet'], REPO_ROOT);
218
+
219
+ try {
220
+ return runSelfTest({
221
+ gate: 'lint_memory_twin_parity',
222
+ minCases: 4,
223
+ minRejectCases: 2,
224
+ cases: [
225
+ {
226
+ name: 'identical copies pass',
227
+ expect: 'accept',
228
+ run: () => run(build('same', 'const a = 1;\n', 'const a = 1;\n', exactTwin)),
229
+ },
230
+ {
231
+ // The verify clause's green half: a comment-only plant must NOT red.
232
+ name: 'a comment-only divergence passes',
233
+ expect: 'accept',
234
+ run: () =>
235
+ run(build('comment', 'const a = 1;\n', '// template-only note\nconst a = 1;\n', exactTwin)),
236
+ },
237
+ {
238
+ // The verify clause's red half: one behavioural line must red.
239
+ name: 'a one-line behavioural divergence is rejected',
240
+ expect: 'reject',
241
+ run: () => run(build('drift', 'const a = 1;\n', 'const a = 2;\n', exactTwin)),
242
+ },
243
+ {
244
+ name: 'a declared twin missing on one side is rejected',
245
+ expect: 'reject',
246
+ run: () => {
247
+ const root = build('missing', 'const a = 1;\n', 'const a = 1;\n', exactTwin);
248
+ fs.rmSync(path.join(root, 'src', 'agent-src', 'templates', 'scripts', 'twin.ts'));
249
+ return run(root);
250
+ },
251
+ },
252
+ {
253
+ name: 'a bounded twin at its ceiling passes and one line over is rejected',
254
+ expect: 'reject',
255
+ run: () =>
256
+ run(
257
+ build(
258
+ 'bounded',
259
+ 'const a = 1;\nconst b = 2;\n',
260
+ 'const a = 9;\nconst b = 8;\n',
261
+ ' - file: twin.ts\n mode: bounded\n max_changed_lines: 2\n verdict: keep-duplicated\n reason: self-test',
262
+ ),
263
+ ),
264
+ },
265
+ ],
266
+ });
267
+ } finally {
268
+ fs.rmSync(tmp, { recursive: true, force: true });
269
+ }
270
+ }
271
+
272
+ export function main(argv: string[] = process.argv.slice(2)): number {
273
+ if (argv.includes('-h') || argv.includes('--help')) {
274
+ process.stdout.write('usage: lint_memory_twin_parity [--format json] [--root DIR] [--quiet] [--self-test]\n');
275
+ return 0;
276
+ }
277
+ if (argv.includes('--self-test')) return selfTest();
278
+
279
+ const quiet = argv.includes('--quiet');
280
+ const asJson = argv.includes('--format') && argv[argv.indexOf('--format') + 1] === 'json';
281
+ const rootIdx = argv.indexOf('--root');
282
+ const root = rootIdx !== -1 && rootIdx + 1 < argv.length ? path.resolve(argv[rootIdx + 1] as string) : REPO_ROOT;
283
+ const isFixture = root !== REPO_ROOT;
284
+
285
+ let report: Report;
286
+ const ledger = isFixture ? undefined : new GateLedger('lint_memory_twin_parity');
287
+ try {
288
+ report = evaluate(root, ledger);
289
+ } catch (e) {
290
+ process.stderr.write(`❌ lint_memory_twin_parity: ${e instanceof Error ? e.message : String(e)}\n`);
291
+ return 2;
292
+ }
293
+
294
+ try {
295
+ assertScanned({
296
+ gate: 'lint_memory_twin_parity',
297
+ scanned: report.scanned,
298
+ units: 'declared twin(s)',
299
+ roots: [CONFIG_REL],
300
+ });
301
+ } catch (e) {
302
+ if (e instanceof DeadScopeError) {
303
+ process.stderr.write(`❌ ${e.message}\n`);
304
+ return 2;
305
+ }
306
+ throw e;
307
+ }
308
+
309
+ if (asJson) {
310
+ process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
311
+ }
312
+ ledger?.report(quiet ? () => undefined : undefined);
313
+ process.stdout.write(`scanned: ${report.scanned}\n`);
314
+
315
+ if (report.findings.length > 0) {
316
+ for (const f of report.findings) {
317
+ process.stderr.write(` 🔴 ${f.file}: ${f.detail}\n`);
318
+ }
319
+ process.stderr.write(`❌ lint_memory_twin_parity: ${report.findings.length} twin(s) diverged beyond their verdict.\n`);
320
+ return 1;
321
+ }
322
+ if (!quiet) {
323
+ for (const m of report.measurements) {
324
+ process.stdout.write(` · ${m.file.padEnd(26)} ${String(m.measured).padStart(4)} line(s) [${m.mode}]\n`);
325
+ }
326
+ process.stdout.write(`✅ lint_memory_twin_parity: ${report.scanned} twin(s) within their recorded verdicts.\n`);
327
+ }
328
+ return 0;
329
+ }
330
+
331
+ /* c8 ignore start */
332
+ if (process.argv[1] !== undefined && pathToFileURL(path.resolve(process.argv[1])).href === pathToFileURL(_HERE).href) {
333
+ process.exit(main());
334
+ }
335
+ /* c8 ignore stop */
@@ -0,0 +1,355 @@
1
+ /**
2
+ * lint_metric_consumers — a metric with no consumer does not land.
3
+ *
4
+ * `road-to-experience-loop-broadening` step 0.3, whose rule is quoted from the
5
+ * roadmap verbatim: *"A metric with no consumer is telemetry decoration and
6
+ * should not land."* Every entry in `src/config/metric-registry.yml` declares
7
+ * three things, and this gate refuses one that does not:
8
+ *
9
+ * - `consumer` — who reads it;
10
+ * - `decision` — what it changes;
11
+ * - `absent` — what fails without it.
12
+ *
13
+ * ## Why the third field, when the first two look sufficient
14
+ *
15
+ * Because the first two are answerable for a metric nobody needs. "The report
16
+ * reads it" and "it informs the roadmap" are both true of a number whose
17
+ * deletion would change nothing, and that is the number this gate exists to
18
+ * catch. `absent` is the falsifiable one: if the honest answer is *nothing
19
+ * fails*, the entry cannot be written and the metric should not land.
20
+ *
21
+ * The tree's own worked example is the 0.27 % dispatch capture rate — a figure
22
+ * that existed for months before anyone could say what decision it fed.
23
+ *
24
+ * ## What this gate does NOT claim
25
+ *
26
+ * It checks SHAPE, not truth. It refuses a missing, empty, or boilerplate
27
+ * field; it cannot tell a real consumer from a plausible sentence, because that
28
+ * is a review judgement and no parser has it. The honest statement is that this
29
+ * makes the OMISSION impossible, not the answer true — and it is written here
30
+ * rather than left for someone to discover by trusting the gate further than it
31
+ * goes.
32
+ *
33
+ * Exit codes: 0 clean · 1 finding · 2 usage error.
34
+ *
35
+ * A CONFIG error — a missing, unparseable or `metrics:`-less registry — exits
36
+ * **1**, not 2, and the docstring said 2 (R2 finding 6). 1 is right: a registry
37
+ * the gate cannot read is a finding about the tree, which is what this gate
38
+ * reports; 2 is reserved for being invoked wrongly. The prose is corrected
39
+ * rather than the code, because changing the exit code would change what a
40
+ * caller distinguishes.
41
+ */
42
+
43
+ import fs from 'node:fs';
44
+ import os from 'node:os';
45
+ import path from 'node:path';
46
+ import { fileURLToPath, pathToFileURL } from 'node:url';
47
+ import { parse as parseYaml } from 'yaml';
48
+
49
+ import { GateLedger } from './_lib/gate_ledger.js';
50
+ import { runGateCli, runSelfTest, type SelfTestCase } from './_lib/gate_self_test.js';
51
+ import { reportScanned } from './_lib/scan_scope.js';
52
+
53
+ const HERE = path.dirname(fileURLToPath(import.meta.url));
54
+ const REPO_ROOT = path.resolve(HERE, '..', '..');
55
+ const REGISTRY_REL = path.join('src', 'config', 'metric-registry.yml');
56
+ const SELF_REL = path.join('src', 'scripts', 'lint_metric_consumers.ts');
57
+
58
+ /** The three fields that make a metric landable, and the reason each exists. */
59
+ export const REQUIRED_FIELDS = ['consumer', 'decision', 'absent'] as const;
60
+ export type RequiredField = (typeof REQUIRED_FIELDS)[number];
61
+
62
+ /** Below this a field is a placeholder rather than an answer. */
63
+ export const MIN_FIELD_CHARS = 20;
64
+
65
+ /**
66
+ * Phrases that pass a length check and answer nothing.
67
+ *
68
+ * Deliberately short and deliberately literal. A long list would turn this into
69
+ * a vocabulary filter, which is not what the gate is for — these are the four
70
+ * shapes that showed up when the registry was seeded, each of which is a way of
71
+ * writing "I do not know" at sufficient length.
72
+ */
73
+ export const BOILERPLATE = [
74
+ 'tbd',
75
+ 'todo',
76
+ 'n/a',
77
+ 'not applicable',
78
+ 'to be determined',
79
+ 'unknown',
80
+ 'nothing',
81
+ 'see above',
82
+ ] as const;
83
+
84
+ export interface MetricEntry {
85
+ readonly id?: unknown;
86
+ readonly producer?: unknown;
87
+ readonly consumer?: unknown;
88
+ readonly decision?: unknown;
89
+ readonly absent?: unknown;
90
+ }
91
+
92
+ export interface Finding {
93
+ readonly id: string;
94
+ readonly field: string;
95
+ readonly reason: string;
96
+ }
97
+
98
+ function normalise(value: unknown): string {
99
+ return typeof value === 'string' ? value.trim().replace(/\s+/g, ' ') : '';
100
+ }
101
+
102
+ /** Judge one entry. Pure, so the self-test can drive it with literals. */
103
+ export function findingsFor(entry: MetricEntry, index: number): Finding[] {
104
+ const out: Finding[] = [];
105
+ const id = normalise(entry.id) === '' ? `#${String(index + 1)} (no id)` : normalise(entry.id);
106
+
107
+ if (normalise(entry.id) === '') {
108
+ out.push({ id, field: 'id', reason: 'missing — an unnamed metric cannot be cited' });
109
+ }
110
+ if (normalise(entry.producer) === '') {
111
+ out.push({
112
+ id,
113
+ field: 'producer',
114
+ reason: 'missing — without it a reader cannot check the other three against anything',
115
+ });
116
+ }
117
+ for (const field of REQUIRED_FIELDS) {
118
+ const value = normalise(entry[field]);
119
+ if (value === '') {
120
+ out.push({ id, field, reason: 'missing or empty' });
121
+ continue;
122
+ }
123
+ // Matched at a WORD BOUNDARY, not on an exact token or a token plus a
124
+ // space (R2 finding 1). `TBD:` and `TODO:` are the natural writing
125
+ // forms of exactly the placeholder class this refuses, and both were
126
+ // accepted — the filter caught only the one spelling nobody uses.
127
+ const lower = value.toLowerCase();
128
+ if (BOILERPLATE.some((b) => lower === b || new RegExp(`^${b}\\b`).test(lower))) {
129
+ out.push({ id, field, reason: `boilerplate ("${value.slice(0, 40)}") — a placeholder, not an answer` });
130
+ continue;
131
+ }
132
+ if (value.length < MIN_FIELD_CHARS) {
133
+ out.push({
134
+ id,
135
+ field,
136
+ reason:
137
+ `${String(value.length)} chars, below the ${String(MIN_FIELD_CHARS)}-char floor — `
138
+ + 'too short to name a consumer, a decision or a consequence',
139
+ });
140
+ }
141
+ }
142
+ return out;
143
+ }
144
+
145
+ export interface RegistryRead {
146
+ readonly entries: readonly MetricEntry[];
147
+ readonly error: string | null;
148
+ }
149
+
150
+ export function readRegistry(root: string): RegistryRead {
151
+ const target = path.join(root, REGISTRY_REL);
152
+ let raw: string;
153
+ try {
154
+ raw = fs.readFileSync(target, 'utf8');
155
+ } catch {
156
+ return { entries: [], error: `${REGISTRY_REL} is missing` };
157
+ }
158
+ let parsed: unknown;
159
+ try {
160
+ parsed = parseYaml(raw);
161
+ } catch (err) {
162
+ return { entries: [], error: `${REGISTRY_REL} does not parse: ${(err as Error).message}` };
163
+ }
164
+ if (parsed === null || typeof parsed !== 'object') {
165
+ return { entries: [], error: `${REGISTRY_REL} is not a mapping` };
166
+ }
167
+ const metrics = (parsed as { metrics?: unknown }).metrics;
168
+ if (!Array.isArray(metrics)) {
169
+ return { entries: [], error: `${REGISTRY_REL} has no \`metrics:\` list` };
170
+ }
171
+ return { entries: metrics as MetricEntry[], error: null };
172
+ }
173
+
174
+ /* -------------------------------------------------------------------------- */
175
+ /* Self-test */
176
+ /* -------------------------------------------------------------------------- */
177
+
178
+ function selfTest(): number {
179
+ const fixture = (yaml: string): number => {
180
+ const dir = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), 'metric-reg-'));
181
+ try {
182
+ const abs = path.join(dir, REGISTRY_REL);
183
+ fs.mkdirSync(path.dirname(abs), { recursive: true });
184
+ fs.writeFileSync(abs, yaml);
185
+ // No `git init` here (R2 finding 7): this gate reads one YAML file
186
+ // and asks git nothing, so seeding a repo was seven subprocess
187
+ // spawns buying nothing.
188
+ return runGateCli(REPO_ROOT, SELF_REL, ['--root', dir], dir);
189
+ } finally {
190
+ fs.rmSync(dir, { recursive: true, force: true });
191
+ }
192
+ };
193
+
194
+ const complete = `
195
+ metrics:
196
+ - id: a-real-metric
197
+ producer: src/scripts/some_gate.ts
198
+ consumer: the ratchet in check_some_gate, and the maintainer reading its expiry
199
+ decision: whether a change that raises the count may merge at all
200
+ absent: the count rises unnoticed and the debt grows with nothing reporting it
201
+ `;
202
+
203
+ const cases: SelfTestCase[] = [
204
+ {
205
+ name: 'a complete entry is accepted',
206
+ expect: 'accept',
207
+ run: () => fixture(complete),
208
+ },
209
+ {
210
+ name: 'a metric with NO consumer is refused — the rule the gate exists for',
211
+ expect: 'reject',
212
+ run: () =>
213
+ fixture(complete.replace(/ consumer: .*\n/, '')),
214
+ },
215
+ {
216
+ name: 'a metric with no `decision` is refused',
217
+ expect: 'reject',
218
+ run: () => fixture(complete.replace(/ decision: .*\n/, '')),
219
+ },
220
+ {
221
+ name: 'a metric with no `absent` is refused — the falsifiable field',
222
+ expect: 'reject',
223
+ run: () => fixture(complete.replace(/ absent: .*\n/, '')),
224
+ },
225
+ {
226
+ name: 'a BOILERPLATE consumer is refused, not merely a missing one',
227
+ expect: 'reject',
228
+ run: () => fixture(complete.replace(/ consumer: .*\n/, ' consumer: TBD\n')),
229
+ },
230
+ {
231
+ // The value must be short AND not boilerplate, or the boilerplate
232
+ // branch `continue`s past it and MIN_FIELD_CHARS gets no coverage
233
+ // at all — which is what happened with `nothing much` (R2 finding
234
+ // 2), while the gate-coverage row and the roadmap's verify clause
235
+ // both lean on this self-test as the discrimination proof.
236
+ name: 'a too-short field is refused, on the LENGTH floor rather than the boilerplate list',
237
+ expect: 'reject',
238
+ run: () => fixture(complete.replace(/ absent: .*\n/, ' absent: it breaks\n')),
239
+ },
240
+ {
241
+ name: 'an EMPTY registry is refused — a gate that scans nothing exits green',
242
+ expect: 'reject',
243
+ run: () => fixture('metrics: []\n'),
244
+ },
245
+ ];
246
+
247
+ return runSelfTest({
248
+ gate: 'lint_metric_consumers',
249
+ cases,
250
+ minCases: 7,
251
+ minRejectCases: 5,
252
+ });
253
+ }
254
+
255
+ /* -------------------------------------------------------------------------- */
256
+
257
+ export function main(argv: readonly string[] = process.argv.slice(2)): number {
258
+ if (argv.includes('--self-test')) return selfTest();
259
+
260
+ const rootFlag = argv.indexOf('--root');
261
+ if (rootFlag >= 0 && (argv[rootFlag + 1] === undefined || argv[rootFlag + 1]?.startsWith('--'))) {
262
+ process.stderr.write('lint_metric_consumers: --root needs a directory\n');
263
+ return 2;
264
+ }
265
+ const root = rootFlag >= 0 ? (argv[rootFlag + 1] as string) : REPO_ROOT;
266
+ const quiet = argv.includes('--quiet');
267
+
268
+ const read = readRegistry(root);
269
+ if (read.error !== null) {
270
+ process.stderr.write(`❌ lint_metric_consumers: ${read.error}\n`);
271
+ return 1;
272
+ }
273
+
274
+ // Publishes AND asserts in one call. No `allowEmpty` reason: an empty
275
+ // registry is not a clean bill of health here, it is a gate reading nothing
276
+ // — and this gate's whole subject is numbers that exist without a purpose.
277
+ try {
278
+ reportScanned({
279
+ gate: 'lint_metric_consumers',
280
+ scanned: read.entries.length,
281
+ units: 'metric(s)',
282
+ roots: [REGISTRY_REL],
283
+ });
284
+ } catch (err) {
285
+ process.stderr.write(`❌ ${(err as Error).message}\n`);
286
+ return 1;
287
+ }
288
+
289
+ // Per-target accounting (R2 finding 4). A NEW gate adopts a ledger or takes
290
+ // an exemption; this one has a real target population — the registry's
291
+ // entries — so there is nothing to exempt. The plan is built from the index
292
+ // rather than from the id, because an entry MISSING its id is one of the
293
+ // findings, and a ledger keyed on a value that may be absent could not
294
+ // account for exactly the target that failed.
295
+ const ledger = new GateLedger('lint_metric_consumers');
296
+ const targetOf = (i: number): string => `${REGISTRY_REL}#${String(i + 1)}`;
297
+ ledger.plan(read.entries.map((_e, i) => targetOf(i)));
298
+
299
+ const findings: Finding[] = [];
300
+ const seen = new Set<string>();
301
+ read.entries.forEach((entry, i) => {
302
+ // A null or non-object element is a FINDING, not a crash (R2 finding
303
+ // 5). `metrics: [null]` reached `normalise(entry.id)` and threw an
304
+ // unhandled TypeError out of a gate whose job is to report.
305
+ if (entry === null || typeof entry !== 'object' || Array.isArray(entry)) {
306
+ findings.push({
307
+ id: `#${String(i + 1)}`,
308
+ field: '(entry)',
309
+ reason: 'not a mapping — a metric entry must be a key/value block',
310
+ });
311
+ ledger.fail(targetOf(i), 'not a mapping');
312
+ return;
313
+ }
314
+ const before = findings.length;
315
+ findings.push(...findingsFor(entry, i));
316
+ const id = normalise(entry.id);
317
+ if (id !== '' && seen.has(id)) {
318
+ findings.push({ id, field: 'id', reason: 'duplicate — two entries claim the same metric' });
319
+ }
320
+ if (id !== '') seen.add(id);
321
+ if (findings.length > before) ledger.fail(targetOf(i), findings[before]?.reason ?? 'finding');
322
+ else ledger.complete(targetOf(i));
323
+ });
324
+ ledger.report();
325
+
326
+ if (findings.length > 0) {
327
+ process.stderr.write(
328
+ `❌ lint_metric_consumers: ${String(findings.length)} finding(s) in ${REGISTRY_REL}\n`,
329
+ );
330
+ for (const f of findings) {
331
+ process.stderr.write(` · ${f.id} → ${f.field}: ${f.reason}\n`);
332
+ }
333
+ process.stderr.write(
334
+ '\n A metric with no consumer is telemetry decoration and should not land.\n'
335
+ + ' `absent` is the falsifiable field: if nothing fails without the metric,\n'
336
+ + ' the entry cannot honestly be written and the metric should not exist.\n',
337
+ );
338
+ return 1;
339
+ }
340
+
341
+ if (!quiet) {
342
+ process.stdout.write(
343
+ `✅ lint_metric_consumers: ${String(read.entries.length)} metric(s), each naming a `
344
+ + 'consumer, a decision and what fails without it.\n',
345
+ );
346
+ }
347
+ return 0;
348
+ }
349
+
350
+ if (process.argv[1] !== undefined) {
351
+ const invoked = pathToFileURL(path.resolve(process.argv[1])).href;
352
+ if (invoked === import.meta.url) {
353
+ process.exitCode = main();
354
+ }
355
+ }