@event4u/agent-config 14.12.0 → 14.14.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 (521) 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 +118 -155
  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 +145 -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-comment-discipline.md +6 -0
  30. package/dist/agent-src/rules/code-provenance.md +4 -0
  31. package/dist/agent-src/rules/design-fidelity.md +1 -1
  32. package/dist/agent-src/rules/domain-safety-pii.md +4 -0
  33. package/dist/agent-src/rules/domain-safety-retention.md +4 -0
  34. package/dist/agent-src/rules/engineering-safety-floor.md +2 -0
  35. package/dist/agent-src/rules/external-code-graph-interop.md +14 -12
  36. package/dist/agent-src/rules/improve-before-implement.md +6 -8
  37. package/dist/agent-src/rules/laravel-routing.md +1 -1
  38. package/dist/agent-src/rules/lethal-trifecta-guard.md +4 -0
  39. package/dist/agent-src/rules/secret-vcs-guard.md +2 -0
  40. package/dist/agent-src/rules/security-sensitive-stop.md +2 -0
  41. package/dist/agent-src/rules/source-confidentiality.md +12 -34
  42. package/dist/agent-src/rules/symfony-routing.md +1 -1
  43. package/dist/agent-src/rules/token-efficiency.md +14 -0
  44. package/dist/agent-src/rules/tool-safety.md +4 -0
  45. package/dist/agent-src/rules/untrusted-input-defense.md +4 -0
  46. package/dist/agent-src/scripts/archive_completed_roadmaps.ts +252 -8
  47. package/dist/agent-src/scripts/guarded_baseline.ts +343 -0
  48. package/dist/agent-src/scripts/roadmap_set_step.ts +253 -0
  49. package/dist/agent-src/scripts/stub_queue.ts +82 -0
  50. package/dist/agent-src/scripts/stubs_due.ts +516 -0
  51. package/dist/agent-src/scripts/ticket_bundles.ts +84 -0
  52. package/dist/agent-src/scripts/update_roadmap_progress.ts +16 -12
  53. package/dist/agent-src/skills/brand-asset-generation/SKILL.md +17 -4
  54. package/dist/agent-src/skills/brand-audit/SKILL.md +33 -0
  55. package/dist/agent-src/skills/code-intelligence/SKILL.md +57 -0
  56. package/dist/agent-src/skills/database/SKILL.md +8 -0
  57. package/dist/agent-src/skills/dependency-upgrade/SKILL.md +27 -0
  58. package/dist/agent-src/skills/design-review/SKILL.md +1 -1
  59. package/dist/agent-src/skills/doc-coauthoring/SKILL.md +1 -1
  60. package/dist/agent-src/skills/existing-ui-audit/SKILL.md +17 -1
  61. package/dist/agent-src/skills/feature-planning/SKILL.md +9 -2
  62. package/dist/agent-src/skills/frontend-render-security/SKILL.md +44 -0
  63. package/dist/agent-src/skills/git-workflow/SKILL.md +1 -1
  64. package/dist/agent-src/skills/js-library-packaging/scripts/check_package_surface.ts +38 -4
  65. package/dist/agent-src/skills/judge-artifact-completeness/rubrics/ticket-quality-score.json +2 -2
  66. package/dist/agent-src/skills/laravel/SKILL.md +35 -0
  67. package/dist/agent-src/skills/laravel-migration/SKILL.md +57 -3
  68. package/dist/agent-src/skills/monorepo-workspace/SKILL.md +26 -0
  69. package/dist/agent-src/skills/playbook-authoring/SKILL.md +28 -1
  70. package/dist/agent-src/skills/react-shadcn-ui/SKILL.md +1 -1
  71. package/dist/agent-src/skills/skill-writing/references/procedure.md +5 -5
  72. package/dist/agent-src/skills/sql-writing/SKILL.md +7 -1
  73. package/dist/agent-src/skills/standards-from-config/SKILL.md +27 -4
  74. package/dist/agent-src/skills/supply-chain-intake/SKILL.md +11 -4
  75. package/dist/agent-src/skills/symfony-workflow/SKILL.md +35 -0
  76. package/dist/agent-src/skills/test-case-discovery/SKILL.md +85 -0
  77. package/dist/agent-src/skills/test-driven-development/SKILL.md +33 -9
  78. package/dist/agent-src/skills/testing-anti-patterns/SKILL.md +1 -1
  79. package/dist/agent-src/skills/testing-anti-patterns/process-anti-patterns.md +37 -0
  80. package/dist/agent-src/skills/ui-component-architect/SKILL.md +45 -2
  81. package/dist/agent-src/skills/upstream-contribute/SKILL.md +38 -1
  82. package/dist/agent-src/skills/verify-repair-loop/SKILL.md +9 -4
  83. package/dist/agent-src/templates/agents/agent-project-settings.example.yml +1 -1
  84. package/dist/agent-src/templates/roadmaps.md +97 -3
  85. package/dist/agent-src/templates/scripts/check_memory.ts +258 -17
  86. package/dist/agent-src/templates/scripts/check_memory_proposal.ts +9 -3
  87. package/dist/agent-src/templates/scripts/memory_lookup.ts +86 -0
  88. package/dist/agent-src/templates/scripts/memory_signal.ts +44 -0
  89. package/dist/agent-src/templates/scripts/work_engine/directives/backend/implement.ts +90 -0
  90. package/dist/agent-src/templates/scripts/work_engine/directives/backend/index.ts +11 -0
  91. package/dist/agent-src/templates/scripts/work_engine/directives/backend/test.ts +23 -1
  92. package/dist/agent-src/templates/scripts/work_engine/directives/backend/verify.ts +10 -1
  93. package/dist/agent-src/templates/scripts/work_engine/directives/mixed/stitch.ts +5 -1
  94. package/dist/agent-src/templates/scripts/work_engine/stack/runner.ts +102 -14
  95. package/dist/cli/commands/uiAudit.js +124 -5
  96. package/dist/cli/registry.js +2 -0
  97. package/dist/cli-delegate/{chunk-HYWSRNUX.js → chunk-2DK2VJWG.js} +3 -3
  98. package/dist/cli-delegate/{chunk-JCW7ZLHK.js → chunk-4FS4WRXP.js} +5 -0
  99. package/dist/cli-delegate/{chunk-OMIVVCG2.js → chunk-A4QJ6ENT.js} +24 -0
  100. package/dist/cli-delegate/{chunk-JJA2QEWA.js → chunk-AKJGUQNO.js} +466 -285
  101. package/dist/cli-delegate/{chunk-E46KPN6K.js → chunk-BTWUYDGN.js} +424 -363
  102. package/dist/cli-delegate/{chunk-K4FFODTS.js → chunk-ILMUPLGW.js} +1227 -316
  103. package/dist/cli-delegate/{chunk-FF7WAFMR.js → chunk-KPBWFOPZ.js} +5 -1
  104. package/dist/cli-delegate/cmd_adr_effective.js +102 -0
  105. package/dist/cli-delegate/cmd_conformance.js +80 -11
  106. package/dist/cli-delegate/cmd_doctor.js +3 -3
  107. package/dist/cli-delegate/cmd_export.js +3 -2
  108. package/dist/cli-delegate/cmd_fleet.js +2 -2
  109. package/dist/cli-delegate/cmd_handoff.js +1 -1
  110. package/dist/cli-delegate/cmd_preflight.js +2 -2
  111. package/dist/cli-delegate/cmd_prune.js +3 -2
  112. package/dist/cli-delegate/cmd_settings_get.js +1 -1
  113. package/dist/cli-delegate/cmd_settings_migrate.js +3 -2
  114. package/dist/cli-delegate/cmd_settings_set.js +2 -2
  115. package/dist/cli-delegate/cmd_sync.js +3 -2
  116. package/dist/cli-delegate/cmd_uninstall.js +3 -2
  117. package/dist/cli-delegate/cmd_validate.js +3 -2
  118. package/dist/discovery/deprecation-report.md +1 -1
  119. package/dist/discovery/discovery-manifest.json +58 -58
  120. package/dist/discovery/discovery-manifest.json.sha256 +1 -1
  121. package/dist/discovery/discovery-manifest.summary.md +1 -1
  122. package/dist/discovery/dormancy-report.md +1 -1
  123. package/dist/discovery/orphan-report.md +1 -1
  124. package/dist/discovery/packs.json +2 -2
  125. package/dist/discovery/trust-report.md +1 -1
  126. package/dist/discovery/workspaces.json +2 -2
  127. package/dist/hooks/dispatch.js +2900 -1049
  128. package/dist/install/claudeRuleRewrite.js +254 -0
  129. package/dist/install/detect.js +29 -16
  130. package/dist/install/detect_php_shape.js +159 -0
  131. package/dist/install/install.mjs +634 -428
  132. package/dist/mcp/awesome-mcp-servers.row.md +1 -1
  133. package/dist/mcp/mcp-cloudflare-catalogue.json +1 -1
  134. package/dist/mcp/registry-manifest.json +3 -3
  135. package/dist/mcp/server.json +3 -3
  136. package/dist/mcp/server.mjs +4895 -2627
  137. package/dist/server/schemas/settings.js +3 -0
  138. package/dist/ui/assets/index-D0jaCRPM.js +29 -0
  139. package/dist/ui/index.html +1 -1
  140. package/docs/MIGRATION.md +1 -1
  141. package/docs/THIRD-PARTY-NOTICES.md +25 -0
  142. package/docs/contracts/provider-lifecycle.md +1 -1
  143. package/docs/contracts/settings-classes.md +10 -4
  144. package/docs/guidelines/agent-infra/5w2h-analysis.md +3 -3
  145. package/docs/guidelines/agent-infra/agent-interaction-and-decision-quality.md +1 -0
  146. package/docs/guidelines/agent-infra/artifact-drafting-protocol-mechanics.md +58 -3
  147. package/docs/guidelines/agent-infra/ask-when-uncertain-demos.md +2 -2
  148. package/docs/guidelines/agent-infra/asking-and-brevity-examples.md +3 -3
  149. package/docs/guidelines/agent-infra/break-glass-usage.md +7 -7
  150. package/docs/guidelines/agent-infra/carve-out-predicates.md +3 -3
  151. package/docs/guidelines/agent-infra/comparison-matrix.md +1 -1
  152. package/docs/guidelines/agent-infra/critical-thinking.md +4 -4
  153. package/docs/guidelines/agent-infra/direct-answers-demos.md +2 -2
  154. package/docs/guidelines/agent-infra/domain-eval-anti-pattern.md +1 -1
  155. package/docs/guidelines/agent-infra/engineering-memory-data-format.md +3 -3
  156. package/docs/guidelines/agent-infra/existence-question-verdicts.md +80 -0
  157. package/docs/guidelines/agent-infra/first-principles.md +2 -2
  158. package/docs/guidelines/agent-infra/framework-neutrality-patterns.md +2 -2
  159. package/docs/guidelines/agent-infra/guarded-baseline.md +39 -0
  160. package/docs/guidelines/agent-infra/inversion-thinking.md +5 -5
  161. package/docs/guidelines/agent-infra/language-and-tone-examples.md +1 -1
  162. package/docs/guidelines/agent-infra/layered-settings.md +10 -10
  163. package/docs/guidelines/agent-infra/memory-access.md +1 -1
  164. package/docs/guidelines/agent-infra/mental-models.md +3 -3
  165. package/docs/guidelines/agent-infra/reuse-verdict-mechanics.md +71 -0
  166. package/docs/guidelines/agent-infra/review-routing-data-format.md +7 -7
  167. package/docs/guidelines/agent-infra/roadmap-progress-mechanics.md +13 -7
  168. package/docs/guidelines/agent-infra/role-contracts.md +2 -2
  169. package/docs/guidelines/agent-infra/role-mode-router.md +3 -3
  170. package/docs/guidelines/agent-infra/rule-type-governance.md +2 -2
  171. package/docs/guidelines/agent-infra/scqa-framework.md +5 -5
  172. package/docs/guidelines/agent-infra/self-improvement-pipeline.md +6 -6
  173. package/docs/guidelines/agent-infra/simplicity-and-goal-demos.md +2 -2
  174. package/docs/guidelines/agent-infra/six-hats.md +3 -3
  175. package/docs/guidelines/agent-infra/skill-quality-checklist.md +6 -6
  176. package/docs/guidelines/agent-infra/source-confidentiality-mechanics.md +109 -0
  177. package/docs/guidelines/agent-infra/systems-thinking.md +1 -1
  178. package/docs/guidelines/agent-infra/verify-before-complete-demos.md +2 -2
  179. package/docs/guidelines/augment-portability-patterns.md +6 -4
  180. package/docs/guidelines/code-clarity.md +18 -0
  181. package/docs/guidelines/component-oriented-and-oop-development.md +3 -2
  182. package/docs/guidelines/cross-role-handoff.md +2 -2
  183. package/docs/guidelines/design-fidelity-mechanics.md +1 -1
  184. package/docs/guidelines/docs/readme-size-and-splitting.md +3 -3
  185. package/docs/guidelines/php/database.md +90 -7
  186. package/docs/guidelines/prompt-templates.md +7 -7
  187. package/llms.txt +2 -2
  188. package/package.json +5 -2
  189. package/src/agent-src/scripts/archive_completed_roadmaps.ts +252 -8
  190. package/src/agent-src/scripts/guarded_baseline.ts +343 -0
  191. package/src/agent-src/scripts/roadmap_set_step.ts +253 -0
  192. package/src/agent-src/scripts/stub_queue.ts +82 -0
  193. package/src/agent-src/scripts/stubs_due.ts +516 -0
  194. package/src/agent-src/scripts/ticket_bundles.ts +84 -0
  195. package/src/agent-src/scripts/update_roadmap_progress.ts +16 -12
  196. package/src/agent-src/templates/scripts/check_memory.ts +258 -17
  197. package/src/agent-src/templates/scripts/check_memory_proposal.ts +9 -3
  198. package/src/agent-src/templates/scripts/memory_lookup.ts +86 -0
  199. package/src/agent-src/templates/scripts/memory_signal.ts +44 -0
  200. package/src/agent-src/templates/scripts/work_engine/directives/backend/implement.ts +90 -0
  201. package/src/agent-src/templates/scripts/work_engine/directives/backend/index.ts +11 -0
  202. package/src/agent-src/templates/scripts/work_engine/directives/backend/test.ts +23 -1
  203. package/src/agent-src/templates/scripts/work_engine/directives/backend/verify.ts +10 -1
  204. package/src/agent-src/templates/scripts/work_engine/directives/mixed/stitch.ts +5 -1
  205. package/src/agent-src/templates/scripts/work_engine/stack/runner.ts +102 -14
  206. package/src/config/agent-settings.template.yml +32 -3
  207. package/src/config/assurance-capability-registry.json +21 -9
  208. package/src/config/canonical-terms.yml +119 -0
  209. package/src/config/capability-native-outcome-bars.json +81 -0
  210. package/src/config/ci-local-parity.yml +80 -0
  211. package/src/config/drafting-channels.yml +43 -0
  212. package/src/config/estate-count-budget.json +57 -36
  213. package/src/config/evaluator-budgets.json +14 -14
  214. package/src/config/gate-coverage.yml +729 -63
  215. package/src/config/gate-reachability-exemptions.json +21 -0
  216. package/src/config/gate-violation-baselines.json +42 -19
  217. package/src/config/harness-evolution-budget.json +25 -0
  218. package/src/config/lapsed-beta-baseline.json +96 -0
  219. package/src/config/memory-twin-verdicts.yml +129 -0
  220. package/src/config/metric-registry.yml +124 -0
  221. package/src/config/pack-size-budget.json +16 -15
  222. package/src/config/preamble-payload-budget.json +12 -4
  223. package/src/config/profiles/balanced.ini +27 -0
  224. package/src/config/profiles/full.ini +27 -0
  225. package/src/config/profiles/minimal.ini +27 -0
  226. package/src/config/publish-surface.json +9 -2
  227. package/src/config/routing-coverage-seed.json +16 -2
  228. package/src/config/rule-stub-ceilings.json +133 -112
  229. package/src/config/turnaround-budget.json +66 -0
  230. package/src/install/claudeRuleRewrite.ts +288 -0
  231. package/src/install/detect.ts +60 -17
  232. package/src/install/detect_php_shape.ts +191 -0
  233. package/src/scripts/_cli/cmd_doctor.ts +27 -112
  234. package/src/scripts/_dispatch.bash +23 -0
  235. package/src/scripts/_lib/activation_ladder.ts +255 -0
  236. package/src/scripts/_lib/activation_receipt_producer.ts +341 -0
  237. package/src/scripts/_lib/activation_states.ts +88 -0
  238. package/src/scripts/_lib/adherence_detectors.ts +143 -0
  239. package/src/scripts/_lib/adr_frontmatter.ts +26 -0
  240. package/src/scripts/_lib/arith_claims.ts +181 -0
  241. package/src/scripts/_lib/arm_ranking.ts +62 -0
  242. package/src/scripts/_lib/asset_delivery_ledger.ts +303 -0
  243. package/src/scripts/_lib/audit_field_provenance.ts +54 -0
  244. package/src/scripts/_lib/auto_dispatch.ts +32 -3
  245. package/src/scripts/_lib/bench_ab_size_claim.ts +98 -4
  246. package/src/scripts/_lib/benchmark_cache_fields.ts +158 -0
  247. package/src/scripts/_lib/candidate_pair_delta.ts +349 -0
  248. package/src/scripts/_lib/candidate_proposer.ts +402 -0
  249. package/src/scripts/_lib/candidate_record.ts +601 -0
  250. package/src/scripts/_lib/capture_rate.ts +319 -0
  251. package/src/scripts/_lib/carrier_status.ts +118 -0
  252. package/src/scripts/_lib/cascade_stage_enumeration.ts +92 -0
  253. package/src/scripts/_lib/collector_denominator.ts +771 -0
  254. package/src/scripts/_lib/collector_record.ts +340 -0
  255. package/src/scripts/_lib/collector_store.ts +664 -0
  256. package/src/scripts/_lib/collector_supervision.ts +827 -0
  257. package/src/scripts/_lib/concern_estate.ts +77 -0
  258. package/src/scripts/_lib/config_chain.ts +477 -0
  259. package/src/scripts/_lib/config_cost.ts +209 -0
  260. package/src/scripts/_lib/corpus_manifest.ts +726 -0
  261. package/src/scripts/_lib/council_settings_block.ts +6 -0
  262. package/src/scripts/_lib/curator_ops.ts +172 -0
  263. package/src/scripts/_lib/delivery_arm_experiment.ts +282 -0
  264. package/src/scripts/_lib/delivery_criticality.ts +126 -0
  265. package/src/scripts/_lib/doctor_runtime_checks.ts +70 -0
  266. package/src/scripts/_lib/empty_cycles.ts +88 -0
  267. package/src/scripts/_lib/estate_carrier_cases.ts +72 -0
  268. package/src/scripts/_lib/eval_publication.ts +354 -0
  269. package/src/scripts/_lib/evaluation_cascade.ts +452 -0
  270. package/src/scripts/_lib/evaluation_vector.ts +283 -0
  271. package/src/scripts/_lib/evaluator_promotion.ts +225 -0
  272. package/src/scripts/_lib/evidence_basis.ts +118 -0
  273. package/src/scripts/_lib/evolution_roi.ts +562 -0
  274. package/src/scripts/_lib/exec_evidence.ts +8 -0
  275. package/src/scripts/_lib/experience_card.ts +272 -0
  276. package/src/scripts/_lib/experience_report.ts +151 -0
  277. package/src/scripts/_lib/experiment_freeze.ts +112 -0
  278. package/src/scripts/_lib/generated_by.ts +66 -0
  279. package/src/scripts/_lib/harness_evolution_guards.ts +239 -0
  280. package/src/scripts/_lib/hook_effect_probe.ts +141 -0
  281. package/src/scripts/_lib/host_denominator.ts +687 -0
  282. package/src/scripts/_lib/host_projection_reach.ts +157 -0
  283. package/src/scripts/_lib/ignored_blocker.ts +148 -0
  284. package/src/scripts/_lib/install_reach_checks.ts +495 -0
  285. package/src/scripts/_lib/json_duplicate_keys.ts +150 -0
  286. package/src/scripts/_lib/judge_hygiene.ts +228 -0
  287. package/src/scripts/_lib/legacy_boundary_map.ts +228 -0
  288. package/src/scripts/_lib/lexical_shortlist.ts +215 -0
  289. package/src/scripts/_lib/llm_candidate_proposer.ts +461 -0
  290. package/src/scripts/_lib/llm_proposer_transport.ts +153 -0
  291. package/src/scripts/_lib/loop_guards.ts +299 -0
  292. package/src/scripts/_lib/loss_class.ts +129 -0
  293. package/src/scripts/_lib/machine_wake.ts +51 -0
  294. package/src/scripts/_lib/md_prose_lines.ts +177 -0
  295. package/src/scripts/_lib/minimality_tiebreak.ts +168 -0
  296. package/src/scripts/_lib/one_resolver_invariant.ts +449 -0
  297. package/src/scripts/_lib/orchestration_record.ts +253 -5
  298. package/src/scripts/_lib/outcome_envelope.ts +227 -0
  299. package/src/scripts/_lib/outcome_vocabularies.ts +125 -0
  300. package/src/scripts/_lib/paired_stats.ts +245 -0
  301. package/src/scripts/_lib/paired_verdict.ts +226 -0
  302. package/src/scripts/_lib/pathology_archive.ts +393 -0
  303. package/src/scripts/_lib/prefix_stable_surfaces.ts +123 -0
  304. package/src/scripts/_lib/privacy_class.ts +47 -0
  305. package/src/scripts/_lib/promotion_capability.ts +315 -0
  306. package/src/scripts/_lib/promotion_evidence.ts +414 -0
  307. package/src/scripts/_lib/promotion_review.ts +294 -0
  308. package/src/scripts/_lib/prompt_shape.ts +92 -0
  309. package/src/scripts/_lib/regression_neighbourhood.ts +259 -0
  310. package/src/scripts/_lib/release_highlights.ts +70 -1
  311. package/src/scripts/_lib/release_material.ts +125 -2
  312. package/src/scripts/_lib/repeated_failure.ts +105 -0
  313. package/src/scripts/_lib/repo_root.ts +111 -0
  314. package/src/scripts/_lib/review_skipped_record.ts +37 -0
  315. package/src/scripts/_lib/role_split.ts +338 -0
  316. package/src/scripts/_lib/routing_corpus.ts +307 -0
  317. package/src/scripts/_lib/routing_index_input.ts +104 -0
  318. package/src/scripts/_lib/rule_injection.ts +22 -1
  319. package/src/scripts/_lib/runtime_journal.ts +1499 -0
  320. package/src/scripts/_lib/runtime_wiring_checks.ts +430 -0
  321. package/src/scripts/_lib/semantic_noop.ts +167 -0
  322. package/src/scripts/_lib/skill_catalogue.ts +11 -1
  323. package/src/scripts/_lib/source_digest.ts +200 -0
  324. package/src/scripts/_lib/source_redact.ts +260 -0
  325. package/src/scripts/_lib/source_shape.ts +502 -0
  326. package/src/scripts/_lib/source_snapshot_dedup.ts +199 -0
  327. package/src/scripts/_lib/strip_comments.d.mts +8 -0
  328. package/src/scripts/_lib/strip_comments.mjs +65 -0
  329. package/src/scripts/_lib/test_red_state.ts +154 -0
  330. package/src/scripts/_lib/trigger_shift.ts +144 -0
  331. package/src/scripts/_lib/untrusted_content.ts +82 -7
  332. package/src/scripts/activation_receipt.ts +258 -0
  333. package/src/scripts/adoption_report.ts +1 -1
  334. package/src/scripts/adr/evidence_census.ts +1 -1
  335. package/src/scripts/adr/regenerate_index.ts +26 -2
  336. package/src/scripts/adr_cite_check.ts +164 -1
  337. package/src/scripts/ai_council/blind_review.ts +11 -1
  338. package/src/scripts/ai_council/clients.ts +4 -0
  339. package/src/scripts/ai_council/config.ts +3 -0
  340. package/src/scripts/ai_council/consensus.ts +28 -8
  341. package/src/scripts/ai_council/consensus_round.ts +226 -0
  342. package/src/scripts/ai_council/cross_exam.ts +128 -0
  343. package/src/scripts/ai_council/disagreement_signal.ts +338 -0
  344. package/src/scripts/ai_council/explain_route.ts +157 -0
  345. package/src/scripts/ai_council/information_gain.ts +293 -0
  346. package/src/scripts/ai_council/inline_findings.ts +340 -0
  347. package/src/scripts/ai_council/judge_position_bias.ts +197 -0
  348. package/src/scripts/ai_council/leakage_corpus.ts +385 -0
  349. package/src/scripts/ai_council/leakage_patterns.ts +453 -0
  350. package/src/scripts/ai_council/minority_retention.ts +171 -0
  351. package/src/scripts/ai_council/orchestrator.ts +16 -203
  352. package/src/scripts/ai_council/orchestrator_results.ts +23 -3
  353. package/src/scripts/ai_council/pricing.ts +106 -0
  354. package/src/scripts/ai_council/prompts.ts +74 -7
  355. package/src/scripts/ai_council/provider_leakage_bench.ts +327 -0
  356. package/src/scripts/ai_council/py_format.ts +14 -3
  357. package/src/scripts/ai_council/recouncil_guard.ts +293 -0
  358. package/src/scripts/ai_council/recouncil_savings.ts +299 -0
  359. package/src/scripts/ai_council/replay_route.ts +223 -0
  360. package/src/scripts/ai_council/response_record.ts +50 -0
  361. package/src/scripts/ai_council/reviewer_assignment.ts +279 -0
  362. package/src/scripts/ai_council/routing_training_row.ts +197 -0
  363. package/src/scripts/ai_council/synthesis_strategy.ts +159 -0
  364. package/src/scripts/ai_council/topology_bench_manifest.ts +860 -0
  365. package/src/scripts/ai_council/topology_vocabulary.ts +175 -0
  366. package/src/scripts/asset_delivery_ledger.ts +157 -0
  367. package/src/scripts/attest_artifact.ts +193 -0
  368. package/src/scripts/audit_auto_rules.ts +2 -1
  369. package/src/scripts/audit_command_surface.ts +1 -1
  370. package/src/scripts/audit_mcp_tools.ts +2 -2
  371. package/src/scripts/audit_skill_overlap_allowlist.json +6 -1
  372. package/src/scripts/audit_user_type_axis.ts +2 -1
  373. package/src/scripts/bench_ab_clone.ts +215 -10
  374. package/src/scripts/bench_ab_integrity.ts +119 -5
  375. package/src/scripts/bench_ab_v2_stats.ts +40 -213
  376. package/src/scripts/build_archive_index.ts +1 -1
  377. package/src/scripts/build_linear_digest.ts +1 -1
  378. package/src/scripts/build_proof.ts +11 -4
  379. package/src/scripts/build_rule_trigger_matrix.ts +1 -1
  380. package/src/scripts/build_source_digests.ts +157 -0
  381. package/src/scripts/cache_realization_report.ts +103 -0
  382. package/src/scripts/check_adr_frontmatter.ts +131 -10
  383. package/src/scripts/check_benchmark_report_fields.ts +207 -0
  384. package/src/scripts/check_beta_review_markers.ts +235 -6
  385. package/src/scripts/check_bundle_path_leakage.ts +34 -1
  386. package/src/scripts/check_claims.ts +576 -6
  387. package/src/scripts/check_composite_arming.ts +35 -0
  388. package/src/scripts/check_concern_admissions.ts +365 -0
  389. package/src/scripts/check_enforcement_coverage.ts +28 -0
  390. package/src/scripts/check_estate_count.ts +347 -25
  391. package/src/scripts/check_gate_reachability.ts +383 -0
  392. package/src/scripts/check_host_loadability.ts +104 -0
  393. package/src/scripts/check_loss_class_declared.ts +308 -0
  394. package/src/scripts/check_md_language.ts +15 -98
  395. package/src/scripts/check_memory_proposal.ts +18 -11
  396. package/src/scripts/check_no_currency_in_cost_surfaces.ts +225 -0
  397. package/src/scripts/check_no_external_sources.ts +281 -6
  398. package/src/scripts/check_pack_conformance_fixture.ts +11 -1
  399. package/src/scripts/check_pack_size.ts +59 -2
  400. package/src/scripts/check_preamble_payload_budget.ts +114 -3
  401. package/src/scripts/check_prefix_stable_mutation.ts +372 -0
  402. package/src/scripts/check_publish_surface.ts +10 -1
  403. package/src/scripts/check_references.ts +24 -1
  404. package/src/scripts/check_release_highlights.ts +96 -13
  405. package/src/scripts/check_roadmap_trackable.ts +17 -2
  406. package/src/scripts/check_rule_evidence_declaration.ts +316 -0
  407. package/src/scripts/check_rule_projection_integrity.ts +14 -4
  408. package/src/scripts/check_score_contract.ts +26 -3
  409. package/src/scripts/check_static_parity.ts +285 -0
  410. package/src/scripts/check_supervision_claim_atomicity.ts +498 -0
  411. package/src/scripts/check_suppression_hygiene.ts +15 -0
  412. package/src/scripts/ci_settle.ts +39 -2
  413. package/src/scripts/code_graph/build.ts +60 -3
  414. package/src/scripts/code_graph/extract.ts +44 -0
  415. package/src/scripts/code_graph/types.ts +24 -0
  416. package/src/scripts/collector_daemon.ts +714 -0
  417. package/src/scripts/config_cost_report.ts +302 -0
  418. package/src/scripts/consumer_matrix.ts +74 -1
  419. package/src/scripts/corpus_manifest.ts +235 -0
  420. package/src/scripts/cost/budget.mjs +10 -3
  421. package/src/scripts/cost/track.mjs +10 -3
  422. package/src/scripts/council_cli.ts +37 -54
  423. package/src/scripts/dispatch_r2_reviewer.ts +46 -5
  424. package/src/scripts/evolution_lab.ts +1153 -0
  425. package/src/scripts/external_sources_denylist.json +5 -18
  426. package/src/scripts/extract_audit_patterns.ts +21 -0
  427. package/src/scripts/fold_intake.ts +12 -1
  428. package/src/scripts/gen_discovery_baseline.ts +1 -1
  429. package/src/scripts/generate_capabilities_index.ts +12 -5
  430. package/src/scripts/generate_capability_matrix.ts +3 -3
  431. package/src/scripts/generate_command_flows.ts +2 -2
  432. package/src/scripts/generate_cookbook.ts +2 -2
  433. package/src/scripts/generate_index.ts +8 -1
  434. package/src/scripts/generate_knowledge_index.ts +2 -1
  435. package/src/scripts/generate_ownership_matrix.ts +2 -1
  436. package/src/scripts/generate_pack_manifests.ts +10 -2
  437. package/src/scripts/generate_role_experiences_catalog.ts +2 -2
  438. package/src/scripts/generate_settings_reference.ts +1 -1
  439. package/src/scripts/git_authorization_hook.ts +22 -1
  440. package/src/scripts/grade_target_readiness.ts +48 -5
  441. package/src/scripts/hook_effect_doctor.ts +261 -0
  442. package/src/scripts/hook_manifest.json +1 -1
  443. package/src/scripts/hook_manifest.yaml +125 -16
  444. package/src/scripts/hooks/block_speaking_inbox_dir.ts +271 -0
  445. package/src/scripts/hooks/block_unauthorized_git.ts +18 -0
  446. package/src/scripts/hooks/comment_discipline_hook.ts +146 -0
  447. package/src/scripts/hooks/concern_registry.ts +6 -0
  448. package/src/scripts/hooks/dispatch_hook.ts +32 -0
  449. package/src/scripts/hooks/journal_record_hook.ts +345 -0
  450. package/src/scripts/hooks/prepush_metadata_sources.sh +110 -0
  451. package/src/scripts/hooks/run_continuation_hook.ts +44 -9
  452. package/src/scripts/hooks/shims/install.sh +61 -0
  453. package/src/scripts/hooks/shims/php +101 -0
  454. package/src/scripts/hooks/suggestion_capture_hook.ts +10 -0
  455. package/src/scripts/hooks/turn_end_gate_hook.ts +9 -1
  456. package/src/scripts/hot_context_hook.ts +16 -0
  457. package/src/scripts/install-hooks.sh +20 -5
  458. package/src/scripts/install.ts +17 -35
  459. package/src/scripts/inventory_frontmatter.ts +2 -1
  460. package/src/scripts/language_mirror_hook.ts +14 -2
  461. package/src/scripts/lint_adr_sweep_routing.ts +12 -0
  462. package/src/scripts/lint_canonical_terms.ts +604 -0
  463. package/src/scripts/lint_carrier_integrity.ts +820 -0
  464. package/src/scripts/lint_code_comments.ts +643 -0
  465. package/src/scripts/lint_composition_review.ts +619 -0
  466. package/src/scripts/lint_consolidation_lineage.ts +666 -0
  467. package/src/scripts/lint_eval_specs.ts +593 -0
  468. package/src/scripts/lint_hook_manifest.ts +19 -0
  469. package/src/scripts/lint_host_portability.ts +373 -0
  470. package/src/scripts/lint_judge_prompt_expectation.ts +27 -1
  471. package/src/scripts/lint_memory_twin_parity.ts +335 -0
  472. package/src/scripts/lint_metric_consumers.ts +355 -0
  473. package/src/scripts/lint_plan_risk_register.ts +39 -19
  474. package/src/scripts/lint_promotion_paths.ts +779 -0
  475. package/src/scripts/lint_skill_descriptions.ts +18 -1
  476. package/src/scripts/lint_skill_frontmatter_safety.ts +58 -0
  477. package/src/scripts/lint_skill_link_reach.ts +252 -0
  478. package/src/scripts/lint_skill_trigger_corpus.ts +426 -23
  479. package/src/scripts/lint_ticket_buildable.ts +17 -1
  480. package/src/scripts/llm_propose.ts +256 -0
  481. package/src/scripts/mcp_server/consumer_tool_catalog.json +1 -1
  482. package/src/scripts/mcp_server/tool_catalog_source.ts +1 -1
  483. package/src/scripts/measure_delivery_sets.ts +361 -0
  484. package/src/scripts/measure_docs_dead_links.ts +348 -0
  485. package/src/scripts/measure_host_capture.ts +461 -0
  486. package/src/scripts/measure_routing_signal.ts +307 -0
  487. package/src/scripts/migration_status.ts +3 -3
  488. package/src/scripts/model_rule_injection.ts +76 -0
  489. package/src/scripts/orchestration_record.ts +14 -0
  490. package/src/scripts/preamble_byte_census.ts +139 -10
  491. package/src/scripts/prepack-check.mjs +4 -1
  492. package/src/scripts/probe_council_retention.ts +495 -0
  493. package/src/scripts/probe_turnaround.ts +384 -0
  494. package/src/scripts/probe_unblocked_ask.ts +279 -0
  495. package/src/scripts/release.ts +21 -37
  496. package/src/scripts/release_drill.ts +143 -0
  497. package/src/scripts/release_env.ts +35 -0
  498. package/src/scripts/release_publication.ts +84 -0
  499. package/src/scripts/render_benchmark_md.ts +1 -1
  500. package/src/scripts/rule_activation_census.ts +70 -9
  501. package/src/scripts/rule_backlinks.ts +1 -1
  502. package/src/scripts/run_lifecycle_suite.ts +208 -0
  503. package/src/scripts/run_skill_evals.ts +40 -0
  504. package/src/scripts/schemas/delivery-manifest.schema.json +43 -0
  505. package/src/scripts/schemas/drafting-channels.schema.json +43 -0
  506. package/src/scripts/schemas/evals.schema.json +24 -1
  507. package/src/scripts/schemas/rule.schema.json +52 -1
  508. package/src/scripts/schemas/skill.schema.json +17 -2
  509. package/src/scripts/skill_overlap.ts +7 -1
  510. package/src/scripts/skill_tools/run_block_d_eval.ts +8 -1
  511. package/src/scripts/skill_tools/score_skill_relevance.ts +84 -3
  512. package/src/scripts/skill_tools/suggest_skill_for_task.ts +8 -1
  513. package/src/scripts/skill_usage_report.ts +2 -1
  514. package/src/scripts/stubs_due.ts +211 -0
  515. package/src/scripts/sweep_source_surfaces.ts +678 -0
  516. package/src/scripts/sync_pr_branch.ts +3 -3
  517. package/src/scripts/trigger_eval_grandfather.json +202 -222
  518. package/src/scripts/utilization_report.ts +22 -2
  519. package/src/server/schemas/settings.ts +5 -0
  520. package/src/templates/marketing-copy.yml +1 -1
  521. package/dist/ui/assets/index-Cxgy63kz.js +0 -26
@@ -0,0 +1,771 @@
1
+ /**
2
+ * collector_denominator — the capture rate's DENOMINATOR, and the spool the
3
+ * numerator is drained from.
4
+ *
5
+ * `road-to-supervised-telemetry-collector` step 1.2 item 1, restated as a
6
+ * constraint on this module: *"the denominator must be produced by a writer
7
+ * that cannot fail in the same way the numerator does; a collector counting its
8
+ * own opportunities is the failure `road-to-journal-host-capture-measurement`
9
+ * exists for."* This is that independent writer, and it also discharges the
10
+ * received item merged into Phase 4 by the AI council on 2026-08-29 — a
11
+ * per-event dispatch counter with no payload and no free-form field, written on
12
+ * hook invocation only.
13
+ *
14
+ * ## Why two writers and not one
15
+ *
16
+ * A collector that counts both halves cannot report its own death. If the
17
+ * daemon is not running, a self-counting instrument records zero opportunities
18
+ * and zero captures, and 0/0 reads as "nothing happened" rather than as "the
19
+ * observer was down". Here the *hook process* writes the denominator
20
+ * synchronously, in-process, with no daemon involved — so a dead collector
21
+ * produces a denominator that keeps climbing and a numerator that does not, and
22
+ * the ratio falls, which is the true statement.
23
+ *
24
+ * The two writers therefore fail differently ON PURPOSE. The denominator fails
25
+ * only if the hook itself does not run; the numerator fails if the daemon is
26
+ * absent, wedged, budget-stopped, opted out, or refusing invalid records.
27
+ *
28
+ * ## What that property does NOT survive: a restart
29
+ *
30
+ * R2 round-4 finding 6, and the correction is to the CLAIM rather than to the
31
+ * code. Captures are spooled to disk while the daemon is down, and the next
32
+ * start replays the whole spool with each record's ORIGINAL `occurred_on` — so
33
+ * the historical rate is repaired retroactively and the outage becomes
34
+ * invisible in the very window the design says would expose it. The two writers
35
+ * fail differently *while the daemon stays down*, not across a restart.
36
+ *
37
+ * That is the right behaviour and the wrong sentence. Dropping the spool on
38
+ * restart would lose real captures to make a graph honest; re-dating them to
39
+ * the drain would move a dispatch that happened on Tuesday into Thursday's
40
+ * denominator. The spool is bounded ({@link SPOOL_MAX_BYTES}) so an unbounded
41
+ * outage still loses records rather than replaying forever, and the reading a
42
+ * human takes from a repaired window is the one this paragraph exists to
43
+ * qualify: a rate that recovered may mean capture recovered, or may mean a
44
+ * daemon came back and flushed. `heartbeat.json`'s three-valued liveness is
45
+ * what distinguishes them, and it is why that file is a state rather than a
46
+ * boolean.
47
+ *
48
+ * ## Never throws, and that is a contract rather than defensive habit
49
+ *
50
+ * `docs/contracts/resident-process-floors.md` § 1 permits observation only, and
51
+ * § 2 Q2 requires that a dispatch the collector cannot serve is DEGRADED, never
52
+ * blocked. A denominator write that can raise would make the observer able to
53
+ * fail a dispatch — which is precisely the falsifiable form § 1 states: *if the
54
+ * module were killed, every dispatch would resolve identically*. Every export
55
+ * here returns a boolean instead of raising, and the boolean is for tests and
56
+ * for the daemon's own diagnostics, never for the dispatch path.
57
+ *
58
+ * ## What is written
59
+ *
60
+ * One tab-separated line per opportunity: UTC **date**, event, platform. No
61
+ * payload, no free-form field, no path, no identifier — the same
62
+ * exclusion-by-construction `collector_record` argues for, applied to a file
63
+ * whose format cannot express anything else. A date rather than a timestamp,
64
+ * for the fingerprinting reason `collector_record.FIELD_PURPOSE` states about
65
+ * `occurred_on`.
66
+ */
67
+
68
+ import crypto from 'node:crypto';
69
+ import fs from 'node:fs';
70
+ import path from 'node:path';
71
+
72
+ import {
73
+ COLLECTOR_EVENTS,
74
+ COLLECTOR_PLATFORMS,
75
+ COLLECTOR_SCHEMA_VERSION,
76
+ type CollectorEvent,
77
+ type CollectorPlatform,
78
+ } from './collector_record.js';
79
+ import { isOptedOut, resolveCollectorStore, RETENTION_DAYS } from './collector_store.js';
80
+
81
+ /**
82
+ * The collector's own version, recorded on every captured record.
83
+ *
84
+ * A literal rather than a read of `package.json`: the record contract calls this
85
+ * the version of the COLLECTOR, and tying it to the package version would make
86
+ * every release look like a collector change to anyone diffing the store.
87
+ */
88
+ export const COLLECTOR_VERSION = '1.0.0';
89
+
90
+ /** Monotonic per-process counters, declared above their first use — the same
91
+ * rule `UUID_RE` was moved for, applied to the other binding added in the same
92
+ * change (R2 round-5 finding 11). */
93
+ let pruneSequence = 0;
94
+
95
+ /** Append-only opportunity log. One short line per dispatch. */
96
+ export const DENOMINATOR_FILE_NAME = 'opportunities.log';
97
+ /** Where hook processes hand records to the daemon. */
98
+ export const SPOOL_DIR_NAME = 'spool';
99
+ /** The file hook processes append to. Renamed away by the daemon to drain it. */
100
+ export const SPOOL_PENDING_NAME = 'pending.jsonl';
101
+
102
+ /**
103
+ * Opt-IN marker. **Absent means off**, which is what "default-off" has to mean
104
+ * to be a property rather than a hope: a fresh install has no marker, so there
105
+ * is no configuration state in which the collector starts by accident.
106
+ */
107
+ export const ENABLED_MARKER_NAME = 'ENABLED';
108
+
109
+ export function denominatorPath(userRoot?: string): string {
110
+ return path.join(resolveCollectorStore(userRoot).root, DENOMINATOR_FILE_NAME);
111
+ }
112
+
113
+ export function spoolDir(userRoot?: string): string {
114
+ return path.join(resolveCollectorStore(userRoot).root, SPOOL_DIR_NAME);
115
+ }
116
+
117
+ export function spoolPendingPath(userRoot?: string): string {
118
+ return path.join(spoolDir(userRoot), SPOOL_PENDING_NAME);
119
+ }
120
+
121
+ export function enabledMarkerPath(userRoot?: string): string {
122
+ return path.join(resolveCollectorStore(userRoot).root, ENABLED_MARKER_NAME);
123
+ }
124
+
125
+ /**
126
+ * Default-off, expressed as the absence of a file.
127
+ *
128
+ * Opt-out still wins over opt-in: a user who has opted out and then somehow
129
+ * acquires an ENABLED marker is opted out. The order matters because the two
130
+ * markers answer different questions — "may this machine capture at all" and
131
+ * "has this user asked for the collector" — and the privacy answer outranks.
132
+ */
133
+ export function isCollectorEnabled(userRoot?: string): boolean {
134
+ if (isOptedOut(userRoot)) return false;
135
+ // The KILL SWITCH stops collection, not merely the daemon (R2 round-5
136
+ // finding 1). Nothing on the dispatch path read it: with `STOP` in place the
137
+ // hook kept appending opportunities and kept spooling captures, and
138
+ // `drainOnce` replays that spool with each record's original `occurred_on`
139
+ // — so the window an operator believed was stopped was captured
140
+ // retroactively the moment the switch came off. That contradicted two
141
+ // shipped statements outright: the operator page's "Nothing is captured"
142
+ // and `DISPATCH_MODE_CONTRACT`'s "not a second, quieter writer".
143
+ //
144
+ // It belongs HERE rather than at the call site for the reason the
145
+ // self-observation exclusion moved here: a guard in the caller's control
146
+ // flow is one an alias or a refactor can remove without the module noticing.
147
+ if (fs.existsSync(killSwitchPathLocal(userRoot))) return false;
148
+ return fs.existsSync(enabledMarkerPath(userRoot));
149
+ }
150
+
151
+ /**
152
+ * The kill-switch marker path.
153
+ *
154
+ * Duplicated from `collector_supervision.killSwitchPath` rather than imported,
155
+ * and the reason is the one thing this module may not do: importing that module
156
+ * would make `collector_denominator` — which the dispatch path loads on EVERY
157
+ * hook invocation — pull in the supervision surface, its heartbeat handling and
158
+ * its process signalling. The literal is one filename, the two are pinned equal
159
+ * by `tests/scripts/collector_self_observation.test.ts`, and a shared constants
160
+ * module for a single string is a third file to keep in sync.
161
+ */
162
+ function killSwitchPathLocal(userRoot?: string): string {
163
+ return path.join(resolveCollectorStore(userRoot).root, 'STOP');
164
+ }
165
+
166
+ /** Create the opt-in marker. Idempotent. */
167
+ export function enableCollector(userRoot?: string): void {
168
+ const target = enabledMarkerPath(userRoot);
169
+ fs.mkdirSync(path.dirname(target), { recursive: true });
170
+ fs.writeFileSync(
171
+ target,
172
+ 'The collector is enabled for this user. Delete this file to return to default-off.\n',
173
+ );
174
+ }
175
+
176
+ /** Remove the opt-in marker. Idempotent. */
177
+ export function disableCollector(userRoot?: string): void {
178
+ fs.rmSync(enabledMarkerPath(userRoot), { force: true });
179
+ }
180
+
181
+ /**
182
+ * The one option both writers take.
183
+ *
184
+ * `includeSelfObserved` is a TEST SEAM and is named as one. The exclusion sits
185
+ * inside the writers rather than at the call site, because putting it in the
186
+ * dispatcher's control flow made both `check_static_parity` runs skip the calls
187
+ * entirely and the comparison vacuous (R2 round-4 finding 3). But the same
188
+ * placement means a test — which always runs under `VITEST` — cannot exercise
189
+ * the write path at all without saying so explicitly. Nothing in `src/` passes
190
+ * it, and `tests/scripts/collector_vocabulary_parity.test.ts` asserts the
191
+ * default still excludes.
192
+ */
193
+ export interface WriterOptions {
194
+ readonly includeSelfObserved?: boolean;
195
+ }
196
+
197
+ /** The UTC date, `YYYY-MM-DD`. Never a time. */
198
+ export function utcDate(at: number = Date.now()): string {
199
+ return new Date(at).toISOString().slice(0, 10);
200
+ }
201
+
202
+ /**
203
+ * Record one dispatch opportunity. Returns whether the line landed.
204
+ *
205
+ * The append is a single `appendFileSync` of a line well under `PIPE_BUF`, so
206
+ * concurrent hook processes interleave whole lines rather than fragments. That
207
+ * is the reason the format is one flat line and not a structure that would need
208
+ * a lock.
209
+ *
210
+ * **Gated on the opt-in MARKER, never on the daemon.** That distinction is the
211
+ * whole independence argument and it is easy to lose: the marker is a file the
212
+ * user created, the daemon is a process that may be absent, wedged,
213
+ * budget-stopped or dead. Counting while the marker exists means a dead daemon
214
+ * produces a climbing denominator and a flat numerator — the capture rate falls
215
+ * and says so. Counting while the DAEMON is up would make the instrument
216
+ * self-reporting and 0/0-blind, which is the failure this module exists for.
217
+ *
218
+ * Gating on the marker at all is a cost decision, and it is the reason a
219
+ * default-off install writes nothing on any hook invocation: **two `existsSync`
220
+ * calls and a return per writer, so FOUR per dispatch once both are wired** — the opt-out probe first, then the ENABLED marker, in
221
+ * that order because the privacy answer outranks. (The first draft of this
222
+ * paragraph said "one `stat`", counting only the marker and forgetting the
223
+ * opt-out check immediately above it — R2 finding 14.) An earlier draft wrote
224
+ * unconditionally so that the ratio would exist for users who never opted in —
225
+ * which is a measurement nobody consented to, paid for by every dispatch on
226
+ * every machine.
227
+ */
228
+ export function recordOpportunity(
229
+ event: CollectorEvent | string,
230
+ platform: CollectorPlatform | string,
231
+ userRoot?: string,
232
+ at: number = Date.now(),
233
+ opts: WriterOptions = {},
234
+ ): boolean {
235
+ try {
236
+ if (opts.includeSelfObserved !== true && isSelfObservation()) return false;
237
+ if (!isCollectorEnabled(userRoot)) return false;
238
+ if (!(COLLECTOR_EVENTS as readonly string[]).includes(event)) return false;
239
+ if (!(COLLECTOR_PLATFORMS as readonly string[]).includes(platform)) return false;
240
+ const target = denominatorPath(userRoot);
241
+ fs.mkdirSync(path.dirname(target), { recursive: true });
242
+ fs.appendFileSync(target, `${utcDate(at)}\t${event}\t${platform}\n`);
243
+ return true;
244
+ } catch {
245
+ // Observation-only: a failed observation is never a failed dispatch.
246
+ return false;
247
+ }
248
+ }
249
+
250
+ export interface DenominatorReading {
251
+ readonly total: number;
252
+ readonly byEvent: Readonly<Record<string, number>>;
253
+ /** Lines the parser refused. A silent skip would understate the denominator. */
254
+ readonly malformed: number;
255
+ /** Earliest and latest UTC date seen, or null on an empty read. */
256
+ readonly firstDate: string | null;
257
+ readonly lastDate: string | null;
258
+ }
259
+
260
+ export interface ReadWindow {
261
+ /** Inclusive lower bound, `YYYY-MM-DD`. Lines before it are not counted. */
262
+ readonly since?: string;
263
+ /** Inclusive upper bound, `YYYY-MM-DD`. */
264
+ readonly until?: string;
265
+ }
266
+
267
+ const DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
268
+
269
+ /**
270
+ * Read the opportunity log. Absent file reads as zero, not as an error.
271
+ *
272
+ * **The window is a parameter now, and its absence was a real defect** (R2
273
+ * round-2 finding 5). Every line carries a UTC date and this function never
274
+ * parsed it, returning a LIFETIME total — while the numerator side is pruned to
275
+ * {@link RETENTION_DAYS} on every daemon iteration. The two halves of the ratio
276
+ * were therefore computed over different time bases, and once the store began
277
+ * pruning, the measured capture rate would have fallen monotonically toward
278
+ * zero regardless of real capture. That is precisely the failure the two-writer
279
+ * design exists to make impossible, arriving through the back door.
280
+ *
281
+ * A date that does not parse is `malformed`, not silently skipped: an
282
+ * understated denominator biases the rate UPWARD.
283
+ */
284
+ export function readOpportunities(userRoot?: string, window: ReadWindow = {}): DenominatorReading {
285
+ let raw = '';
286
+ try {
287
+ raw = fs.readFileSync(denominatorPath(userRoot), 'utf8');
288
+ } catch {
289
+ return Object.freeze({
290
+ total: 0,
291
+ byEvent: Object.freeze({}),
292
+ malformed: 0,
293
+ firstDate: null,
294
+ lastDate: null,
295
+ });
296
+ }
297
+ const byEvent: Record<string, number> = {};
298
+ let total = 0;
299
+ let malformed = 0;
300
+ let firstDate: string | null = null;
301
+ let lastDate: string | null = null;
302
+ for (const line of raw.split('\n')) {
303
+ if (line.length === 0) continue;
304
+ const parts = line.split('\t');
305
+ if (parts.length !== 3 || !(COLLECTOR_EVENTS as readonly string[]).includes(parts[1] ?? '')) {
306
+ malformed += 1;
307
+ continue;
308
+ }
309
+ const date = parts[0] as string;
310
+ if (!DATE_RE.test(date)) {
311
+ malformed += 1;
312
+ continue;
313
+ }
314
+ if (window.since !== undefined && date < window.since) continue;
315
+ if (window.until !== undefined && date > window.until) continue;
316
+ const event = parts[1] as string;
317
+ byEvent[event] = (byEvent[event] ?? 0) + 1;
318
+ total += 1;
319
+ if (firstDate === null || date < firstDate) firstDate = date;
320
+ if (lastDate === null || date > lastDate) lastDate = date;
321
+ }
322
+ return Object.freeze({
323
+ total,
324
+ byEvent: Object.freeze(byEvent),
325
+ malformed,
326
+ firstDate,
327
+ lastDate,
328
+ });
329
+ }
330
+
331
+ /**
332
+ * Drop opportunity lines older than the retention horizon, rewriting the log.
333
+ *
334
+ * The denominator's half of what `pruneOlderThan` does for the store, and it
335
+ * exists for two reasons that are easy to conflate. The ratio one: both sides
336
+ * must age out on the same clock or the rate drifts by construction. The
337
+ * resource one: nothing bounded this file, and it lives inside the directory
338
+ * `RESOURCE_BUDGETS.disk_bytes` puts a ceiling on — a ceiling whose breach
339
+ * STOPS the collector. An unbounded log in a budgeted directory is a slow
340
+ * self-shutdown.
341
+ *
342
+ * Whole-file rewrite through a temp and a rename, so a crash mid-prune leaves
343
+ * either the old file or the new one and never a truncated log. Returns how
344
+ * many lines were dropped.
345
+ */
346
+ export function pruneOpportunitiesOlderThan(
347
+ userRoot: string | undefined,
348
+ today: string,
349
+ days: number = RETENTION_DAYS,
350
+ ): number {
351
+ const target = denominatorPath(userRoot);
352
+ // The on-disk size BEFORE the read, not after the filter (R2 round-5
353
+ // finding 2). Taking it afterwards made the lost-append window the WHOLE
354
+ // filter — a line appended between the read and the `statSync` was inside
355
+ // `consumed` (so excluded from the carried tail) and absent from `raw` (so
356
+ // excluded from the kept set), i.e. silently dropped — while the comment
357
+ // beneath claimed the window was microseconds. The direction is the
358
+ // dangerous one: an understated denominator biases the rate UPWARD.
359
+ let consumed = 0;
360
+ let raw = '';
361
+ try {
362
+ consumed = fs.statSync(target).size;
363
+ raw = fs.readFileSync(target, 'utf8');
364
+ } catch {
365
+ return 0;
366
+ }
367
+ const cutoff = new Date(Date.parse(`${today}T00:00:00Z`) - days * 86_400_000)
368
+ .toISOString()
369
+ .slice(0, 10);
370
+ const lines = raw.split('\n').filter((l) => l.length > 0);
371
+ const kept = lines.filter((l) => {
372
+ const date = l.split('\t')[0] ?? '';
373
+ // A line whose date does not parse is KEPT: it is already counted as
374
+ // malformed by the reader, and deleting what you cannot classify is how
375
+ // a prune quietly becomes a truncation.
376
+ if (!DATE_RE.test(date)) return true;
377
+ return date >= cutoff;
378
+ });
379
+ if (kept.length === lines.length) return 0;
380
+
381
+ // CARRY THE TAIL (R2 round-3 finding 5). Hook processes append to this file
382
+ // from other processes while the prune runs, and a plain read-filter-rename
383
+ // drops everything written into the old inode inside that window. The
384
+ // direction is the dangerous one and this module states it three functions
385
+ // earlier: an understated denominator biases the rate UPWARD, toward making
386
+ // the target look met.
387
+ //
388
+ // So: remember how many bytes we read, and before the rename, append
389
+ // whatever arrived after that offset. A line that lands between the tail
390
+ // read and the rename is still lost — the window is now microseconds rather
391
+ // than the whole filter, and closing it completely needs a lock the
392
+ // append path deliberately does not take.
393
+ // `consumed` is the on-disk size taken BEFORE the read (see above): the
394
+ // stat, not `Buffer.byteLength(raw)`, because that re-encode round-trips
395
+ // through U+FFFD for any invalid byte and misaligns the carried tail
396
+ // (R2 round-4 finding 7).
397
+ let tail = '';
398
+ try {
399
+ const fd = fs.openSync(target, 'r');
400
+ try {
401
+ const size = fs.fstatSync(fd).size;
402
+ if (size > consumed) {
403
+ const buf = Buffer.alloc(size - consumed);
404
+ fs.readSync(fd, buf, 0, buf.length, consumed);
405
+ tail = buf.toString('utf8');
406
+ }
407
+ } finally {
408
+ fs.closeSync(fd);
409
+ }
410
+ } catch {
411
+ /* the file vanished; the rename below re-creates it from `kept` */
412
+ }
413
+
414
+ // A unique temp name, for the reason `claimSpool` was given a counter: the
415
+ // function is exported and driven directly by tests, and two concurrent
416
+ // prunes sharing one staging path clobber each other.
417
+ pruneSequence += 1;
418
+ const tmp = `${target}.${String(process.pid)}.${String(pruneSequence)}.tmp`;
419
+ const body = kept.length === 0 ? '' : `${kept.join('\n')}\n`;
420
+ fs.writeFileSync(tmp, `${body}${tail}`);
421
+ fs.renameSync(tmp, target);
422
+ return lines.length - kept.length;
423
+ }
424
+
425
+ /**
426
+ * Is this dispatch the package observing ITSELF?
427
+ *
428
+ * Metric definition item 3 excludes *"dispatches in the package's own test
429
+ * suite and CI (self-observation)"*, and nothing implemented it (R2 round-3
430
+ * finding 3): on an opted-in developer machine every dispatcher test that
431
+ * reached `main` appended to the real `~/.event4u/agent-config` opportunity
432
+ * log. Those dispatches spool no capture, so they biased the measured rate
433
+ * DOWN.
434
+ *
435
+ * Three signals, all set by the RUNNER rather than by this package, so none can
436
+ * be spoofed by the thing being measured: vitest exports `VITEST`, every CI
437
+ * provider exports `CI`, and `NODE_ENV=test` is the conventional third. A
438
+ * consumer's own CI is excluded too, and that is correct — a dispatch from a
439
+ * pipeline is not a dispatch from a session, and the metric is about sessions.
440
+ *
441
+ * It lives here rather than in the dispatcher because it is a statement about
442
+ * the METRIC, not about hook dispatch; the dispatcher merely asks.
443
+ */
444
+ export function isSelfObservation(env: NodeJS.ProcessEnv = process.env): boolean {
445
+ return env.VITEST !== undefined || env.CI !== undefined || env.NODE_ENV === 'test';
446
+ }
447
+
448
+ /** Where the stable per-machine id lives. Generated once, never derived from a host fact. */
449
+ export const MACHINE_ID_NAME = 'machine-id';
450
+ /** Where the per-episode sequence counter lives, keyed by episode. */
451
+ export const EPISODE_DIR_NAME = 'episodes';
452
+
453
+ /** Declared ABOVE its first use: `machineId` reads it, and a TDZ ReferenceError
454
+ * inside a function whose whole contract is never to throw is a defect waiting
455
+ * for a re-order (R2 round-4 finding 11). */
456
+ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
457
+
458
+ export function machineIdPath(userRoot?: string): string {
459
+ return path.join(resolveCollectorStore(userRoot).root, MACHINE_ID_NAME);
460
+ }
461
+
462
+ /**
463
+ * The machine id: read it, or mint one.
464
+ *
465
+ * `FIELD_PURPOSE` requires a LOCALLY GENERATED random UUID and explicitly not a
466
+ * derived one — a hash of a hostname or a username is a pseudonym for the thing
467
+ * it derives from, which is the leak class the whole schema is built against.
468
+ * So this is `randomUUID()` and nothing else, persisted so it is stable across
469
+ * invocations and rotatable by deleting one file.
470
+ */
471
+ export function machineId(userRoot?: string): string | null {
472
+ const target = machineIdPath(userRoot);
473
+ try {
474
+ const existing = fs.readFileSync(target, 'utf8').trim();
475
+ if (UUID_RE.test(existing)) return existing;
476
+ } catch {
477
+ /* mint below */
478
+ }
479
+ try {
480
+ const minted = crypto.randomUUID();
481
+ fs.mkdirSync(path.dirname(target), { recursive: true });
482
+ fs.writeFileSync(target, `${minted}\n`);
483
+ return minted;
484
+ } catch {
485
+ return null;
486
+ }
487
+ }
488
+
489
+ /**
490
+ * The episode id for this process.
491
+ *
492
+ * One per session, per `FIELD_PURPOSE`. A hook runs as a fresh process per
493
+ * event, so "this process" is not a session — the id is derived from the host's
494
+ * own session identifier when it exposes one, and is otherwise minted per
495
+ * process, which degrades the SCOPE of `sequence` and nothing else.
496
+ *
497
+ * The host id is HASHED rather than carried: it is an opaque host value, and
498
+ * this schema's rule is that no field holds a foreign identifier. The hash is
499
+ * one-way and the result is shaped as a UUID so `validateRecord` accepts it.
500
+ */
501
+ /**
502
+ * The per-process fallback episode, minted once.
503
+ *
504
+ * A fresh UUID per CALL was the defect: with no host session id in the
505
+ * environment, two `recordCapture` calls in one process landed in two
506
+ * episodes, so `dedup_key` scoping and the per-episode sequence counter both
507
+ * broke. It was invisible on a developer machine because the host exports
508
+ * `CLAUDE_CODE_SESSION_ID`, and it surfaced only on CI, where nothing does —
509
+ * which is the whole population of unattended runs.
510
+ *
511
+ * Memoised per process, not per file: a hook is one process per event, so the
512
+ * honest degradation without a host session is one episode per event rather
513
+ * than one episode per record. That is a coarser grouping, and it is a
514
+ * grouping.
515
+ */
516
+ let fallbackEpisode: string | null = null;
517
+
518
+ export function episodeId(env: NodeJS.ProcessEnv = process.env): string {
519
+ const hostSession = env.CLAUDE_CODE_SESSION_ID ?? env.AGENT_CONFIG_SESSION_ID ?? '';
520
+ if (hostSession === '') {
521
+ fallbackEpisode ??= crypto.randomUUID();
522
+ return fallbackEpisode;
523
+ }
524
+ const digest = crypto.createHash('sha256').update(hostSession).digest('hex');
525
+ return [
526
+ digest.slice(0, 8),
527
+ digest.slice(8, 12),
528
+ `4${digest.slice(13, 16)}`,
529
+ `8${digest.slice(17, 20)}`,
530
+ digest.slice(20, 32),
531
+ ].join('-');
532
+ }
533
+
534
+ /**
535
+ * The next sequence number within an episode.
536
+ *
537
+ * Monotonic and non-negative per `FIELD_PURPOSE`, and it has to survive across
538
+ * PROCESSES because a hook is one process per event. So it is a counter file
539
+ * per episode, incremented under `wx`-free read-then-write — a lost update
540
+ * costs a duplicate `dedup_key`, which the store already collapses at read
541
+ * time, so the failure mode is a duplicate rather than a gap.
542
+ */
543
+ export function nextSequence(episode: string, userRoot?: string): number {
544
+ const dir = path.join(resolveCollectorStore(userRoot).root, EPISODE_DIR_NAME);
545
+ const target = path.join(dir, `${episode}.seq`);
546
+ try {
547
+ fs.mkdirSync(dir, { recursive: true });
548
+ let current = 0;
549
+ try {
550
+ const raw = Number.parseInt(fs.readFileSync(target, 'utf8').trim(), 10);
551
+ if (Number.isInteger(raw) && raw >= 0) current = raw + 1;
552
+ } catch {
553
+ /* first record of this episode */
554
+ }
555
+ fs.writeFileSync(target, `${current}\n`);
556
+ return current;
557
+ } catch {
558
+ return 0;
559
+ }
560
+ }
561
+
562
+ /**
563
+ * Drop episode counters older than the retention horizon.
564
+ *
565
+ * `nextSequence` writes one `episodes/<id>.seq` per session and nothing pruned
566
+ * the directory (R2 round-4 finding 4) — inside the very directory
567
+ * `RESOURCE_BUDGETS.disk_bytes` ceilings, whose breach STOPS the collector.
568
+ * That is the argument the disk-budget basis already makes for
569
+ * `opportunities.log`, left unapplied to the sibling directory added in the
570
+ * same change. `directorySize` also re-`stat`s every one of these files on
571
+ * every beat, so the per-beat cost grew without bound too.
572
+ *
573
+ * Pruned by MTIME rather than by content: a counter file carries a number, not
574
+ * a date, and inventing a date format for it would be a second thing to keep in
575
+ * sync with the retention horizon.
576
+ */
577
+ export function pruneEpisodeCounters(
578
+ userRoot: string | undefined,
579
+ now: number = Date.now(),
580
+ days: number = RETENTION_DAYS,
581
+ ): number {
582
+ const dir = path.join(resolveCollectorStore(userRoot).root, EPISODE_DIR_NAME);
583
+ let entries: string[];
584
+ try {
585
+ entries = fs.readdirSync(dir);
586
+ } catch {
587
+ return 0;
588
+ }
589
+ const cutoff = now - days * 86_400_000;
590
+ let dropped = 0;
591
+ for (const name of entries) {
592
+ if (!name.endsWith('.seq')) continue;
593
+ const full = path.join(dir, name);
594
+ try {
595
+ if (fs.statSync(full).mtimeMs >= cutoff) continue;
596
+ fs.rmSync(full, { force: true });
597
+ dropped += 1;
598
+ } catch {
599
+ /* raced with another prune */
600
+ }
601
+ }
602
+ return dropped;
603
+ }
604
+
605
+ /**
606
+ * Record a CAPTURE — the numerator's production writer.
607
+ *
608
+ * **This function is the fix for the defect three review rounds took to find**
609
+ * (R2 round-3 finding 1): `spoolRecord` had no production caller at all. The
610
+ * denominator was wired into `dispatch_hook.main` and climbed; the numerator
611
+ * stayed 0 by construction, so once the observation window and sample
612
+ * thresholds were met, the miss branch would have fired a decision record for a
613
+ * WIRING OMISSION rather than for a capture failure — and the two are
614
+ * indistinguishable from the number.
615
+ *
616
+ * It builds a valid record from the three facts a hook has (event, platform,
617
+ * date) plus the three the store needs to deduplicate (machine, episode,
618
+ * sequence), and hands it to {@link spoolRecord}. Same never-throw contract as
619
+ * everything else on this path.
620
+ */
621
+ export function recordCapture(
622
+ event: CollectorEvent | string,
623
+ platform: CollectorPlatform | string,
624
+ userRoot?: string,
625
+ at: number = Date.now(),
626
+ opts: WriterOptions = {},
627
+ ): boolean {
628
+ try {
629
+ if (opts.includeSelfObserved !== true && isSelfObservation()) return false;
630
+ if (!isCollectorEnabled(userRoot)) return false;
631
+ if (!(COLLECTOR_EVENTS as readonly string[]).includes(event)) return false;
632
+ if (!(COLLECTOR_PLATFORMS as readonly string[]).includes(platform)) return false;
633
+ const machine = machineId(userRoot);
634
+ if (machine === null) return false;
635
+ const episode = episodeId();
636
+ return spoolRecord(
637
+ {
638
+ schema_version: COLLECTOR_SCHEMA_VERSION,
639
+ machine_id: machine,
640
+ episode_id: episode,
641
+ event,
642
+ sequence: nextSequence(episode, userRoot),
643
+ outcome: 'captured',
644
+ platform,
645
+ occurred_on: utcDate(at),
646
+ collector_version: COLLECTOR_VERSION,
647
+ },
648
+ userRoot,
649
+ );
650
+ } catch {
651
+ return false;
652
+ }
653
+ }
654
+
655
+ /**
656
+ * Hand a record to the daemon. Returns whether it was spooled.
657
+ *
658
+ * The hook does NOT write to the SQLite store. Short-lived processes opening a
659
+ * database on every dispatch is the write contention and the version-skew risk
660
+ * the daemon exists to remove, and it would put a `node:sqlite` dependency on
661
+ * the dispatch path — which the observation-only contract's "resolve
662
+ * identically" clause cannot survive on a runtime without it.
663
+ *
664
+ * Refuses when the collector is off, so a disabled collector accumulates no
665
+ * spool to drain later. That is deliberate: a spool that survives the disabled
666
+ * period would make a re-enable look like a capture spike.
667
+ */
668
+ /**
669
+ * Hard ceiling on the undrained spool, in bytes.
670
+ *
671
+ * The spool is written by hook processes and drained ONLY by the daemon, and
672
+ * "collector enabled, daemon not running" is a supported state — the daemon is
673
+ * a separate opt-in process and `start()` legitimately refuses on `lock-held`
674
+ * or `store-unavailable`. Without a cap the spool grew without limit in exactly
675
+ * that state, and the disk budget that would have noticed is enforced by the
676
+ * process that is not running (R2 round-4 finding 5).
677
+ *
678
+ * 8 MiB is two thirds of the disk budget's 12 MiB expected peak, so a spool at
679
+ * its ceiling still leaves the store's own share. A record is ~250 bytes, so
680
+ * this is roughly 33,000 undrained captures — far past any plausible drain
681
+ * interval, which is what makes hitting it a signal rather than a limit.
682
+ */
683
+ export const SPOOL_MAX_BYTES = 8 * 1024 * 1024;
684
+
685
+ export function spoolRecord(record: unknown, userRoot?: string): boolean {
686
+ try {
687
+ if (!isCollectorEnabled(userRoot)) return false;
688
+ const target = spoolPendingPath(userRoot);
689
+ fs.mkdirSync(path.dirname(target), { recursive: true });
690
+ // Refuse rather than rotate. A rotated spool would keep the newest
691
+ // captures and silently drop the oldest, which inflates the measured
692
+ // rate for the window that follows an outage; refusing loses the same
693
+ // records and does not pretend otherwise. Either way the loss is real —
694
+ // the difference is whether the numerator lies about it.
695
+ try {
696
+ if (fs.statSync(target).size >= SPOOL_MAX_BYTES) return false;
697
+ } catch {
698
+ /* absent is size 0 */
699
+ }
700
+ fs.appendFileSync(target, `${JSON.stringify(record)}\n`);
701
+ return true;
702
+ } catch {
703
+ return false;
704
+ }
705
+ }
706
+
707
+ /**
708
+ * Atomically claim the pending spool for draining, returning the claimed path
709
+ * or null when there is nothing to drain.
710
+ *
711
+ * The rename is what makes concurrent appends safe: writers that already have
712
+ * the old inode finish into the claimed file, and every writer that opens after
713
+ * the rename creates a fresh `pending.jsonl`. No lock, no lost line.
714
+ */
715
+ let claimSequence = 0;
716
+
717
+ export function claimSpool(userRoot?: string, at: number = Date.now()): string | null {
718
+ const pending = spoolPendingPath(userRoot);
719
+ if (!fs.existsSync(pending)) return null;
720
+ // A monotonic per-process counter, because the timestamp alone is not
721
+ // unique (R2 round-2 finding 14): two claims by one process inside the same
722
+ // millisecond produced the same name, and `renameSync` overwrites silently
723
+ // — so the earlier batch was destroyed rather than the collision being
724
+ // noticed. The per-user lock makes that narrow in production, but both
725
+ // functions are exported and driven directly by tests, and the docstring's
726
+ // "no lock, no lost line" is only true if the claimed name is unique.
727
+ claimSequence += 1;
728
+ const claimed = path.join(
729
+ spoolDir(userRoot),
730
+ `draining-${at}-${process.pid}-${claimSequence}.jsonl`,
731
+ );
732
+ try {
733
+ fs.renameSync(pending, claimed);
734
+ return claimed;
735
+ } catch {
736
+ return null;
737
+ }
738
+ }
739
+
740
+ /**
741
+ * Parse a claimed spool file into records. Unparseable lines are reported,
742
+ * never dropped silently — and an UNREADABLE FILE is reported too.
743
+ *
744
+ * `unreadable` exists because its absence destroyed batches (R2 round-3 finding
745
+ * 2): a read error returned `{records: [], malformed: 0}`, which is
746
+ * indistinguishable from an empty file, and the caller then deleted the batch.
747
+ * A transient `EACCES` or `EMFILE` therefore lost every record in it and
748
+ * reported nothing — the exact loss class the crash-recovery re-drain exists to
749
+ * prevent, arriving through the recovery path itself.
750
+ */
751
+ export function readClaimedSpool(
752
+ claimed: string,
753
+ ): { records: unknown[]; malformed: number; unreadable: boolean } {
754
+ let raw = '';
755
+ try {
756
+ raw = fs.readFileSync(claimed, 'utf8');
757
+ } catch {
758
+ return { records: [], malformed: 0, unreadable: true };
759
+ }
760
+ const records: unknown[] = [];
761
+ let malformed = 0;
762
+ for (const line of raw.split('\n')) {
763
+ if (line.length === 0) continue;
764
+ try {
765
+ records.push(JSON.parse(line));
766
+ } catch {
767
+ malformed += 1;
768
+ }
769
+ }
770
+ return { records, malformed, unreadable: false };
771
+ }