chainlesschain 0.162.160 → 0.162.162

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 (199) hide show
  1. package/package.json +1 -1
  2. package/src/assets/web-panel/assets/{AIOps-CkRHjW8e.js → AIOps-ClI2ALgv.js} +1 -1
  3. package/src/assets/web-panel/assets/{ActionButton-BP3MeNi2.js → ActionButton-BDjRcJaK.js} +1 -1
  4. package/src/assets/web-panel/assets/{Analytics-CjbjQsOr.js → Analytics-DikSRsaT.js} +2 -2
  5. package/src/assets/web-panel/assets/{AppLayout-i0adH1M8.js → AppLayout-9mLhMFmz.js} +5 -5
  6. package/src/assets/web-panel/assets/{Artifacts-CCCG2GOU.js → Artifacts-BSr88BrH.js} +1 -1
  7. package/src/assets/web-panel/assets/{Audit-DrsgvwHC.js → Audit-CdvyimOF.js} +1 -1
  8. package/src/assets/web-panel/assets/{BackgroundAgents-DkXNkbZM.js → BackgroundAgents-D3_hCedh.js} +1 -1
  9. package/src/assets/web-panel/assets/{Backup-DarTWCex.js → Backup-Cyvv9RHf.js} +1 -1
  10. package/src/assets/web-panel/assets/{BaseInput-DaDzhcto.js → BaseInput-BZM14kt8.js} +1 -1
  11. package/src/assets/web-panel/assets/{Chat-C4EzGw6V.js → Chat-DrQSokVz.js} +5 -5
  12. package/src/assets/web-panel/assets/{ChatBubbleRenderer-lkavIWMS.js → ChatBubbleRenderer-HwJR-C6B.js} +1 -1
  13. package/src/assets/web-panel/assets/{Checkbox-gddNgaQi.js → Checkbox-bFLYeWMV.js} +1 -1
  14. package/src/assets/web-panel/assets/{Codegen-BKldFWf9.js → Codegen-7mN7xJXT.js} +1 -1
  15. package/src/assets/web-panel/assets/{Col-ChR-brDs.js → Col-DAB5ZWm7.js} +1 -1
  16. package/src/assets/web-panel/assets/{Community-DlHtZX86.js → Community-B_1TymaK.js} +1 -1
  17. package/src/assets/web-panel/assets/{Compact-BAhfcBpC.js → Compact-Bq1uSWLJ.js} +1 -1
  18. package/src/assets/web-panel/assets/{Compliance-gIrKkm6m.js → Compliance-CyiE1jEe.js} +1 -1
  19. package/src/assets/web-panel/assets/{Cowork-BYnzLBO3.js → Cowork-Cpk0D7vm.js} +2 -2
  20. package/src/assets/web-panel/assets/{Cron-ByUDIljI.js → Cron-d_Lv3rdB.js} +2 -2
  21. package/src/assets/web-panel/assets/{Crosschain-DOdNbHFp.js → Crosschain-DMqhHjMR.js} +1 -1
  22. package/src/assets/web-panel/assets/{DID-BJHaNWD2.js → DID-Br-oPuBw.js} +2 -2
  23. package/src/assets/web-panel/assets/{Dashboard-Drm_tClz.js → Dashboard-CfLW_RaP.js} +2 -2
  24. package/src/assets/web-panel/assets/{Dropdown-CA1WLzGN.js → Dropdown-DZnTOExV.js} +1 -1
  25. package/src/assets/web-panel/assets/{EmailListRenderer-Di1SKjNO.js → EmailListRenderer-DHIUHDv1.js} +1 -1
  26. package/src/assets/web-panel/assets/{FamilyGuardDashboard-44Ve7ZFe.js → FamilyGuardDashboard-Bw3t3myu.js} +1 -1
  27. package/src/assets/web-panel/assets/{Federation-CubPwv_N.js → Federation-D9cpQPAY.js} +1 -1
  28. package/src/assets/web-panel/assets/{FormItemContext-CsUFe5bU.js → FormItemContext-Jdczr9Mr.js} +1 -1
  29. package/src/assets/web-panel/assets/GenericCardRenderer-BT8ZMpC5.js +1 -0
  30. package/src/assets/web-panel/assets/{Git-D-Qx9dhO.js → Git-DjARip2b.js} +2 -2
  31. package/src/assets/web-panel/assets/{Governance-_YMYaU4n.js → Governance-BgWbSRcj.js} +1 -1
  32. package/src/assets/web-panel/assets/{Inference-J7Yzh-K_.js → Inference-CfouJciM.js} +1 -1
  33. package/src/assets/web-panel/assets/{KnowledgeGraph-D37iI_Mo.js → KnowledgeGraph-50l7mrAr.js} +1 -1
  34. package/src/assets/web-panel/assets/{Logs-oMTSYfGY.js → Logs-rq50H84B.js} +2 -2
  35. package/src/assets/web-panel/assets/{MarkdownRenderer-BqaPmC0W.js → MarkdownRenderer-DUPIEdQH.js} +1 -1
  36. package/src/assets/web-panel/assets/{Marketplace-DKUqYDaF.js → Marketplace-BYgPDmHU.js} +1 -1
  37. package/src/assets/web-panel/assets/{McpTools-BEv3Gx61.js → McpTools-5DVgcFdx.js} +3 -3
  38. package/src/assets/web-panel/assets/{Memory-CtA8XD2y.js → Memory-B70LPk7f.js} +2 -2
  39. package/src/assets/web-panel/assets/{MobileBridge-ug7c1mMj.js → MobileBridge-De8ssA5Y.js} +1 -1
  40. package/src/assets/web-panel/assets/MobileProjects-D10VISps.js +1 -0
  41. package/src/assets/web-panel/assets/{Mtc-WJaMs4i2.js → Mtc-RJsOTsIA.js} +2 -2
  42. package/src/assets/web-panel/assets/{MtcAudit-DXnK_-ru.js → MtcAudit-Crh-6BEK.js} +5 -5
  43. package/src/assets/web-panel/assets/{Multisig-C4Ev7726.js → Multisig-wuoR5l3x.js} +3 -3
  44. package/src/assets/web-panel/assets/{NLProgramming-BMFb9_zk.js → NLProgramming-U2Mji4fZ.js} +1 -1
  45. package/src/assets/web-panel/assets/{Notes-Bl93nvBX.js → Notes-CdTZAyDH.js} +3 -3
  46. package/src/assets/web-panel/assets/{NotificationSettings-BN2W5cl6.js → NotificationSettings-C-FucWE7.js} +1 -1
  47. package/src/assets/web-panel/assets/{OrderTableRenderer-Dxc9oN4o.js → OrderTableRenderer-DCYFTeaI.js} +1 -1
  48. package/src/assets/web-panel/assets/{Organization-CAs48637.js → Organization-an3OF85L.js} +4 -4
  49. package/src/assets/web-panel/assets/{Overflow-Bhq27Zoc.js → Overflow-C-1vmYFe.js} +1 -1
  50. package/src/assets/web-panel/assets/{P2P-D61-2542.js → P2P-Cwh47xKg.js} +2 -2
  51. package/src/assets/web-panel/assets/{PdhVaultBrowser-C1j4iUxz.js → PdhVaultBrowser-Dm38GT0k.js} +3 -3
  52. package/src/assets/web-panel/assets/{Permissions-rJ2-fScM.js → Permissions-DS2Y8awD.js} +4 -4
  53. package/src/assets/web-panel/assets/{PersonalDataHub-DFIlTok6.js → PersonalDataHub-BQNziA0I.js} +3 -3
  54. package/src/assets/web-panel/assets/{Pipeline-BjIgEWoZ.js → Pipeline-DF85L1ue.js} +1 -1
  55. package/src/assets/web-panel/assets/{Privacy-BQoH5wcX.js → Privacy-BuLornF-.js} +1 -1
  56. package/src/assets/web-panel/assets/{ProjectInit-CYR_-_Ub.js → ProjectInit-C3Y31IwX.js} +2 -2
  57. package/src/assets/web-panel/assets/{ProjectSettings-nkatCq-9.js → ProjectSettings-BCPiI8aD.js} +2 -2
  58. package/src/assets/web-panel/assets/Projects-BZ6s4k6g.js +1 -0
  59. package/src/assets/web-panel/assets/{Providers-BnKM9fsF.js → Providers-D3Wp022X.js} +1 -1
  60. package/src/assets/web-panel/assets/{QrScannerModal-Cfe82dbz.js → QrScannerModal-CUJfQkH4.js} +1 -1
  61. package/src/assets/web-panel/assets/{QuickAsk-r1RbPufV.js → QuickAsk-UAc0EO8l.js} +1 -1
  62. package/src/assets/web-panel/assets/{Recommend-CjCbM2WE.js → Recommend-BaxKupcs.js} +1 -1
  63. package/src/assets/web-panel/assets/{RemoteSession-BuMGPH6V.js → RemoteSession-C75NNnXZ.js} +2 -2
  64. package/src/assets/web-panel/assets/{Reputation-Bmj3NY1F.js → Reputation-BVXynZAk.js} +1 -1
  65. package/src/assets/web-panel/assets/{Row-BIsNcE_1.js → Row-C5MRZ0bi.js} +1 -1
  66. package/src/assets/web-panel/assets/{RssFeed-BObmojD2.js → RssFeed-DAKkEYka.js} +3 -3
  67. package/src/assets/web-panel/assets/{Search-BRmbkQNS.js → Search-DvWv9YOA.js} +1 -1
  68. package/src/assets/web-panel/assets/{Security-_rF3DRpY.js → Security-B5peafW1.js} +4 -4
  69. package/src/assets/web-panel/assets/{Services-D0If9ZV4.js → Services-UQmvPEaG.js} +2 -2
  70. package/src/assets/web-panel/assets/{Skeleton-o7zIksUg.js → Skeleton-C3bhWpuF.js} +1 -1
  71. package/src/assets/web-panel/assets/{Skills-oUR7zi9u.js → Skills-DfkKf9_H.js} +1 -1
  72. package/src/assets/web-panel/assets/{Sla-fqKyGi03.js → Sla-DqMSjlBM.js} +1 -1
  73. package/src/assets/web-panel/assets/{SpeechSettings-B1wZ_gxx.js → SpeechSettings-C-WbxWUX.js} +1 -1
  74. package/src/assets/web-panel/assets/{SyncSettings-B3qJWYXq.js → SyncSettings-CYuHZ4N2.js} +2 -2
  75. package/src/assets/web-panel/assets/{Tasks-BH5_dUfr.js → Tasks-CyeAyB9d.js} +1 -1
  76. package/src/assets/web-panel/assets/{Templates-CCIVpjRP.js → Templates-BTCeQnpq.js} +1 -1
  77. package/src/assets/web-panel/assets/{Tenant-2GZtTGnB.js → Tenant-BifQuA9Y.js} +1 -1
  78. package/src/assets/web-panel/assets/{Terminal-BlMkfs_u.js → Terminal-Bx_9Bocf.js} +2 -2
  79. package/src/assets/web-panel/assets/{TimelineRenderer-U6hi5OMc.js → TimelineRenderer-BX0RFCOm.js} +1 -1
  80. package/src/assets/web-panel/assets/{Tokens-C-_BerGJ.js → Tokens-CGHrCmUS.js} +1 -1
  81. package/src/assets/web-panel/assets/{Trigger-DaKzBgN3.js → Trigger-DlqaY4SO.js} +1 -1
  82. package/src/assets/web-panel/assets/{Trust-CLvcsco3.js → Trust-DfOmj-ay.js} +1 -1
  83. package/src/assets/web-panel/assets/{UkeySign-BFn_Y1zZ.js → UkeySign-B72jH5fE.js} +1 -1
  84. package/src/assets/web-panel/assets/{VideoEditing-B0V18dLT.js → VideoEditing-BT-63fqv.js} +1 -1
  85. package/src/assets/web-panel/assets/{Wallet-BbmP7Imu.js → Wallet-Dsul3sHr.js} +3 -3
  86. package/src/assets/web-panel/assets/{WebAuthn-Dewj_0kt.js → WebAuthn-Izkwwdgy.js} +4 -4
  87. package/src/assets/web-panel/assets/{WorkflowEditor-DOxz3Lwa.js → WorkflowEditor-D3FHSiH1.js} +1 -1
  88. package/src/assets/web-panel/assets/{chat-B1OIpZnh.js → chat-CmVz5z3U.js} +1 -1
  89. package/src/assets/web-panel/assets/{colors-CSGNVdri.js → colors-CuLgyJBp.js} +1 -1
  90. package/src/assets/web-panel/assets/{compact-item-8S0vcg0u.js → compact-item-Ml1YkXpw.js} +1 -1
  91. package/src/assets/web-panel/assets/{createContext-vZyc27TN.js → createContext-BvjJ48bs.js} +1 -1
  92. package/src/assets/web-panel/assets/devWarning-DhiGrbsh.js +1 -0
  93. package/src/assets/web-panel/assets/{hasIn-DWw3-UhA.js → hasIn-DVC0157C.js} +1 -1
  94. package/src/assets/web-panel/assets/{index-DLAvUtOH.js → index-4SBta3d7.js} +1 -1
  95. package/src/assets/web-panel/assets/{index-BZnauCqN.js → index-4wcgvPoF.js} +1 -1
  96. package/src/assets/web-panel/assets/{index-t-q3rkVm.js → index-B0S8NzO6.js} +1 -1
  97. package/src/assets/web-panel/assets/index-B7a1TrU6.js +1 -0
  98. package/src/assets/web-panel/assets/{index-BwKrTVV0.js → index-BUt8kTIB.js} +1 -1
  99. package/src/assets/web-panel/assets/{index-BQssf0EQ.js → index-B_EvG_GW.js} +1 -1
  100. package/src/assets/web-panel/assets/{index-xx5M5Rbj.js → index-BbPtzB8M.js} +1 -1
  101. package/src/assets/web-panel/assets/{index-DKwl1AkQ.js → index-BbepULSc.js} +1 -1
  102. package/src/assets/web-panel/assets/{index-B4pP2ycr.js → index-BfuKP4qe.js} +1 -1
  103. package/src/assets/web-panel/assets/{index-D1i_8oop.js → index-BiC6dQZG.js} +1 -1
  104. package/src/assets/web-panel/assets/{index-Dc40pX3F.js → index-BjHBpRYZ.js} +1 -1
  105. package/src/assets/web-panel/assets/{index-DeCS12Mh.js → index-BpFJHiTv.js} +1 -1
  106. package/src/assets/web-panel/assets/{index-BjC-TrOb.js → index-BydjdZnl.js} +1 -1
  107. package/src/assets/web-panel/assets/{index-DcfLXAW7.js → index-BzG-x9p4.js} +1 -1
  108. package/src/assets/web-panel/assets/{index-CfEvacz7.js → index-Bzr0rB5T.js} +1 -1
  109. package/src/assets/web-panel/assets/{index-BoIPHP4R.js → index-C229ABfy.js} +1 -1
  110. package/src/assets/web-panel/assets/index-C2B1KRja.js +1 -0
  111. package/src/assets/web-panel/assets/{index-BdyokEau.js → index-CFYVQMvu.js} +1 -1
  112. package/src/assets/web-panel/assets/{index-CLPr2vC_.js → index-CXkJH-lK.js} +1 -1
  113. package/src/assets/web-panel/assets/{index-DtHWHJnj.js → index-C_H_S-DY.js} +1 -1
  114. package/src/assets/web-panel/assets/{index-Q6oyLnKC.js → index-C_XuRgGn.js} +1 -1
  115. package/src/assets/web-panel/assets/{index-BeBv896m.js → index-C_vRgTl9.js} +1 -1
  116. package/src/assets/web-panel/assets/{index-DLgkAhEE.js → index-Ck_52dz_.js} +1 -1
  117. package/src/assets/web-panel/assets/{index-tr1x96W9.js → index-CmFNvNLZ.js} +1 -1
  118. package/src/assets/web-panel/assets/{index-CB2MOS_8.js → index-CudmwNBV.js} +3 -3
  119. package/src/assets/web-panel/assets/{index-CFQWZvak.js → index-Cw0Rjy87.js} +1 -1
  120. package/src/assets/web-panel/assets/{index-BuB5P0TR.js → index-D3lD5qap.js} +1 -1
  121. package/src/assets/web-panel/assets/{index-D8qXVfoV.js → index-DC_OI9H2.js} +1 -1
  122. package/src/assets/web-panel/assets/{index-C6r27A-o.js → index-DDGaOWBV.js} +1 -1
  123. package/src/assets/web-panel/assets/{index-CbZqw7pg.js → index-DJyN28Et.js} +1 -1
  124. package/src/assets/web-panel/assets/{index-CJgKP2cm.js → index-DYXHFr4r.js} +1 -1
  125. package/src/assets/web-panel/assets/{index-CI3slBKI.js → index-Dt5KMjwJ.js} +1 -1
  126. package/src/assets/web-panel/assets/{index-CwXBblm-.js → index-DuGszaWY.js} +1 -1
  127. package/src/assets/web-panel/assets/{index-BKEmQQvR.js → index-DukDVXCv.js} +1 -1
  128. package/src/assets/web-panel/assets/{index-Dg0Izipr.js → index-KMqhcF7l.js} +1 -1
  129. package/src/assets/web-panel/assets/{index-BkWd6YL7.js → index-KjgKdZe-.js} +1 -1
  130. package/src/assets/web-panel/assets/{index-DPgY5GLy.js → index-QXzlT9KD.js} +1 -1
  131. package/src/assets/web-panel/assets/{index-BDHyovnK.js → index-vG4XSJ1d.js} +1 -1
  132. package/src/assets/web-panel/assets/{index-dxocDVD_.js → index-wvb_FtaV.js} +1 -1
  133. package/src/assets/web-panel/assets/{initDefaultProps-Di5YFId0.js → initDefaultProps-BDgd2YZ8.js} +1 -1
  134. package/src/assets/web-panel/assets/{motion-BVSN0aeL.js → motion-ErtrXdYe.js} +1 -1
  135. package/src/assets/web-panel/assets/{move-DHNiaP2l.js → move-_GPPNCgs.js} +1 -1
  136. package/src/assets/web-panel/assets/{mtc-parser-xJPf3gQD.js → mtc-parser-DRLL-8V2.js} +1 -1
  137. package/src/assets/web-panel/assets/{omit-C7qzUdkN.js → omit-p86sl023.js} +1 -1
  138. package/src/assets/web-panel/assets/{pickAttrs-pSpjg3Le.js → pickAttrs-BTu7cMLH.js} +1 -1
  139. package/src/assets/web-panel/assets/{placementArrow-cs_jL_UC.js → placementArrow-BEKz6Isr.js} +1 -1
  140. package/src/assets/web-panel/assets/{responsiveObserve-DkS_Zs4r.js → responsiveObserve-Ck42c9MP.js} +1 -1
  141. package/src/assets/web-panel/assets/{slide-B5O1Kpxo.js → slide-1CEvuPsB.js} +1 -1
  142. package/src/assets/web-panel/assets/{statusUtils-BX02GnZp.js → statusUtils-BDfdx1GJ.js} +1 -1
  143. package/src/assets/web-panel/assets/{styleChecker-EmBYRftd.js → styleChecker-B4K46m5w.js} +1 -1
  144. package/src/assets/web-panel/assets/{useFlexGapSupport-ZGzN1cU8.js → useFlexGapSupport-DoIVpJNq.js} +1 -1
  145. package/src/assets/web-panel/assets/{useFs-CfP7M1m7.js → useFs-DdAWi-ln.js} +1 -1
  146. package/src/assets/web-panel/assets/{usePersonalDataHub-CbIPHgNh.js → usePersonalDataHub-iUu6k8dx.js} +1 -1
  147. package/src/assets/web-panel/assets/{vnode-7xh0If_O.js → vnode-DZz4R6Tw.js} +1 -1
  148. package/src/assets/web-panel/assets/{zoom-DmitvTR2.js → zoom-DXlf9sA3.js} +1 -1
  149. package/src/assets/web-panel/index.html +1 -1
  150. package/src/commands/agenda.js +15 -1
  151. package/src/commands/agent.js +7 -0
  152. package/src/commands/background-session.js +16 -2
  153. package/src/commands/review.js +6 -1
  154. package/src/data/changelog.json +12 -2
  155. package/src/gateways/ws/background-agent-protocol.js +88 -14
  156. package/src/lib/agent-authority.js +251 -0
  157. package/src/lib/agents.js +19 -0
  158. package/src/lib/background-agent-phase.js +100 -0
  159. package/src/lib/background-agent-supervisor.js +322 -15
  160. package/src/lib/backpressure-policy.js +105 -0
  161. package/src/lib/capability-negotiation.js +212 -0
  162. package/src/lib/credential-guard.js +11 -0
  163. package/src/lib/credential-proxy.js +182 -0
  164. package/src/lib/docs-drift.js +166 -0
  165. package/src/lib/doctor-checkup.js +76 -0
  166. package/src/lib/event-seq-replay.js +167 -0
  167. package/src/lib/goal-condition-engine.js +369 -0
  168. package/src/lib/headless-manifest.js +13 -0
  169. package/src/lib/hook-event-bus.cjs +0 -0
  170. package/src/lib/ide-context.js +53 -14
  171. package/src/lib/json-schema-output.js +30 -1
  172. package/src/lib/json-schema-validate.js +691 -0
  173. package/src/lib/lsp/__tests__/diagnostics-scheduler.test.js +93 -0
  174. package/src/lib/lsp/diagnostics-scheduler.js +127 -0
  175. package/src/lib/operation-fingerprint.js +95 -0
  176. package/src/lib/plugin-runtime/capabilities.js +371 -0
  177. package/src/lib/plugin-runtime/manifest.js +30 -0
  178. package/src/lib/project-instructions.js +98 -1
  179. package/src/lib/remote-approval-bridge.js +16 -0
  180. package/src/lib/remote-path-mapping.js +229 -0
  181. package/src/lib/review-pipeline.js +227 -0
  182. package/src/lib/runtime-checkup.js +318 -0
  183. package/src/lib/schedule-planner.js +136 -0
  184. package/src/lib/session-lifecycle.js +305 -0
  185. package/src/lib/settings-hook-events.cjs +39 -13
  186. package/src/lib/subagent-contract.js +383 -0
  187. package/src/lib/telemetry-ids.js +190 -0
  188. package/src/lib/turn-binding.js +397 -0
  189. package/src/repl/bg-dashboard.js +13 -12
  190. package/src/runtime/agent-core.js +24 -10
  191. package/src/runtime/headless-stream.js +195 -11
  192. package/src/runtime/system-prompt.js +3 -0
  193. package/src/workers/background-agent-worker.js +37 -2
  194. package/src/assets/web-panel/assets/GenericCardRenderer-D7xRjPjZ.js +0 -1
  195. package/src/assets/web-panel/assets/MobileProjects-BpX5g9It.js +0 -1
  196. package/src/assets/web-panel/assets/Projects-SS9uZKCl.js +0 -1
  197. package/src/assets/web-panel/assets/devWarning-DYEZcY55.js +0 -1
  198. package/src/assets/web-panel/assets/index-DmVFfcNR.js +0 -1
  199. package/src/assets/web-panel/assets/index-x7YyhMUa.js +0 -1
@@ -27,6 +27,11 @@
27
27
  import fs from "fs";
28
28
  import path from "path";
29
29
  import semver from "semver";
30
+ import {
31
+ normalizeCapabilities,
32
+ normalizeOptionsSchema,
33
+ auditDeclaredCapabilities,
34
+ } from "./capabilities.js";
30
35
 
31
36
  export const _deps = {
32
37
  existsSync: fs.existsSync,
@@ -73,6 +78,14 @@ export function parsePluginManifest(root) {
73
78
  scope: null,
74
79
  metadata: {},
75
80
  components: emptyComponents(),
81
+ // Declarative security contract (P1): the plugin's declared capabilities
82
+ // (manifest.permissions) + typed config schema (manifest.optionsSchema),
83
+ // normalized by capabilities.js. `capabilitiesDeclared` distinguishes a
84
+ // plugin that declared a permissions block (subject to the declared-vs-
85
+ // actual audit) from a legacy one that declared nothing (unrestricted).
86
+ capabilities: normalizeCapabilities(null),
87
+ capabilitiesDeclared: false,
88
+ optionsSchema: {},
76
89
  errors,
77
90
  warnings,
78
91
  };
@@ -165,6 +178,23 @@ export function parsePluginManifest(root) {
165
178
  c.bin = resolveBin(abs, manifest, safeResolve, warnings);
166
179
  c.settings = resolveSettings(abs, manifest, safeResolve);
167
180
 
181
+ // ── declared capabilities + options schema (P1) ──
182
+ const hasPermissions =
183
+ manifest.permissions &&
184
+ typeof manifest.permissions === "object" &&
185
+ !Array.isArray(manifest.permissions);
186
+ result.capabilities = normalizeCapabilities(manifest.permissions);
187
+ result.capabilitiesDeclared = Boolean(hasPermissions);
188
+ result.optionsSchema = normalizeOptionsSchema(manifest.optionsSchema);
189
+ // Declared-vs-actual audit runs ONLY when a permissions block is present — a
190
+ // legacy plugin that declares nothing is unrestricted by design, so it never
191
+ // gets these warnings (keeps existing manifests warning-free).
192
+ if (hasPermissions) {
193
+ for (const f of auditDeclaredCapabilities(result)) {
194
+ warnings.push(`capability: ${f.reason}`);
195
+ }
196
+ }
197
+
168
198
  result.ok = errors.length === 0;
169
199
  return result;
170
200
  }
@@ -44,6 +44,83 @@ export const PROJECT_FILE_NAMES = ["cc.md", "CLAUDE.md", "AGENTS.md"];
44
44
  /** Local (gitignored) companion names, first match wins. */
45
45
  export const LOCAL_FILE_NAMES = ["cc.local.md", "CLAUDE.local.md"];
46
46
 
47
+ /**
48
+ * `instructionExcludes` — a large-monorepo reduction lever (P1). Subtrees whose
49
+ * instruction / rule / @import files should NEVER load (legacy, vendored,
50
+ * generated). Opt-in: empty by default so behavior is byte-identical unless the
51
+ * caller (config `instructionExcludes`) sets it. Patterns are matched against a
52
+ * path RELATIVE to the project root, with forward slashes:
53
+ * - a bare name (`node_modules`, `dist`) matches that segment ANYWHERE;
54
+ * - a slashed prefix (`packages/legacy`) matches that dir and everything under;
55
+ * - a glob (`**​/generated/**`, `vendor/*`) matches via `*`/`**`/`?`.
56
+ */
57
+ export function normalizeInstructionExcludes(list) {
58
+ if (!Array.isArray(list)) return [];
59
+ return [
60
+ ...new Set(
61
+ list
62
+ .map((s) =>
63
+ String(s || "")
64
+ .trim()
65
+ .replace(/\\/g, "/")
66
+ .replace(/\/+$/, ""),
67
+ )
68
+ .filter(Boolean),
69
+ ),
70
+ ];
71
+ }
72
+
73
+ function globToRegExp(glob) {
74
+ let re = "";
75
+ for (let i = 0; i < glob.length; i++) {
76
+ const c = glob[i];
77
+ if (c === "*") {
78
+ if (glob[i + 1] === "*") {
79
+ re += ".*";
80
+ i++;
81
+ if (glob[i + 1] === "/") i++; // `**/` also swallows the slash
82
+ } else {
83
+ re += "[^/]*";
84
+ }
85
+ } else if (c === "?") {
86
+ re += "[^/]";
87
+ } else {
88
+ re += c.replace(/[.+^${}()|[\]\\]/g, "\\$&");
89
+ }
90
+ }
91
+ return new RegExp("^" + re + "$");
92
+ }
93
+
94
+ /**
95
+ * True when a repo-relative path falls under an excluded subtree. A file is
96
+ * excluded when it, or any of its ancestor directories, matches a pattern.
97
+ */
98
+ export function pathIsExcluded(relPath, excludes) {
99
+ if (!excludes || excludes.length === 0) return false;
100
+ const rel = String(relPath || "")
101
+ .replace(/\\/g, "/")
102
+ .replace(/^\.\/?/, "")
103
+ .replace(/\/+$/, "");
104
+ if (!rel) return false;
105
+ const segs = rel.split("/");
106
+ for (const pat of excludes) {
107
+ if (/[*?[\]]/.test(pat)) {
108
+ const re = globToRegExp(pat);
109
+ if (re.test(rel)) return true;
110
+ let acc = "";
111
+ for (const s of segs) {
112
+ acc = acc ? `${acc}/${s}` : s;
113
+ if (re.test(acc)) return true;
114
+ }
115
+ } else {
116
+ if (rel === pat || rel.startsWith(`${pat}/`)) return true;
117
+ // a bare (slash-free) name matches that path segment anywhere
118
+ if (!pat.includes("/") && segs.includes(pat)) return true;
119
+ }
120
+ }
121
+ return false;
122
+ }
123
+
47
124
  // Same boundary rule as file-ref-expander: `@` at start / after whitespace or
48
125
  // an opening bracket-quote, so emails and decorative @ never match.
49
126
  const IMPORT_TOKEN_RE = /(^|[\s("'`[{])@([^\s"'`)\]}]+)/g;
@@ -100,13 +177,21 @@ export function findInstructionFiles(opts = {}) {
100
177
  const { fs, path, os } = resolveDeps(opts);
101
178
  const cwd = path.resolve(opts.cwd || process.cwd());
102
179
  const home = opts.home || os.homedir() || "";
180
+ const excludes = normalizeInstructionExcludes(opts.instructionExcludes);
103
181
 
104
182
  const seen = new Set();
105
183
  const out = [];
184
+ const root = findProjectRoot(cwd, opts) || cwd;
185
+ // A discovered file under an excluded subtree (relative to the project root)
186
+ // never loads. The user-scope file lives outside the repo, so it is exempt.
187
+ const excluded = (abs) =>
188
+ excludes.length > 0 &&
189
+ pathIsExcluded(path.relative(root, path.resolve(abs)), excludes);
106
190
  const push = (p, scope) => {
107
191
  if (!p) return;
108
192
  const abs = path.resolve(p);
109
193
  if (seen.has(abs) || !isFile(fs, abs)) return;
194
+ if (scope !== "user" && excluded(abs)) return;
110
195
  seen.add(abs);
111
196
  out.push({ path: abs, scope });
112
197
  };
@@ -121,7 +206,6 @@ export function findInstructionFiles(opts = {}) {
121
206
  );
122
207
  }
123
208
 
124
- const root = findProjectRoot(cwd, opts) || cwd;
125
209
  const chain = [];
126
210
  let dir = cwd;
127
211
  for (;;) {
@@ -274,6 +358,10 @@ export function loadProjectInstructions(opts = {}) {
274
358
  : DEFAULT_MAX_TOTAL_BYTES;
275
359
 
276
360
  const roots = findInstructionFiles(opts);
361
+ const excludes = normalizeInstructionExcludes(opts.instructionExcludes);
362
+ const projectRoot =
363
+ findProjectRoot(path.resolve(opts.cwd || process.cwd()), opts) ||
364
+ path.resolve(opts.cwd || process.cwd());
277
365
  const visited = new Set(roots.map((r) => r.path));
278
366
  const out = [];
279
367
  const warnings = [];
@@ -322,6 +410,15 @@ export function loadProjectInstructions(opts = {}) {
322
410
  const resolved = path.resolve(baseDir, target);
323
411
  if (visited.has(resolved) || !isFile(fs, resolved)) continue; // silent:
324
412
  // non-files are decorative @tokens (npm scopes, emails), not imports.
413
+ // An @import that resolves under an excluded subtree does not load — the
414
+ // reduction lever must not be bypassed by an import pointing into it.
415
+ if (
416
+ excludes.length > 0 &&
417
+ pathIsExcluded(path.relative(projectRoot, resolved), excludes)
418
+ ) {
419
+ visited.add(resolved);
420
+ continue;
421
+ }
325
422
  // Refuse to import a credential / secret file. `cc agent` auto-loads
326
423
  // project memory, so a cloned/untrusted repo's cc.md must NOT be able to
327
424
  // pull `@~/.ssh/id_rsa` / `@../.env` into the LLM-bound system prompt
@@ -23,6 +23,10 @@
23
23
 
24
24
  import { randomBytes } from "crypto";
25
25
  import { WsRpcClient } from "./ws-rpc-client.js";
26
+ import {
27
+ approvalFingerprintOk,
28
+ operationFingerprint,
29
+ } from "./operation-fingerprint.js";
26
30
  import {
27
31
  buildDirectPairingUri,
28
32
  pickLanAddress,
@@ -113,6 +117,15 @@ export class RemoteApprovalBridge {
113
117
  const requestId = message.event.requestId || message.event.approvalId;
114
118
  const pending = requestId ? this._pending.get(requestId) : null;
115
119
  if (!pending) return;
120
+ // Confused-deputy guard: a resolve that carries an operation fingerprint
121
+ // must match the operation this ask published. A mismatch (stale card /
122
+ // replayed / swapped body) is rejected — the ask stays pending and fails
123
+ // closed on timeout. Absent fingerprint = legacy device, accepted.
124
+ if (
125
+ !approvalFingerprintOk(pending.fingerprint, message.event.fingerprint)
126
+ ) {
127
+ return;
128
+ }
116
129
  this._pending.delete(requestId);
117
130
  clearTimeout(pending.timer);
118
131
  const answer = message.event.answer ?? message.event.approved;
@@ -150,6 +163,7 @@ export class RemoteApprovalBridge {
150
163
  onRequestId = null,
151
164
  } = {}) {
152
165
  const requestId = `ra-${process.pid}-${++this._counter}-${randomBytes(4).toString("hex")}`;
166
+ const fingerprint = operationFingerprint({ tool, action, detail });
153
167
  if (onRequestId) {
154
168
  try {
155
169
  onRequestId(requestId);
@@ -171,6 +185,7 @@ export class RemoteApprovalBridge {
171
185
  if (typeof timer.unref === "function") timer.unref();
172
186
  this._pending.set(requestId, {
173
187
  timer,
188
+ fingerprint,
174
189
  resolve: (decision) => {
175
190
  this._publish({
176
191
  type: "permission.resolved",
@@ -187,6 +202,7 @@ export class RemoteApprovalBridge {
187
202
  tool: tool || null,
188
203
  action,
189
204
  detail,
205
+ fingerprint,
190
206
  askedAt: this._now(),
191
207
  });
192
208
  });
@@ -0,0 +1,229 @@
1
+ /**
2
+ * Remote URI ↔ filesystem path mapping for the IDE bridge.
3
+ *
4
+ * A local IDE (VS Code / JetBrains) can report a file in a representation that
5
+ * does not exist on the host `cc` runs on, so inlining it into the prompt makes
6
+ * the agent's read_file/edit_file target a path the CLI host can't see:
7
+ * - a URI: file:///c:/code/my%20proj/文件.ts
8
+ * vscode-remote://wsl+Ubuntu/home/u/x
9
+ * vscode-remote://ssh-remote+host/home/u/x
10
+ * vscode-remote://dev-container+<hex>/workspaces/x
11
+ * - a WSL UNC path from a Windows host: \\wsl.localhost\Ubuntu\home\u\x
12
+ * (older Windows builds: \\wsl$\Ubuntu\home\u\x)
13
+ * - a Windows drive path while cc runs INSIDE WSL: C:\code\x == /mnt/c/code/x
14
+ *
15
+ * normalizeIdePathForCli() folds these foreign forms into the CLI's native fs
16
+ * path; anything already native (or unrecognized) passes through byte-for-byte.
17
+ * cliPathToIdeUri() is the reverse, for surfaces that must hand a URI back to
18
+ * the editor.
19
+ *
20
+ * Pure: string transforms only (no fs / process). Percent-decoding preserves
21
+ * spaces and non-ASCII (中文) exactly, so round-trips are lossless.
22
+ */
23
+
24
+ /** decodeURIComponent that never throws on a malformed % sequence. */
25
+ function decodeSafe(s) {
26
+ if (typeof s !== "string") return s;
27
+ try {
28
+ return decodeURIComponent(s);
29
+ } catch {
30
+ return s; // leave a malformed sequence as-is rather than lose the value
31
+ }
32
+ }
33
+
34
+ /** Has a `scheme://` prefix (a URI, not a bare path). */
35
+ export function looksLikeUri(value) {
36
+ return typeof value === "string" && /^[a-z][a-z0-9.+-]*:\/\//i.test(value);
37
+ }
38
+
39
+ /** `C:\…` or `C:/…` — a Windows drive-absolute path. */
40
+ export function looksLikeWindowsPath(value) {
41
+ return typeof value === "string" && /^[a-zA-Z]:[\\/]/.test(value);
42
+ }
43
+
44
+ /** `\\host\…` or `//host/…` — a UNC path. */
45
+ function looksLikeUnc(value) {
46
+ return typeof value === "string" && /^(\\\\|\/\/)[^\\/]/.test(value);
47
+ }
48
+
49
+ const WSL_UNC_RE =
50
+ /^(?:\\\\|\/\/)(?:wsl\.localhost|wsl\$)[\\/]([^\\/]+)[\\/]?(.*)$/i;
51
+
52
+ /**
53
+ * `\\wsl.localhost\Ubuntu\home\u\x` (or `\\wsl$\…`, forward-slash variants) →
54
+ * `{ distro:"Ubuntu", posix:"/home/u/x" }`, or null when not a WSL UNC path.
55
+ */
56
+ export function wslUncToPosix(value) {
57
+ if (typeof value !== "string") return null;
58
+ const m = value.match(WSL_UNC_RE);
59
+ if (!m) return null;
60
+ const distro = m[1];
61
+ const rest = (m[2] || "").replace(/\\/g, "/");
62
+ return { distro, posix: "/" + rest.replace(/^\/+/, "") };
63
+ }
64
+
65
+ /** `/home/u/x` + distro → `\\wsl.localhost\Ubuntu\home\u\x` (host overridable). */
66
+ export function posixToWslUnc(posix, distro, { host = "wsl.localhost" } = {}) {
67
+ if (typeof posix !== "string" || !distro) return null;
68
+ const win = posix.replace(/^\/+/, "").replace(/\//g, "\\");
69
+ return `\\\\${host}\\${distro}\\${win}`;
70
+ }
71
+
72
+ /** `C:\code\x` → `/mnt/c/code/x`, or null when not a drive path. */
73
+ export function winDriveToWslMount(value) {
74
+ if (!looksLikeWindowsPath(value)) return null;
75
+ const drive = value[0].toLowerCase();
76
+ const rest = value.slice(2).replace(/\\/g, "/").replace(/^\/+/, "");
77
+ return `/mnt/${drive}${rest ? "/" + rest : ""}`;
78
+ }
79
+
80
+ const WSL_MOUNT_RE = /^\/mnt\/([a-zA-Z])(?:\/(.*))?$/;
81
+
82
+ /** `/mnt/c/code/x` → `C:\code\x`, or null when not a /mnt/<drive> path. */
83
+ export function wslMountToWinDrive(value) {
84
+ if (typeof value !== "string") return null;
85
+ const m = value.match(WSL_MOUNT_RE);
86
+ if (!m) return null;
87
+ const drive = m[1].toUpperCase();
88
+ const rest = (m[2] || "").replace(/\//g, "\\");
89
+ return `${drive}:\\${rest}`;
90
+ }
91
+
92
+ /**
93
+ * Parse an IDE URI into its parts. Returns null for a non-URI or an unknown
94
+ * scheme (so the caller leaves it untouched).
95
+ * { scheme, remoteKind, remoteParam, fsPath }
96
+ * `fsPath` is the decoded on-host path: a Windows drive path for a `file://`
97
+ * drive URI, else POSIX. `remoteKind`/`remoteParam` are set for vscode-remote.
98
+ */
99
+ export function parseRemoteUri(uri) {
100
+ if (!looksLikeUri(uri)) return null;
101
+ const m = uri.match(/^([a-z][a-z0-9.+-]*):\/\/([^/]*)((?:\/[^?#]*)?)/i);
102
+ if (!m) return null;
103
+ const scheme = m[1].toLowerCase();
104
+ const authority = m[2] || "";
105
+ const rawPath = m[3] || "";
106
+
107
+ if (scheme === "file") {
108
+ if (authority) {
109
+ // file://server/share/… → UNC \\server\share\…
110
+ const unc = "\\\\" + authority + rawPath.replace(/\//g, "\\");
111
+ return {
112
+ scheme,
113
+ remoteKind: null,
114
+ remoteParam: null,
115
+ fsPath: decodeSafe(unc),
116
+ };
117
+ }
118
+ const decoded = decodeSafe(rawPath);
119
+ // /c:/code/x → c:\code\x (Windows drive URI); else a POSIX path.
120
+ if (/^\/[a-zA-Z]:/.test(decoded)) {
121
+ return {
122
+ scheme,
123
+ remoteKind: null,
124
+ remoteParam: null,
125
+ fsPath: decoded.slice(1).replace(/\//g, "\\"),
126
+ };
127
+ }
128
+ return {
129
+ scheme,
130
+ remoteKind: null,
131
+ remoteParam: null,
132
+ fsPath: decoded || "/",
133
+ };
134
+ }
135
+
136
+ if (scheme === "vscode-remote" || scheme === "vscode-vfs") {
137
+ // authority = "<kind>+<param>", where `+` may be percent-encoded (%2B).
138
+ const auth = decodeSafe(authority);
139
+ const plus = auth.indexOf("+");
140
+ const remoteKind = plus >= 0 ? auth.slice(0, plus) : auth;
141
+ const remoteParam = plus >= 0 ? auth.slice(plus + 1) : null;
142
+ return {
143
+ scheme,
144
+ remoteKind: remoteKind || null,
145
+ remoteParam: remoteParam || null,
146
+ fsPath: decodeSafe(rawPath) || "/",
147
+ };
148
+ }
149
+
150
+ return null; // unknown scheme — don't guess
151
+ }
152
+
153
+ /**
154
+ * Detect the running CLI's WSL context from env (best-effort). Returns
155
+ * `{ isWsl, distro }`. Injected in tests; ide-context passes process.env.
156
+ */
157
+ export function detectWsl(env = {}) {
158
+ const distro = env.WSL_DISTRO_NAME || null;
159
+ const isWsl = Boolean(distro || env.WSL_INTEROP);
160
+ return { isWsl, distro };
161
+ }
162
+
163
+ /**
164
+ * Fold an IDE-reported file value into the path `cc` can actually open on THIS
165
+ * host. Only recognized foreign forms are rewritten; a native or unrecognized
166
+ * value is returned unchanged (byte-for-byte).
167
+ *
168
+ * @param {string} value URI or path as the IDE reported it
169
+ * @param {object} [opts]
170
+ * @param {string} [opts.platform=process.platform] the CLI host platform
171
+ * @param {{isWsl?:boolean, distro?:string}} [opts.wsl] WSL context (see detectWsl)
172
+ * @returns {string}
173
+ */
174
+ export function normalizeIdePathForCli(value, opts = {}) {
175
+ if (typeof value !== "string" || value.length === 0) return value;
176
+ const platform = opts.platform || process.platform;
177
+ const wsl = opts.wsl || {};
178
+ const onWindows = platform === "win32";
179
+
180
+ // 1) Resolve a URI down to its on-host fsPath first.
181
+ let base = value;
182
+ let remoteKind = null;
183
+ let remoteParam = null;
184
+ if (looksLikeUri(value)) {
185
+ const parsed = parseRemoteUri(value);
186
+ if (!parsed) return value; // unknown scheme → leave untouched
187
+ base = parsed.fsPath;
188
+ remoteKind = parsed.remoteKind;
189
+ remoteParam = parsed.remoteParam;
190
+ }
191
+
192
+ if (onWindows) {
193
+ // Native form is a Windows path. A POSIX path from a wsl-remote URI is
194
+ // reachable from Windows as a \\wsl.localhost\<distro>\… UNC.
195
+ if (remoteKind === "wsl" && remoteParam && base.startsWith("/")) {
196
+ return posixToWslUnc(base, remoteParam) || base;
197
+ }
198
+ return base;
199
+ }
200
+
201
+ // Native form is POSIX (Linux remote-host or inside WSL).
202
+ const wslUnc = wslUncToPosix(base);
203
+ if (wslUnc) return wslUnc.posix;
204
+ if (looksLikeWindowsPath(base) && wsl.isWsl) {
205
+ return winDriveToWslMount(base) || base;
206
+ }
207
+ return base;
208
+ }
209
+
210
+ /**
211
+ * Reverse: a CLI-native fs path → a URI the editor can open. `remoteAuthority`
212
+ * (e.g. "wsl+Ubuntu", "ssh-remote+host") produces a vscode-remote URI; without
213
+ * it, a `file://` URI. Percent-encodes spaces / non-ASCII per component.
214
+ */
215
+ export function cliPathToIdeUri(fsPath, { remoteAuthority } = {}) {
216
+ if (typeof fsPath !== "string" || fsPath.length === 0) return fsPath;
217
+ const isWinDrive = looksLikeWindowsPath(fsPath);
218
+ const posixish = fsPath.replace(/\\/g, "/");
219
+ const encoded = posixish
220
+ .split("/")
221
+ .map((seg) => (/^[a-zA-Z]:$/.test(seg) ? seg : encodeURIComponent(seg)))
222
+ .join("/");
223
+ if (remoteAuthority) {
224
+ const path = encoded.startsWith("/") ? encoded : "/" + encoded;
225
+ return `vscode-remote://${remoteAuthority}${path}`;
226
+ }
227
+ const path = isWinDrive ? "/" + encoded : encoded;
228
+ return `file://${path.startsWith("/") ? "" : "/"}${path}`;
229
+ }
@@ -0,0 +1,227 @@
1
+ /**
2
+ * Multi-agent code-review pipeline core (P2 "多 Agent Code Review").
3
+ *
4
+ * `cc review` today is a single read-only agent pass. The gap upgrades it to:
5
+ * 1. multiple finders (correctness / security / performance / tests),
6
+ * 2. a verifier that independently reproduces each finding,
7
+ * 3. a deduper that merges duplicates and confidence-filters,
8
+ * 4. structured output (path / line / category / severity / failure_scenario /
9
+ * evidence).
10
+ *
11
+ * This module is the PURE aggregation core — normalize, dedup+merge, apply
12
+ * verifier verdicts, confidence-filter, rank, and render the structured report.
13
+ * The fan-out of finder/verifier AGENTS is orchestration the command layer adds;
14
+ * everything here is deterministic and testable without an LLM.
15
+ */
16
+
17
+ /** The finder dimensions a full review fans out across. */
18
+ export const REVIEW_DIMENSIONS = Object.freeze([
19
+ "correctness",
20
+ "security",
21
+ "performance",
22
+ "tests",
23
+ ]);
24
+
25
+ const SEVERITY_RANK = Object.freeze({
26
+ critical: 5,
27
+ high: 4,
28
+ medium: 3,
29
+ low: 2,
30
+ note: 1,
31
+ });
32
+ const SEVERITY_LABEL = Object.freeze({
33
+ 5: "Critical",
34
+ 4: "High",
35
+ 3: "Medium",
36
+ 2: "Low",
37
+ 1: "Note",
38
+ });
39
+
40
+ /** Canonical severity label (unknown → "Note"). */
41
+ export function normalizeSeverity(sev) {
42
+ const r = SEVERITY_RANK[String(sev || "").toLowerCase()] || 1;
43
+ return SEVERITY_LABEL[r];
44
+ }
45
+ export function severityRank(sev) {
46
+ return SEVERITY_RANK[String(sev || "").toLowerCase()] || 0;
47
+ }
48
+
49
+ /** Clamp a confidence to [0,1]; non-numeric → `dflt`. */
50
+ export function clampConfidence(v, dflt = 0.5) {
51
+ const n = Number(v);
52
+ if (!Number.isFinite(n)) return dflt;
53
+ return Math.max(0, Math.min(1, n));
54
+ }
55
+
56
+ function confOf(f) {
57
+ const n = Number(f?.confidence);
58
+ return Number.isFinite(n) ? n : 0.5;
59
+ }
60
+
61
+ function normTitle(title) {
62
+ return String(title || "")
63
+ .toLowerCase()
64
+ .replace(/\s+/g, " ")
65
+ .trim()
66
+ .slice(0, 80);
67
+ }
68
+
69
+ /** Dedup identity: same file location + same issue (title). Category-agnostic so
70
+ * two finders reporting one bug under different categories merge. */
71
+ export function findingKey(f) {
72
+ return `${f?.path || ""}:${f?.line ?? ""}:${normTitle(f?.title)}`;
73
+ }
74
+
75
+ function unionField(group, keys) {
76
+ const out = [];
77
+ const seen = new Set();
78
+ for (const f of group) {
79
+ for (const key of keys) {
80
+ const v = f?.[key];
81
+ const list = Array.isArray(v) ? v : v != null ? [v] : [];
82
+ for (const item of list) {
83
+ const s = String(item);
84
+ if (s && !seen.has(s)) {
85
+ seen.add(s);
86
+ out.push(s);
87
+ }
88
+ }
89
+ }
90
+ }
91
+ return out;
92
+ }
93
+
94
+ function maxSeverityLabel(group) {
95
+ let best = 0;
96
+ for (const f of group) best = Math.max(best, severityRank(f.severity));
97
+ return SEVERITY_LABEL[best] || "Note";
98
+ }
99
+
100
+ /**
101
+ * Merge findings that describe the SAME issue at the SAME location. The
102
+ * representative keeps the highest severity + highest confidence; categories /
103
+ * dimensions / evidence are unioned WHEN PRESENT (so a plain
104
+ * `{path,line,severity,title}` finding round-trips unchanged — the wiring into
105
+ * `parseFindings` relies on this). Shape-preserving for inputs without those
106
+ * fields.
107
+ */
108
+ export function dedupeFindings(findings) {
109
+ const groups = new Map();
110
+ const order = [];
111
+ for (const f of findings || []) {
112
+ if (!f) continue;
113
+ const key = findingKey(f);
114
+ if (!groups.has(key)) {
115
+ groups.set(key, []);
116
+ order.push(key);
117
+ }
118
+ groups.get(key).push(f);
119
+ }
120
+ return order.map((key) => {
121
+ const group = groups.get(key);
122
+ const winner = [...group].sort(
123
+ (a, b) =>
124
+ severityRank(b.severity) - severityRank(a.severity) ||
125
+ confOf(b) - confOf(a),
126
+ )[0];
127
+ const merged = { ...winner, severity: maxSeverityLabel(group) };
128
+ if (Number.isFinite(Number(winner.confidence))) {
129
+ merged.confidence = Math.max(...group.map(confOf));
130
+ }
131
+ const cats = unionField(group, ["category", "categories"]);
132
+ if (cats.length) merged.categories = cats;
133
+ const dims = unionField(group, ["dimension", "dimensions"]);
134
+ if (dims.length) merged.dimensions = dims;
135
+ if (group.length > 1) merged.mergedCount = group.length;
136
+ return merged;
137
+ });
138
+ }
139
+
140
+ /**
141
+ * Apply verifier verdicts. `verdicts` maps a findingKey → { verified, confidence?,
142
+ * note? }. A finding the verifier could NOT reproduce (`verified === false`) is
143
+ * DROPPED; a reproduced one is kept, its confidence updated and a `verified`
144
+ * flag set. Findings with no verdict pass through as unverified.
145
+ */
146
+ export function applyVerdicts(findings, verdicts = {}) {
147
+ const map =
148
+ verdicts instanceof Map
149
+ ? verdicts
150
+ : new Map(Object.entries(verdicts || {}));
151
+ const out = [];
152
+ for (const f of findings || []) {
153
+ if (!f) continue;
154
+ const v = map.get(findingKey(f));
155
+ if (!v) {
156
+ out.push(f);
157
+ continue;
158
+ }
159
+ if (v.verified === false) continue; // refuted → drop
160
+ out.push({
161
+ ...f,
162
+ verified: true,
163
+ confidence:
164
+ v.confidence != null ? clampConfidence(v.confidence) : confOf(f),
165
+ ...(v.note ? { verifierNote: String(v.note) } : {}),
166
+ });
167
+ }
168
+ return out;
169
+ }
170
+
171
+ /** Keep findings whose confidence is at least `minConfidence`. */
172
+ export function filterByConfidence(findings, { minConfidence = 0 } = {}) {
173
+ return (findings || []).filter((f) => f && confOf(f) >= minConfidence);
174
+ }
175
+
176
+ /** Sort by severity (desc) then confidence (desc) then path/line for stability. */
177
+ export function rankFindings(findings) {
178
+ return [...(findings || [])].sort(
179
+ (a, b) =>
180
+ severityRank(b.severity) - severityRank(a.severity) ||
181
+ confOf(b) - confOf(a) ||
182
+ String(a.path || "").localeCompare(String(b.path || "")) ||
183
+ (Number(a.line) || 0) - (Number(b.line) || 0),
184
+ );
185
+ }
186
+
187
+ function toOutputFinding(f) {
188
+ return {
189
+ path: String(f.path || ""),
190
+ line: Number.isFinite(Number(f.line)) ? Math.floor(Number(f.line)) : null,
191
+ category: f.category || (f.categories && f.categories[0]) || "correctness",
192
+ severity: normalizeSeverity(f.severity),
193
+ failure_scenario: String(f.failureScenario || f.failure_scenario || ""),
194
+ evidence: String(f.evidence || ""),
195
+ confidence: clampConfidence(f.confidence),
196
+ };
197
+ }
198
+
199
+ /**
200
+ * The full pipeline: dedup → apply verdicts → confidence-filter → rank → render
201
+ * the structured report (the gap's path/line/category/severity/failure_scenario/
202
+ * evidence output) plus severity/category rollups.
203
+ *
204
+ * @param {Array} rawFindings findings from all finders
205
+ * @param {object} [opts] { verdicts, minConfidence }
206
+ */
207
+ export function buildReviewReport(
208
+ rawFindings,
209
+ { verdicts = {}, minConfidence = 0 } = {},
210
+ ) {
211
+ const deduped = dedupeFindings(rawFindings);
212
+ const verified = applyVerdicts(deduped, verdicts);
213
+ const filtered = filterByConfidence(verified, { minConfidence });
214
+ const ranked = rankFindings(filtered);
215
+ const findings = ranked.map(toOutputFinding);
216
+
217
+ const bySeverity = { Critical: 0, High: 0, Medium: 0, Low: 0, Note: 0 };
218
+ const byCategory = {};
219
+ for (const f of findings) {
220
+ bySeverity[f.severity] = (bySeverity[f.severity] || 0) + 1;
221
+ byCategory[f.category] = (byCategory[f.category] || 0) + 1;
222
+ }
223
+ return {
224
+ findings,
225
+ summary: { total: findings.length, bySeverity, byCategory },
226
+ };
227
+ }