chainlesschain 0.162.163 → 0.162.164

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 (213) hide show
  1. package/package.json +3 -1
  2. package/src/assets/web-panel/assets/{AIOps-BQ0kgfA1.js → AIOps-BqRkaHGb.js} +1 -1
  3. package/src/assets/web-panel/assets/{ActionButton-Dr1MDANe.js → ActionButton-DWO7OZLC.js} +1 -1
  4. package/src/assets/web-panel/assets/{Analytics-CZ5vsaXW.js → Analytics-C5oaVezM.js} +2 -2
  5. package/src/assets/web-panel/assets/{AppLayout-BIBga881.js → AppLayout-CsUESDoZ.js} +3 -3
  6. package/src/assets/web-panel/assets/{Artifacts-vpXHRU3u.js → Artifacts-Dragqu5k.js} +1 -1
  7. package/src/assets/web-panel/assets/{Audit-B8exuoub.js → Audit-CQsaHctY.js} +1 -1
  8. package/src/assets/web-panel/assets/{BackgroundAgents-923evRRN.js → BackgroundAgents-DdG1jNSM.js} +1 -1
  9. package/src/assets/web-panel/assets/{Backup-sr_sswlQ.js → Backup-DN6sjK4z.js} +1 -1
  10. package/src/assets/web-panel/assets/{BaseInput-CnW-4XKT.js → BaseInput-Cxm_ZTCl.js} +1 -1
  11. package/src/assets/web-panel/assets/{Chat-FZCsTZIL.js → Chat-DVXGmA56.js} +5 -5
  12. package/src/assets/web-panel/assets/{ChatBubbleRenderer-DDPd9qT0.js → ChatBubbleRenderer-BdaCL_iE.js} +1 -1
  13. package/src/assets/web-panel/assets/{Checkbox-DFrxB59H.js → Checkbox-DEc4vD_A.js} +1 -1
  14. package/src/assets/web-panel/assets/{Codegen-_b6WEel8.js → Codegen-CKiTcv36.js} +1 -1
  15. package/src/assets/web-panel/assets/{Col-CkbnnZ-K.js → Col-Br9UUNN0.js} +1 -1
  16. package/src/assets/web-panel/assets/{Community-BxM_82zW.js → Community-NeUH_70T.js} +1 -1
  17. package/src/assets/web-panel/assets/{Compact-DYinP45R.js → Compact-BqR-z2Df.js} +1 -1
  18. package/src/assets/web-panel/assets/{Compliance-DyXp3sSP.js → Compliance-DcWB_SgZ.js} +1 -1
  19. package/src/assets/web-panel/assets/{Cowork-BEr2DhW7.js → Cowork-BfhgcudF.js} +2 -2
  20. package/src/assets/web-panel/assets/{Cron-B-Vwo1hN.js → Cron-LaU29LgD.js} +2 -2
  21. package/src/assets/web-panel/assets/{Crosschain-DbLxKddd.js → Crosschain-B2ZB9uwW.js} +1 -1
  22. package/src/assets/web-panel/assets/{DID-BsQ_LFw9.js → DID-BuHl38A2.js} +2 -2
  23. package/src/assets/web-panel/assets/{Dashboard-lVlXQHu5.js → Dashboard-m627yqvZ.js} +2 -2
  24. package/src/assets/web-panel/assets/{Dropdown-Beo_foO7.js → Dropdown-C7bxF8F5.js} +1 -1
  25. package/src/assets/web-panel/assets/{EmailListRenderer-DszHIXED.js → EmailListRenderer-DPLPQzLp.js} +1 -1
  26. package/src/assets/web-panel/assets/{FamilyGuardDashboard-DcAwoh81.js → FamilyGuardDashboard-cOiH6ike.js} +1 -1
  27. package/src/assets/web-panel/assets/{Federation-CuJLa2eb.js → Federation-Bvkzg88z.js} +1 -1
  28. package/src/assets/web-panel/assets/{FormItemContext-Cxd1lxhc.js → FormItemContext-BTsiA7h_.js} +1 -1
  29. package/src/assets/web-panel/assets/{GenericCardRenderer-BBvq5Jsw.js → GenericCardRenderer-BGnVE5qu.js} +1 -1
  30. package/src/assets/web-panel/assets/{Git-WqPKetx6.js → Git-DS_j6r25.js} +2 -2
  31. package/src/assets/web-panel/assets/{Governance-CWYtY3ke.js → Governance-CKRG3Rsl.js} +1 -1
  32. package/src/assets/web-panel/assets/{Inference-BnX1iiNV.js → Inference-DW8INoNQ.js} +1 -1
  33. package/src/assets/web-panel/assets/{KnowledgeGraph-BJuM8GN5.js → KnowledgeGraph-BhEYeFhC.js} +1 -1
  34. package/src/assets/web-panel/assets/{Logs-DqLMNntm.js → Logs-wqi0uxuR.js} +2 -2
  35. package/src/assets/web-panel/assets/{MarkdownRenderer-BqYwEYou.js → MarkdownRenderer-DS8dE8ty.js} +1 -1
  36. package/src/assets/web-panel/assets/{Marketplace-_vGfmhNl.js → Marketplace-CMlTuPLT.js} +1 -1
  37. package/src/assets/web-panel/assets/{McpTools-BeJDOkDj.js → McpTools-CSx4m5JJ.js} +3 -3
  38. package/src/assets/web-panel/assets/{Memory-Da0EEZDO.js → Memory-vnQCUy-_.js} +2 -2
  39. package/src/assets/web-panel/assets/{MobileBridge-DSuzcCCy.js → MobileBridge-BV2NA742.js} +1 -1
  40. package/src/assets/web-panel/assets/MobileProjects-Bw0k8e1B.js +1 -0
  41. package/src/assets/web-panel/assets/{Mtc-BE9YgdA0.js → Mtc-CzK_FPB1.js} +2 -2
  42. package/src/assets/web-panel/assets/{MtcAudit-fC2BpO5S.js → MtcAudit-Cb7G9oL3.js} +2 -2
  43. package/src/assets/web-panel/assets/{Multisig-D24Xxqw3.js → Multisig-qJxlUyzr.js} +3 -3
  44. package/src/assets/web-panel/assets/{NLProgramming-Bs3oGvS8.js → NLProgramming-DqaBJ7Te.js} +1 -1
  45. package/src/assets/web-panel/assets/{Notes-BFjmAA-3.js → Notes-CwhTJkRi.js} +3 -3
  46. package/src/assets/web-panel/assets/{NotificationSettings-BX_9Bhv0.js → NotificationSettings-YDs4Fal4.js} +1 -1
  47. package/src/assets/web-panel/assets/{OrderTableRenderer-wjkZwJXI.js → OrderTableRenderer-CvEALpq7.js} +1 -1
  48. package/src/assets/web-panel/assets/{Organization-DBcdsVEP.js → Organization-CEmQm9n8.js} +2 -2
  49. package/src/assets/web-panel/assets/{Overflow-DDyaF9vC.js → Overflow-zby_P_LA.js} +1 -1
  50. package/src/assets/web-panel/assets/{P2P-BB78L4he.js → P2P-DsmDNFNT.js} +2 -2
  51. package/src/assets/web-panel/assets/{PdhVaultBrowser-DqTiZFum.js → PdhVaultBrowser-FWwFlZGF.js} +3 -3
  52. package/src/assets/web-panel/assets/{Permissions-INbnWv4j.js → Permissions-DuzQ6me5.js} +4 -4
  53. package/src/assets/web-panel/assets/{PersonalDataHub-BST-VmTq.js → PersonalDataHub-DZbeN613.js} +2 -2
  54. package/src/assets/web-panel/assets/{Pipeline-CE7_tS2k.js → Pipeline-CoNSVAUS.js} +1 -1
  55. package/src/assets/web-panel/assets/{Privacy-hK5L8dOB.js → Privacy-OYJEAaMC.js} +1 -1
  56. package/src/assets/web-panel/assets/{ProjectInit-BvkkUoDn.js → ProjectInit-CWIapV--.js} +2 -2
  57. package/src/assets/web-panel/assets/{ProjectSettings-u7_EUtkm.js → ProjectSettings-CX8xMxZn.js} +2 -2
  58. package/src/assets/web-panel/assets/{Projects-5Fmt_8Nw.js → Projects-BLCN8YyY.js} +1 -1
  59. package/src/assets/web-panel/assets/{Providers-B9eanarR.js → Providers-BV_sxp75.js} +1 -1
  60. package/src/assets/web-panel/assets/{QrScannerModal-C0dX5N2f.js → QrScannerModal-BTnRrHRp.js} +1 -1
  61. package/src/assets/web-panel/assets/{QuickAsk-CKTpG-PZ.js → QuickAsk-D1KZg018.js} +1 -1
  62. package/src/assets/web-panel/assets/{Recommend-Px_w_XQM.js → Recommend-COm5cuNg.js} +1 -1
  63. package/src/assets/web-panel/assets/{RemoteSession-Di0fnYkM.js → RemoteSession-0saWEkYI.js} +2 -2
  64. package/src/assets/web-panel/assets/{Reputation-vesnWjXt.js → Reputation-Dr7iKLS2.js} +1 -1
  65. package/src/assets/web-panel/assets/{Row-exbY6LkT.js → Row-C1vpPBcr.js} +1 -1
  66. package/src/assets/web-panel/assets/{RssFeed-B3O3x70t.js → RssFeed-CxlWrtNj.js} +2 -2
  67. package/src/assets/web-panel/assets/{Search-uRA0DhNz.js → Search-BT2V8dPN.js} +1 -1
  68. package/src/assets/web-panel/assets/{Security-CViqdH6K.js → Security-bm9V3c5k.js} +4 -4
  69. package/src/assets/web-panel/assets/{Services-CkXS97WN.js → Services-CocH1yyX.js} +2 -2
  70. package/src/assets/web-panel/assets/{Skeleton-DxW5WYnE.js → Skeleton-DHItlMdd.js} +1 -1
  71. package/src/assets/web-panel/assets/{Skills-CLxmGFse.js → Skills-C59sLc4w.js} +1 -1
  72. package/src/assets/web-panel/assets/{Sla-Cr5fziXq.js → Sla-Crj4BMc8.js} +1 -1
  73. package/src/assets/web-panel/assets/{SpeechSettings-H0JSb3cp.js → SpeechSettings-DX4KFceY.js} +1 -1
  74. package/src/assets/web-panel/assets/{SyncSettings-DMYWMae7.js → SyncSettings-BD_iwQLr.js} +2 -2
  75. package/src/assets/web-panel/assets/{Tasks-CbYyDdF4.js → Tasks-BmCTzYsH.js} +1 -1
  76. package/src/assets/web-panel/assets/{Templates-Cpc8JjiK.js → Templates-BGUaxZYl.js} +1 -1
  77. package/src/assets/web-panel/assets/{Tenant-CRh77Jda.js → Tenant-BS0tJx2O.js} +1 -1
  78. package/src/assets/web-panel/assets/{Terminal-4D8tNBO5.js → Terminal--tHSAsld.js} +2 -2
  79. package/src/assets/web-panel/assets/{TimelineRenderer-D41jzzbG.js → TimelineRenderer-D9-MFxS4.js} +1 -1
  80. package/src/assets/web-panel/assets/{Tokens-C_jWEy6b.js → Tokens-CiCB485f.js} +1 -1
  81. package/src/assets/web-panel/assets/{Trigger-Dk1ZUhsy.js → Trigger-SmCFuW_l.js} +1 -1
  82. package/src/assets/web-panel/assets/{Trust-CLVy4l7m.js → Trust-DkK3Rqe0.js} +1 -1
  83. package/src/assets/web-panel/assets/{UkeySign-C-FeqFDj.js → UkeySign-CDM4LGdB.js} +1 -1
  84. package/src/assets/web-panel/assets/{VideoEditing-CqVZzyMm.js → VideoEditing-IDB7DJXt.js} +1 -1
  85. package/src/assets/web-panel/assets/{Wallet-DX9BW9pu.js → Wallet-Bikj5cmt.js} +2 -2
  86. package/src/assets/web-panel/assets/{WebAuthn-7hQDpWw_.js → WebAuthn-C9abWlAw.js} +2 -2
  87. package/src/assets/web-panel/assets/{WorkflowEditor-6XqUAO0j.js → WorkflowEditor-DrbRKVky.js} +1 -1
  88. package/src/assets/web-panel/assets/{chat-5R_vkaJG.js → chat-CccCSDQf.js} +1 -1
  89. package/src/assets/web-panel/assets/{colors-huUPZdC-.js → colors-CXObXLhb.js} +1 -1
  90. package/src/assets/web-panel/assets/{compact-item-CfJlgisE.js → compact-item-B5kfYAxC.js} +1 -1
  91. package/src/assets/web-panel/assets/{createContext-jO8heofr.js → createContext-DmiPBfip.js} +1 -1
  92. package/src/assets/web-panel/assets/devWarning-CvKrADNa.js +1 -0
  93. package/src/assets/web-panel/assets/{hasIn-DY8Vxqx7.js → hasIn-Q6lYteAc.js} +1 -1
  94. package/src/assets/web-panel/assets/{index-BtEXSoPL.js → index-25Wlqps4.js} +1 -1
  95. package/src/assets/web-panel/assets/{index-BsBtRWbA.js → index-33U29UNV.js} +1 -1
  96. package/src/assets/web-panel/assets/{index-BQS-uPOK.js → index-4oXBDczR.js} +1 -1
  97. package/src/assets/web-panel/assets/{index-DXBcvUnf.js → index-BGYXvMc5.js} +1 -1
  98. package/src/assets/web-panel/assets/{index-C2iVSFpy.js → index-BJCCa0s_.js} +1 -1
  99. package/src/assets/web-panel/assets/{index-A48PtXw9.js → index-BJJ5DhKI.js} +1 -1
  100. package/src/assets/web-panel/assets/{index-Db1cLWir.js → index-BLOzViJ6.js} +1 -1
  101. package/src/assets/web-panel/assets/{index-vXs3B8GJ.js → index-BRrbUMoG.js} +1 -1
  102. package/src/assets/web-panel/assets/{index-BHE8OIls.js → index-BUFpT17Q.js} +1 -1
  103. package/src/assets/web-panel/assets/{index-BmqGerpC.js → index-BkRDiqGc.js} +1 -1
  104. package/src/assets/web-panel/assets/{index-CzKmteNS.js → index-BsfvW6my.js} +1 -1
  105. package/src/assets/web-panel/assets/{index-BAON7TzU.js → index-By-kfPED.js} +1 -1
  106. package/src/assets/web-panel/assets/{index-DoI2sZm7.js → index-BzFTcQsc.js} +1 -1
  107. package/src/assets/web-panel/assets/{index-j3O2EEJe.js → index-C2FXdboq.js} +1 -1
  108. package/src/assets/web-panel/assets/{index-CRElRhyS.js → index-C5XOlTHN.js} +1 -1
  109. package/src/assets/web-panel/assets/{index-KRHroryI.js → index-CLhBDS2w.js} +1 -1
  110. package/src/assets/web-panel/assets/{index-Bxax9vQ6.js → index-CMq7UeoT.js} +1 -1
  111. package/src/assets/web-panel/assets/{index-BfCx4pFT.js → index-CVMTWNKl.js} +1 -1
  112. package/src/assets/web-panel/assets/{index-CA6sO5VD.js → index-Caghi-yL.js} +1 -1
  113. package/src/assets/web-panel/assets/{index-CA90TZxG.js → index-CdB6FuSH.js} +1 -1
  114. package/src/assets/web-panel/assets/{index-Cbe9G0V4.js → index-CoK8-kWQ.js} +1 -1
  115. package/src/assets/web-panel/assets/{index-D0pet5Uy.js → index-CsO3GUAY.js} +1 -1
  116. package/src/assets/web-panel/assets/{index-ChvHubX3.js → index-CwZBa20Z.js} +1 -1
  117. package/src/assets/web-panel/assets/{index-BRXOP09X.js → index-D-kASucw.js} +1 -1
  118. package/src/assets/web-panel/assets/{index-B74OXFxC.js → index-D8eBw7vW.js} +1 -1
  119. package/src/assets/web-panel/assets/{index-D1VLuww0.js → index-D9wgifk4.js} +1 -1
  120. package/src/assets/web-panel/assets/{index-p85YaaKw.js → index-DAYCTc07.js} +1 -1
  121. package/src/assets/web-panel/assets/{index-BliaT02i.js → index-DF6Q8l2h.js} +1 -1
  122. package/src/assets/web-panel/assets/index-DTdFUWc_.js +1 -0
  123. package/src/assets/web-panel/assets/{index-Cjxbu68P.js → index-DgTLApS9.js} +1 -1
  124. package/src/assets/web-panel/assets/{index-Cis_Ip7H.js → index-DgxVJ8pg.js} +1 -1
  125. package/src/assets/web-panel/assets/{index-v3pTaEQm.js → index-DlJvIqoR.js} +1 -1
  126. package/src/assets/web-panel/assets/{index-BYLBPpt8.js → index-Dw4eHfsG.js} +1 -1
  127. package/src/assets/web-panel/assets/{index-B300RPnT.js → index-Rh-ytwow.js} +3 -3
  128. package/src/assets/web-panel/assets/{index-CLhBh-qE.js → index-StAEt2pq.js} +1 -1
  129. package/src/assets/web-panel/assets/{index-d1xkbtNz.js → index-bzfw951k.js} +1 -1
  130. package/src/assets/web-panel/assets/{index-BCqxyvqp.js → index-fSUMbMvv.js} +1 -1
  131. package/src/assets/web-panel/assets/{index-dKx57io7.js → index-tFHy5Wol.js} +1 -1
  132. package/src/assets/web-panel/assets/index-zQ0M_kz-.js +1 -0
  133. package/src/assets/web-panel/assets/{initDefaultProps-MCB7-raZ.js → initDefaultProps-BNq7X72N.js} +1 -1
  134. package/src/assets/web-panel/assets/{motion-UWDF9swV.js → motion-DOf9viTc.js} +1 -1
  135. package/src/assets/web-panel/assets/{move-CsTmgXZU.js → move-CRHTcW4x.js} +1 -1
  136. package/src/assets/web-panel/assets/{mtc-parser-fQy6KGLg.js → mtc-parser-Cyoozgm2.js} +1 -1
  137. package/src/assets/web-panel/assets/{omit-woPnEjd1.js → omit-6-TjFNru.js} +1 -1
  138. package/src/assets/web-panel/assets/{pickAttrs-Bs2dZ1gf.js → pickAttrs-DlUmO1zi.js} +1 -1
  139. package/src/assets/web-panel/assets/{placementArrow-C7EVgPkg.js → placementArrow-CFOL8mIR.js} +1 -1
  140. package/src/assets/web-panel/assets/{responsiveObserve-BH2kYCTN.js → responsiveObserve-BtaFK5zd.js} +1 -1
  141. package/src/assets/web-panel/assets/{slide-BFBP2V_e.js → slide-BIbc2bur.js} +1 -1
  142. package/src/assets/web-panel/assets/{statusUtils-xdDxDYse.js → statusUtils-BXX9d6-w.js} +1 -1
  143. package/src/assets/web-panel/assets/{styleChecker-aAWgPPQf.js → styleChecker-BGv10Bpn.js} +1 -1
  144. package/src/assets/web-panel/assets/{useFlexGapSupport-DkGSAWoO.js → useFlexGapSupport-bKnKT8tw.js} +1 -1
  145. package/src/assets/web-panel/assets/{useFs-BrOih40N.js → useFs-FY4IL4jn.js} +1 -1
  146. package/src/assets/web-panel/assets/{usePersonalDataHub-eIYrgMqO.js → usePersonalDataHub-Bwl9TEgu.js} +1 -1
  147. package/src/assets/web-panel/assets/{vnode-B8lutNSE.js → vnode-BM3OHxAT.js} +1 -1
  148. package/src/assets/web-panel/assets/{zoom-CffNre7V.js → zoom-D1fbFiHB.js} +1 -1
  149. package/src/assets/web-panel/index.html +1 -1
  150. package/src/commands/agenda.js +71 -14
  151. package/src/commands/doctor.js +85 -1
  152. package/src/commands/plugin.js +226 -9
  153. package/src/commands/review.js +17 -1
  154. package/src/commands/session.js +191 -0
  155. package/src/commands/team.js +19 -1
  156. package/src/data/changelog.json +5 -4
  157. package/src/gateways/ws/session-protocol.js +50 -0
  158. package/src/gateways/ws/ws-agent-handler.js +50 -0
  159. package/src/harness/jsonl-session-store.js +60 -0
  160. package/src/harness/worktree-isolator.js +140 -5
  161. package/src/lib/agent-schedule-store.js +30 -1
  162. package/src/lib/agent-team/team-worktree.js +37 -2
  163. package/src/lib/agent-tool-admission.js +125 -0
  164. package/src/lib/app-preview.js +360 -0
  165. package/src/lib/async-hook-supervisor.cjs +54 -0
  166. package/src/lib/capability-manifest.js +319 -0
  167. package/src/lib/capability-negotiation.js +23 -12
  168. package/src/lib/diagnostic-bundle.js +230 -0
  169. package/src/lib/doctor-checkup.js +223 -4
  170. package/src/lib/execution-location.js +286 -0
  171. package/src/lib/headless-manifest.js +13 -40
  172. package/src/lib/hook-stats-store.cjs +197 -0
  173. package/src/lib/idempotency.js +109 -0
  174. package/src/lib/jsonl-session-store.js +1 -0
  175. package/src/lib/lsp/__tests__/lsp-manager.test.js +58 -0
  176. package/src/lib/lsp/lsp-manager.js +56 -0
  177. package/src/lib/monitor-event.js +290 -0
  178. package/src/lib/operation-fingerprint.js +217 -0
  179. package/src/lib/plugin-runtime/bin.js +37 -0
  180. package/src/lib/plugin-runtime/capabilities.js +19 -0
  181. package/src/lib/plugin-runtime/capability-consent.js +28 -0
  182. package/src/lib/plugin-runtime/hooks.js +37 -0
  183. package/src/lib/plugin-runtime/lsp.js +38 -0
  184. package/src/lib/plugin-runtime/mcp.js +40 -0
  185. package/src/lib/plugin-runtime/monitors.js +37 -0
  186. package/src/lib/plugin-runtime/plugin-options.js +151 -0
  187. package/src/lib/pr-automation-policy.js +224 -0
  188. package/src/lib/remote-approval-bridge.js +66 -13
  189. package/src/lib/repl-rewind.js +101 -0
  190. package/src/lib/review-comment-anchor.js +241 -0
  191. package/src/lib/review-pipeline.js +46 -7
  192. package/src/lib/runtime-checkup.js +221 -0
  193. package/src/lib/secret-scan.js +409 -0
  194. package/src/lib/session-branch.js +0 -0
  195. package/src/lib/side-effect-ledger.js +105 -0
  196. package/src/lib/skill-loader.js +32 -0
  197. package/src/lib/skill-path-scope.js +63 -0
  198. package/src/lib/sub-agent-context.js +36 -4
  199. package/src/lib/sub-agent-registry.js +19 -0
  200. package/src/lib/telemetry/span-recorder.js +17 -4
  201. package/src/lib/terminal-context.js +195 -0
  202. package/src/lib/unattended-action-policy.js +182 -0
  203. package/src/lib/worktree-cleanup-safety.js +138 -0
  204. package/src/lib/worktree-isolator.js +8 -0
  205. package/src/lib/worktree-sparse.js +131 -0
  206. package/src/repl/agent-repl.js +152 -38
  207. package/src/runtime/agent-core.js +191 -13
  208. package/src/runtime/diagnostics.js +60 -0
  209. package/src/runtime/headless-runner.js +126 -1
  210. package/src/assets/web-panel/assets/MobileProjects-D0RAk4e8.js +0 -1
  211. package/src/assets/web-panel/assets/devWarning-DeIrCfH-.js +0 -1
  212. package/src/assets/web-panel/assets/index-BxUKMRpP.js +0 -1
  213. package/src/assets/web-panel/assets/index-DscU9iPI.js +0 -1
@@ -216,6 +216,227 @@ export function checkHooks(hooks = [], t) {
216
216
  return out;
217
217
  }
218
218
 
219
+ /**
220
+ * STATICALLY validate a settings.json `hooks` block for reliably-detectable
221
+ * breakage — a hook that will silently NEVER fire (unknown event name, an
222
+ * uncompilable `/…/` regex matcher, a missing command) or is misconfigured (a
223
+ * non-positive / over-cap timeout). This is the config subset a doctor can
224
+ * catch without running anything; RUNTIME health (slow / circuit-broken hooks)
225
+ * needs live per-process stats and is handled by `checkHooks`, not here.
226
+ *
227
+ * @param {object} hooksBlock raw `.hooks` object from settings.json
228
+ * @param {object} [opts] { validEvents: Set<string>|string[], maxTimeoutSec }
229
+ */
230
+ export function checkHookConfig(
231
+ hooksBlock,
232
+ { validEvents, maxTimeoutSec = 600 } = {},
233
+ ) {
234
+ const out = [];
235
+ if (!hooksBlock || typeof hooksBlock !== "object") return out;
236
+ const known =
237
+ validEvents instanceof Set ? validEvents : new Set(validEvents || []);
238
+
239
+ for (const [event, groups] of Object.entries(hooksBlock)) {
240
+ // An event key the loader doesn't understand → every hook under it is dead
241
+ // (silently never fires). This is the highest-value static catch.
242
+ if (known.size && !known.has(event)) {
243
+ out.push(
244
+ finding(
245
+ "hook-unknown-event",
246
+ "hooks",
247
+ "warn",
248
+ `Hook event "${event}" is not recognized — its hooks never fire`,
249
+ "fix the event name in settings.json hooks",
250
+ { ref: event },
251
+ ),
252
+ );
253
+ continue;
254
+ }
255
+ if (!Array.isArray(groups)) {
256
+ out.push(
257
+ finding(
258
+ "hook-malformed",
259
+ "hooks",
260
+ "warn",
261
+ `Hook event "${event}" is not an array`,
262
+ "settings.json hooks[<event>] must be an array",
263
+ { ref: event },
264
+ ),
265
+ );
266
+ continue;
267
+ }
268
+ groups.forEach((g, gi) => {
269
+ const at = `${event}[${gi}]`;
270
+ // A `/body/flags` regex matcher that won't compile silently never matches.
271
+ if (g && typeof g.matcher === "string" && g.matcher.startsWith("/")) {
272
+ const m = g.matcher.match(/^\/(.*)\/([a-z]*)$/);
273
+ try {
274
+ new RegExp(m ? m[1] : g.matcher.slice(1), m ? m[2] : "");
275
+ } catch {
276
+ out.push(
277
+ finding(
278
+ "hook-bad-matcher",
279
+ "hooks",
280
+ "warn",
281
+ `Hook ${at} matcher ${g.matcher} is not a valid regex — it never matches`,
282
+ "fix the regex matcher",
283
+ { ref: at },
284
+ ),
285
+ );
286
+ }
287
+ }
288
+ const cmds = g && Array.isArray(g.hooks) ? g.hooks : null;
289
+ if (!cmds) {
290
+ out.push(
291
+ finding(
292
+ "hook-malformed",
293
+ "hooks",
294
+ "warn",
295
+ `Hook ${at} has no hooks array`,
296
+ "each hook group needs a hooks:[] array",
297
+ { ref: at },
298
+ ),
299
+ );
300
+ return;
301
+ }
302
+ cmds.forEach((h, hi) => {
303
+ const hAt = `${at}.hooks[${hi}]`;
304
+ if (!h || typeof h.command !== "string" || !h.command.trim()) {
305
+ out.push(
306
+ finding(
307
+ "hook-no-command",
308
+ "hooks",
309
+ "warn",
310
+ `Hook ${hAt} has no command — it does nothing`,
311
+ "add a command string or remove the entry",
312
+ { ref: hAt },
313
+ ),
314
+ );
315
+ }
316
+ if (h && h.timeout != null) {
317
+ const t = Number(h.timeout);
318
+ if (!Number.isFinite(t) || t <= 0) {
319
+ out.push(
320
+ finding(
321
+ "hook-bad-timeout",
322
+ "hooks",
323
+ "info",
324
+ `Hook ${hAt} timeout ${h.timeout} is not a positive number`,
325
+ "timeout is in seconds; use a positive value",
326
+ { ref: hAt },
327
+ ),
328
+ );
329
+ } else if (t > maxTimeoutSec) {
330
+ out.push(
331
+ finding(
332
+ "hook-bad-timeout",
333
+ "hooks",
334
+ "info",
335
+ `Hook ${hAt} timeout ${t}s exceeds the ${maxTimeoutSec}s cap (clamped)`,
336
+ "lower the timeout",
337
+ { ref: hAt },
338
+ ),
339
+ );
340
+ }
341
+ }
342
+ });
343
+ });
344
+ }
345
+ return out;
346
+ }
347
+
348
+ /**
349
+ * Sandbox real-capability / silent-degradation (P2 doctor "沙箱真实能力和静默
350
+ * 降级"). Given a probed snapshot, flag the dangerous case: a sandbox is
351
+ * CONFIGURED but its engine is unavailable AND `failIfUnavailable` is off — so
352
+ * tool subprocesses run with NO isolation and nothing warns (you think you're
353
+ * sandboxed but you're not). Strict-but-unavailable is a warn (fail-closed: the
354
+ * agent refuses to start, so it's not silent). Available → an info confirming
355
+ * the true isolation level.
356
+ *
357
+ * @param {object} snapshot { configured, engine, available, reason,
358
+ * failIfUnavailable, isolationLevel }
359
+ */
360
+ export function checkSandbox(snapshot = {}) {
361
+ const out = [];
362
+ if (!snapshot || !snapshot.configured) return out;
363
+ const engine = snapshot.engine || "sandbox";
364
+ const why = snapshot.reason ? `: ${snapshot.reason}` : "";
365
+ if (snapshot.available) {
366
+ out.push(
367
+ finding(
368
+ "sandbox-active",
369
+ "sandbox",
370
+ "info",
371
+ `Sandbox ${engine} available (isolation: ${snapshot.isolationLevel || "unknown"})`,
372
+ "",
373
+ { ref: engine },
374
+ ),
375
+ );
376
+ return out;
377
+ }
378
+ if (snapshot.failIfUnavailable) {
379
+ out.push(
380
+ finding(
381
+ "sandbox-strict-unavailable",
382
+ "sandbox",
383
+ "warn",
384
+ `Sandbox ${engine} is configured (strict) but unavailable${why} — the agent refuses to start until it's installed`,
385
+ `install ${engine} or unset sandbox.failIfUnavailable`,
386
+ { ref: engine },
387
+ ),
388
+ );
389
+ } else {
390
+ out.push(
391
+ finding(
392
+ "sandbox-silent-degrade",
393
+ "sandbox",
394
+ "error",
395
+ `Sandbox ${engine} is configured but unavailable${why} — commands run WITHOUT isolation and won't warn`,
396
+ `install ${engine}, or set sandbox.failIfUnavailable:true to fail closed`,
397
+ { ref: engine },
398
+ ),
399
+ );
400
+ }
401
+ return out;
402
+ }
403
+
404
+ /**
405
+ * Skill ids defined MORE THAN ONCE across (or within) layers (P2 doctor "重复
406
+ * Skill"). The loader's loadAll() silently keeps only the highest-priority copy
407
+ * per id (Map.set overwrite), so a user never learns their custom skill shadowed
408
+ * — or was shadowed by — another. Given the full, un-deduped per-layer entry
409
+ * list, flag every id with more than one definition.
410
+ *
411
+ * @param {Array<{id:string, layer:string}>} entries
412
+ */
413
+ export function checkDuplicateSkills(entries = []) {
414
+ const byId = new Map(); // id → layer[]
415
+ for (const e of entries) {
416
+ if (!e || !e.id) continue;
417
+ const arr = byId.get(e.id) || [];
418
+ arr.push(e.layer || "?");
419
+ byId.set(e.id, arr);
420
+ }
421
+ const out = [];
422
+ for (const [id, layers] of byId) {
423
+ if (layers.length > 1) {
424
+ const uniq = [...new Set(layers)];
425
+ out.push(
426
+ finding(
427
+ "skill-duplicate",
428
+ "skills",
429
+ "warn",
430
+ `Skill "${id}" has ${layers.length} definitions (${uniq.join(", ")}) — only the highest-priority copy loads`,
431
+ "rename or remove the redundant skill copy",
432
+ { ref: id, layers: uniq },
433
+ ),
434
+ );
435
+ }
436
+ }
437
+ return out;
438
+ }
439
+
219
440
  /** Plugins / LSP servers reported unhealthy or dead. */
220
441
  export function checkPluginsAndLsp(plugins = []) {
221
442
  const out = [];
@@ -0,0 +1,409 @@
1
+ /**
2
+ * Recall-first secret scanner + a labeled corpus and recall / false-positive /
3
+ * latency benchmark (IDE gap §8.1 "凭据脱敏真实语料基准" of
4
+ * CLAUDE_CODE_IDE_INCREMENTAL_GAP_ANALYSIS_2026-07-13.md).
5
+ *
6
+ * [[ide-context-redaction.js]] `redactSecretsInText` is deliberately PRECISION-
7
+ * first: it guards editor context that rides into the model, where mangling
8
+ * everyday code teaches users to disable it. That posture is right there, but it
9
+ * is the wrong one for the EXPORT gate — a diagnostic bundle / artifact / trace
10
+ * that leaves the machine must fail toward NOT leaking, i.e. recall-first. §8.1
11
+ * asks to prove a redactor with a real corpus (provider tokens, JWT, Bearer,
12
+ * PEM, cloud creds, connection strings, cookies, `.env` variants) plus counter-
13
+ * examples (sample values, hashes, UUIDs, plain base64) and to report recall,
14
+ * false-positive rate, P95 latency and max input size — per surface.
15
+ *
16
+ * This module is that recall-first scanner (`scanSecrets` / `redactSecrets`),
17
+ * the labeled `SECRET_CORPUS`, and `runRedactionBenchmark` which measures ANY
18
+ * redactor against it. It does NOT touch the precision-first IDE redactor, so
19
+ * default IDE-context behavior is unchanged; this is the scanner the future
20
+ * "Secret Scan before export" gate (P1-9 诊断包) runs.
21
+ *
22
+ * Pure: no fs / RNG / clock. Latency is measured only when a `clock` is
23
+ * injected, keeping the module deterministic in tests.
24
+ */
25
+
26
+ import { isSecretEnvName } from "./credential-guard.js";
27
+
28
+ export const REDACTED = "[REDACTED]";
29
+
30
+ /** The export surfaces §8.1 wants measured independently. */
31
+ export const SURFACES = Object.freeze([
32
+ "source_context",
33
+ "transcript",
34
+ "trace",
35
+ "artifact",
36
+ "diagnostic_bundle",
37
+ ]);
38
+
39
+ /** The secret categories the scanner covers. */
40
+ export const SECRET_CATEGORIES = Object.freeze([
41
+ "provider_token",
42
+ "jwt",
43
+ "bearer",
44
+ "pem_private_key",
45
+ "connection_string",
46
+ "cookie",
47
+ "env_assignment",
48
+ ]);
49
+
50
+ // Each pattern is stored as a SOURCE string so `scanSecrets` compiles a fresh
51
+ // RegExp per call — a shared /g/ regex carries `lastIndex` state across calls
52
+ // and would skip matches. `replace` resets state itself, but scan/match do not.
53
+ const PATTERNS = [
54
+ {
55
+ category: "provider_token",
56
+ // OpenAI/Anthropic sk-, GitHub PAT/OAuth, GitLab PAT, Slack xox*, AWS keys.
57
+ source:
58
+ "\\b(?:sk-[A-Za-z0-9_-]{16,}|gh[pousr]_[A-Za-z0-9]{20,}|github_pat_[A-Za-z0-9_]{20,}|glpat-[A-Za-z0-9_-]{16,}|xox[baprs]-[A-Za-z0-9-]{10,}|(?:AKIA|ASIA)[0-9A-Z]{16})\\b",
59
+ flags: "g",
60
+ replace: () => REDACTED,
61
+ },
62
+ {
63
+ category: "jwt",
64
+ // Three base64url segments; the first two begin `eyJ` (base64 of `{"`).
65
+ source:
66
+ "\\beyJ[A-Za-z0-9_-]{6,}\\.eyJ[A-Za-z0-9_-]{6,}\\.[A-Za-z0-9_-]{6,}\\b",
67
+ flags: "g",
68
+ replace: () => REDACTED,
69
+ },
70
+ {
71
+ category: "bearer",
72
+ source: "\\bBearer\\s+[A-Za-z0-9\\-._~+/=]{8,}",
73
+ flags: "g",
74
+ replace: () => `Bearer ${REDACTED}`,
75
+ },
76
+ {
77
+ category: "pem_private_key",
78
+ // The bytes ARE the secret; a truncated END marker still redacts to the end.
79
+ source:
80
+ "-----BEGIN [A-Z0-9 ]*PRIVATE KEY-----[\\s\\S]*?(?:-----END [A-Z0-9 ]*PRIVATE KEY-----|$)",
81
+ flags: "g",
82
+ replace: () => REDACTED,
83
+ },
84
+ {
85
+ category: "connection_string",
86
+ // scheme://user:PASSWORD@host — the embedded password is the secret. Only
87
+ // the credential form (with `:pass@`) matches, so `https://example.com` is
88
+ // untouched. Keep the scheme + user, swallow the password + host tail.
89
+ source:
90
+ "\\b((?:postgres|postgresql|mysql|mongodb(?:\\+srv)?|redis|rediss|amqp|amqps|https?|ftp|ssh):\\/\\/[^:\\s/@]+:)[^@\\s/]+@\\S+",
91
+ flags: "gi",
92
+ replace: (_m, prefix) => `${prefix}${REDACTED}`,
93
+ },
94
+ {
95
+ category: "cookie",
96
+ // A Cookie / Set-Cookie header value. Keep the header name, swallow the value.
97
+ source: "\\b(Set-Cookie|Cookie)(\\s*:\\s*)\\S[^\\n\\r]*",
98
+ flags: "gi",
99
+ replace: (_m, name, sep) => `${name}${sep}${REDACTED}`,
100
+ },
101
+ {
102
+ category: "env_assignment",
103
+ // `API_KEY = "value"` / `token: value`. The key must contain a boundary-
104
+ // delimited secret word (isSecretEnvName), the value a bare token-like run.
105
+ // The lookahead rejects `token = getToken()` (code, not a secret).
106
+ source:
107
+ "\\b([A-Za-z_][A-Za-z0-9_-]*)([\"']?\\s*[=:]\\s*)([\"']?)([A-Za-z0-9+/=_.\\-]{8,})\\3(?![A-Za-z0-9+/=_.\\-(])",
108
+ flags: "g",
109
+ replace: (m, key, sep, quote) =>
110
+ isSecretEnvName(key.replace(/-/g, "_"))
111
+ ? `${key}${sep}${quote}${REDACTED}${quote}`
112
+ : m, // key isn't secret-shaped → leave the assignment alone
113
+ // env_assignment only counts as a detection when the key gates true; a
114
+ // guard so scanSecrets doesn't report `count = 12345678` as a secret.
115
+ guard: (m, groups) => isSecretEnvName(String(groups[0]).replace(/-/g, "_")),
116
+ },
117
+ ];
118
+
119
+ /**
120
+ * Find every secret-shaped span in `text`. Recall-first: overlapping categories
121
+ * all report, so an assignment holding a JWT is caught by both.
122
+ *
123
+ * @param {string} text
124
+ * @returns {Array<{category:string, index:number, length:number, match:string}>}
125
+ */
126
+ export function scanSecrets(text) {
127
+ if (typeof text !== "string" || text.length === 0) return [];
128
+ const findings = [];
129
+ for (const p of PATTERNS) {
130
+ const re = new RegExp(p.source, p.flags);
131
+ for (const m of text.matchAll(re)) {
132
+ if (p.guard && !p.guard(m[0], m.slice(1))) continue;
133
+ findings.push({
134
+ category: p.category,
135
+ index: m.index,
136
+ length: m[0].length,
137
+ match: m[0],
138
+ });
139
+ }
140
+ }
141
+ return findings.sort((a, b) => a.index - b.index || a.length - b.length);
142
+ }
143
+
144
+ /**
145
+ * Redact every secret-shaped span, keeping the surviving structure a human /
146
+ * agent still needs (the `Bearer` word, a header/key name, a connection scheme)
147
+ * while swallowing the secret value.
148
+ *
149
+ * @param {string} text
150
+ * @returns {string}
151
+ */
152
+ export function redactSecrets(text) {
153
+ if (typeof text !== "string" || text.length === 0) return text;
154
+ let out = text;
155
+ for (const p of PATTERNS) {
156
+ const re = new RegExp(p.source, p.flags);
157
+ out = out.replace(re, p.replace);
158
+ }
159
+ return out;
160
+ }
161
+
162
+ /** Whether `text` contains any secret (the export gate's fail-closed check). */
163
+ export function containsSecret(text) {
164
+ return scanSecrets(text).length > 0;
165
+ }
166
+
167
+ // ── Labeled corpus ───────────────────────────────────────────────────────────
168
+ //
169
+ // Positives carry the exact `secret` substring that MUST disappear after
170
+ // redaction; negatives (sample/placeholder values, hashes, UUIDs, plain base64,
171
+ // ordinary code) MUST pass through byte-for-byte. Items are spread across the
172
+ // export surfaces so the benchmark can report per-surface.
173
+
174
+ const POSITIVES = [
175
+ {
176
+ id: "openai",
177
+ category: "provider_token",
178
+ surface: "transcript",
179
+ secret: "sk-abcd1234efgh5678ijkl",
180
+ text: 'client = OpenAI(api_key="sk-abcd1234efgh5678ijkl")',
181
+ },
182
+ {
183
+ id: "github_pat",
184
+ category: "provider_token",
185
+ surface: "artifact",
186
+ secret: "ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789",
187
+ text: "git remote set-url origin https://ghp_ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789@github.com/x/y",
188
+ },
189
+ {
190
+ id: "aws",
191
+ category: "provider_token",
192
+ surface: "diagnostic_bundle",
193
+ secret: "AKIAIOSFODNN7EXAMPLE",
194
+ text: "aws_access_key_id AKIAIOSFODNN7EXAMPLE",
195
+ },
196
+ {
197
+ id: "jwt",
198
+ category: "jwt",
199
+ surface: "trace",
200
+ secret:
201
+ "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.dozjgNryP4J3jVmNHl0w5N_XgL0n3I9PlFUP0THsR8U",
202
+ text: "Authorization header carried eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.dozjgNryP4J3jVmNHl0w5N_XgL0n3I9PlFUP0THsR8U",
203
+ },
204
+ {
205
+ id: "bearer",
206
+ category: "bearer",
207
+ surface: "source_context",
208
+ secret: "aG9sZDpteS1zZWNyZXQtdG9rZW4",
209
+ text: "curl -H 'Authorization: Bearer aG9sZDpteS1zZWNyZXQtdG9rZW4' https://api",
210
+ },
211
+ {
212
+ id: "pem",
213
+ category: "pem_private_key",
214
+ surface: "artifact",
215
+ secret:
216
+ "-----BEGIN RSA PRIVATE KEY-----\nMIIBOwIBAAJBAKj34Gkx...\n-----END RSA PRIVATE KEY-----",
217
+ text: "key file:\n-----BEGIN RSA PRIVATE KEY-----\nMIIBOwIBAAJBAKj34Gkx...\n-----END RSA PRIVATE KEY-----\n",
218
+ },
219
+ {
220
+ id: "postgres",
221
+ category: "connection_string",
222
+ surface: "diagnostic_bundle",
223
+ secret: "s3cr3tP@ss",
224
+ text: "DATABASE_URL=postgres://appuser:s3cr3tP@ss@db.internal:5432/prod",
225
+ },
226
+ {
227
+ id: "mongodb",
228
+ category: "connection_string",
229
+ surface: "trace",
230
+ secret: "hunter2hunter2",
231
+ text: "mongodb+srv://svc:hunter2hunter2@cluster0.mongodb.net/app",
232
+ },
233
+ {
234
+ id: "cookie",
235
+ category: "cookie",
236
+ surface: "transcript",
237
+ secret: "sessionid=9f8b7c6d5e4f3a2b1c0d",
238
+ text: "Set-Cookie: sessionid=9f8b7c6d5e4f3a2b1c0d; HttpOnly",
239
+ },
240
+ {
241
+ id: "env_password",
242
+ category: "env_assignment",
243
+ surface: "diagnostic_bundle",
244
+ secret: "Sup3rSecret_Value-123",
245
+ text: 'DB_PASSWORD="Sup3rSecret_Value-123"',
246
+ },
247
+ ];
248
+
249
+ const NEGATIVES = [
250
+ {
251
+ id: "uuid",
252
+ surface: "trace",
253
+ text: "requestId = 550e8400-e29b-41d4-a716-446655440000",
254
+ },
255
+ {
256
+ id: "git_sha",
257
+ surface: "transcript",
258
+ text: "commit beaa3f9ec7069258aad5e8587d32f779da5e5d20 landed",
259
+ },
260
+ {
261
+ id: "plain_base64",
262
+ surface: "artifact",
263
+ text: "payload blob aGVsbG8gd29ybGQgdGhpcyBpcyBub3QgYSBzZWNyZXQ=",
264
+ },
265
+ {
266
+ id: "prose",
267
+ surface: "source_context",
268
+ text: "The function returns the number of open editor tabs.",
269
+ },
270
+ {
271
+ id: "code_call",
272
+ surface: "source_context",
273
+ text: "const token = getToken(config)",
274
+ },
275
+ {
276
+ id: "nonsecret_assignment",
277
+ surface: "diagnostic_bundle",
278
+ text: "retryCount = 12345678",
279
+ },
280
+ {
281
+ id: "plain_url",
282
+ surface: "artifact",
283
+ text: "docs at https://example.com/guide/index.html",
284
+ },
285
+ {
286
+ id: "hex_digest",
287
+ surface: "diagnostic_bundle",
288
+ text: "sha256 9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08",
289
+ },
290
+ ];
291
+
292
+ export const SECRET_CORPUS = Object.freeze({
293
+ positives: Object.freeze(POSITIVES.map((p) => Object.freeze(p))),
294
+ negatives: Object.freeze(NEGATIVES.map((n) => Object.freeze(n))),
295
+ });
296
+
297
+ function percentile(sortedAsc, p) {
298
+ if (sortedAsc.length === 0) return null;
299
+ const idx = Math.min(
300
+ sortedAsc.length - 1,
301
+ Math.max(0, Math.ceil((p / 100) * sortedAsc.length) - 1),
302
+ );
303
+ return sortedAsc[idx];
304
+ }
305
+
306
+ /**
307
+ * Measure a redactor against a labeled corpus: recall (positives whose secret
308
+ * disappeared), false-positive rate (negatives that were altered), max input
309
+ * size, and — only when a `clock` is injected — P95 redaction latency. Reports
310
+ * overall, per category (recall) and per surface (recall + FPR).
311
+ *
312
+ * @param {object} [corpus=SECRET_CORPUS]
313
+ * @param {object} [opts]
314
+ * @param {(text:string)=>string} [opts.redactor=redactSecrets]
315
+ * @param {()=>number} [opts.clock] ms clock; enables latency metrics
316
+ * @returns {object} the benchmark report
317
+ */
318
+ export function runRedactionBenchmark(corpus = SECRET_CORPUS, opts = {}) {
319
+ const redactor =
320
+ typeof opts.redactor === "function" ? opts.redactor : redactSecrets;
321
+ const clock = typeof opts.clock === "function" ? opts.clock : null;
322
+ const positives = Array.isArray(corpus?.positives) ? corpus.positives : [];
323
+ const negatives = Array.isArray(corpus?.negatives) ? corpus.negatives : [];
324
+
325
+ const latencies = [];
326
+ let maxInputBytes = 0;
327
+ const measure = (text) => {
328
+ maxInputBytes = Math.max(maxInputBytes, Buffer.byteLength(text, "utf8"));
329
+ if (!clock) return redactor(text);
330
+ const t0 = Number(clock());
331
+ const out = redactor(text);
332
+ const t1 = Number(clock());
333
+ if (Number.isFinite(t0) && Number.isFinite(t1)) latencies.push(t1 - t0);
334
+ return out;
335
+ };
336
+
337
+ const catRecall = {}; // category -> {detected, total}
338
+ const surface = {}; // surface -> {detected, posTotal, falsePos, negTotal}
339
+ const bumpSurface = (s) =>
340
+ (surface[s] = surface[s] || {
341
+ detected: 0,
342
+ posTotal: 0,
343
+ falsePos: 0,
344
+ negTotal: 0,
345
+ });
346
+
347
+ const missed = [];
348
+ let detected = 0;
349
+ for (const item of positives) {
350
+ const out = measure(item.text);
351
+ const ok = typeof item.secret === "string" && !out.includes(item.secret);
352
+ const cat = (catRecall[item.category] = catRecall[item.category] || {
353
+ detected: 0,
354
+ total: 0,
355
+ });
356
+ cat.total += 1;
357
+ const s = bumpSurface(item.surface);
358
+ s.posTotal += 1;
359
+ if (ok) {
360
+ detected += 1;
361
+ cat.detected += 1;
362
+ s.detected += 1;
363
+ } else {
364
+ missed.push(item.id);
365
+ }
366
+ }
367
+
368
+ const falsePositives = [];
369
+ let fp = 0;
370
+ for (const item of negatives) {
371
+ const out = measure(item.text);
372
+ const s = bumpSurface(item.surface);
373
+ s.negTotal += 1;
374
+ if (out !== item.text) {
375
+ fp += 1;
376
+ s.falsePos += 1;
377
+ falsePositives.push(item.id);
378
+ }
379
+ }
380
+
381
+ const byCategory = {};
382
+ for (const [c, v] of Object.entries(catRecall)) {
383
+ byCategory[c] = { recall: v.total ? v.detected / v.total : 1, ...v };
384
+ }
385
+ const bySurface = {};
386
+ for (const [sName, v] of Object.entries(surface)) {
387
+ bySurface[sName] = {
388
+ recall: v.posTotal ? v.detected / v.posTotal : 1,
389
+ falsePositiveRate: v.negTotal ? v.falsePos / v.negTotal : 0,
390
+ ...v,
391
+ };
392
+ }
393
+
394
+ const sorted = latencies.slice().sort((a, b) => a - b);
395
+ return {
396
+ overall: {
397
+ recall: positives.length ? detected / positives.length : 1,
398
+ falsePositiveRate: negatives.length ? fp / negatives.length : 0,
399
+ positives: positives.length,
400
+ negatives: negatives.length,
401
+ maxInputBytes,
402
+ p95LatencyMs: clock ? percentile(sorted, 95) : null,
403
+ },
404
+ missed,
405
+ falsePositives,
406
+ byCategory,
407
+ bySurface,
408
+ };
409
+ }
Binary file