@allocator-one/rcl 4.4.19

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 (663) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1415 -0
  3. package/dist/ci.d.ts +16 -0
  4. package/dist/ci.js +42 -0
  5. package/dist/ci.js.map +1 -0
  6. package/dist/config/data-dir.d.ts +6 -0
  7. package/dist/config/data-dir.js +12 -0
  8. package/dist/config/data-dir.js.map +1 -0
  9. package/dist/config/defaults.d.ts +82 -0
  10. package/dist/config/defaults.js +89 -0
  11. package/dist/config/defaults.js.map +1 -0
  12. package/dist/config/harness.d.ts +40 -0
  13. package/dist/config/harness.js +151 -0
  14. package/dist/config/harness.js.map +1 -0
  15. package/dist/config/loader.d.ts +13 -0
  16. package/dist/config/loader.js +131 -0
  17. package/dist/config/loader.js.map +1 -0
  18. package/dist/config/provider-concurrency.d.ts +4 -0
  19. package/dist/config/provider-concurrency.js +6 -0
  20. package/dist/config/provider-concurrency.js.map +1 -0
  21. package/dist/config/providers.d.ts +3 -0
  22. package/dist/config/providers.js +9 -0
  23. package/dist/config/providers.js.map +1 -0
  24. package/dist/config/schema.d.ts +179 -0
  25. package/dist/config/schema.js +115 -0
  26. package/dist/config/schema.js.map +1 -0
  27. package/dist/consensus/claim-contract.d.ts +9 -0
  28. package/dist/consensus/claim-contract.js +307 -0
  29. package/dist/consensus/claim-contract.js.map +1 -0
  30. package/dist/consensus/claim-identity.d.ts +5 -0
  31. package/dist/consensus/claim-identity.js +39 -0
  32. package/dist/consensus/claim-identity.js.map +1 -0
  33. package/dist/consensus/deduper.d.ts +43 -0
  34. package/dist/consensus/deduper.js +1052 -0
  35. package/dist/consensus/deduper.js.map +1 -0
  36. package/dist/consensus/finding-identity.d.ts +20 -0
  37. package/dist/consensus/finding-identity.js +49 -0
  38. package/dist/consensus/finding-identity.js.map +1 -0
  39. package/dist/consensus/gating.d.ts +215 -0
  40. package/dist/consensus/gating.js +699 -0
  41. package/dist/consensus/gating.js.map +1 -0
  42. package/dist/consensus/parser.d.ts +22 -0
  43. package/dist/consensus/parser.js +255 -0
  44. package/dist/consensus/parser.js.map +1 -0
  45. package/dist/consensus/semantic-deduper.d.ts +10 -0
  46. package/dist/consensus/semantic-deduper.js +70 -0
  47. package/dist/consensus/semantic-deduper.js.map +1 -0
  48. package/dist/consensus/types.d.ts +196 -0
  49. package/dist/consensus/types.js +2 -0
  50. package/dist/consensus/types.js.map +1 -0
  51. package/dist/consensus/verification-contract.d.ts +8 -0
  52. package/dist/consensus/verification-contract.js +95 -0
  53. package/dist/consensus/verification-contract.js.map +1 -0
  54. package/dist/consensus/voter.d.ts +29 -0
  55. package/dist/consensus/voter.js +394 -0
  56. package/dist/consensus/voter.js.map +1 -0
  57. package/dist/converge/attempt-budget.d.ts +88 -0
  58. package/dist/converge/attempt-budget.js +717 -0
  59. package/dist/converge/attempt-budget.js.map +1 -0
  60. package/dist/converge/bound-fix-recovery-source.d.ts +24 -0
  61. package/dist/converge/bound-fix-recovery-source.js +25 -0
  62. package/dist/converge/bound-fix-recovery-source.js.map +1 -0
  63. package/dist/converge/bound-fix-recovery.d.ts +15 -0
  64. package/dist/converge/bound-fix-recovery.js +65 -0
  65. package/dist/converge/bound-fix-recovery.js.map +1 -0
  66. package/dist/converge/correction-anchors.d.ts +1 -0
  67. package/dist/converge/correction-anchors.js +2 -0
  68. package/dist/converge/correction-anchors.js.map +1 -0
  69. package/dist/converge/cycle-remote.d.ts +6 -0
  70. package/dist/converge/cycle-remote.js +41 -0
  71. package/dist/converge/cycle-remote.js.map +1 -0
  72. package/dist/converge/delivery-reconciliation.d.ts +17 -0
  73. package/dist/converge/delivery-reconciliation.js +53 -0
  74. package/dist/converge/delivery-reconciliation.js.map +1 -0
  75. package/dist/converge/finding-identity.d.ts +6 -0
  76. package/dist/converge/finding-identity.js +7 -0
  77. package/dist/converge/finding-identity.js.map +1 -0
  78. package/dist/converge/fresh-review.d.ts +37 -0
  79. package/dist/converge/fresh-review.js +351 -0
  80. package/dist/converge/fresh-review.js.map +1 -0
  81. package/dist/converge/launch-guard.d.ts +55 -0
  82. package/dist/converge/launch-guard.js +363 -0
  83. package/dist/converge/launch-guard.js.map +1 -0
  84. package/dist/converge/launch-preflight.d.ts +5 -0
  85. package/dist/converge/launch-preflight.js +41 -0
  86. package/dist/converge/launch-preflight.js.map +1 -0
  87. package/dist/converge/launch-record.d.ts +136 -0
  88. package/dist/converge/launch-record.js +86 -0
  89. package/dist/converge/launch-record.js.map +1 -0
  90. package/dist/converge/legacy-launch-health.d.ts +311 -0
  91. package/dist/converge/legacy-launch-health.js +159 -0
  92. package/dist/converge/legacy-launch-health.js.map +1 -0
  93. package/dist/converge/legacy-roster.d.ts +29 -0
  94. package/dist/converge/legacy-roster.js +268 -0
  95. package/dist/converge/legacy-roster.js.map +1 -0
  96. package/dist/converge/native-lock.d.ts +18 -0
  97. package/dist/converge/native-lock.js +122 -0
  98. package/dist/converge/native-lock.js.map +1 -0
  99. package/dist/converge/ordinary-pending-export.d.ts +67 -0
  100. package/dist/converge/ordinary-pending-export.js +175 -0
  101. package/dist/converge/ordinary-pending-export.js.map +1 -0
  102. package/dist/converge/ordinary-pending-package.d.ts +28 -0
  103. package/dist/converge/ordinary-pending-package.js +36 -0
  104. package/dist/converge/ordinary-pending-package.js.map +1 -0
  105. package/dist/converge/pending-legacy-resume.d.ts +119 -0
  106. package/dist/converge/pending-legacy-resume.js +720 -0
  107. package/dist/converge/pending-legacy-resume.js.map +1 -0
  108. package/dist/converge/pending-recovery-source.d.ts +131 -0
  109. package/dist/converge/pending-recovery-source.js +71 -0
  110. package/dist/converge/pending-recovery-source.js.map +1 -0
  111. package/dist/converge/recovery-launch.d.ts +66 -0
  112. package/dist/converge/recovery-launch.js +518 -0
  113. package/dist/converge/recovery-launch.js.map +1 -0
  114. package/dist/converge/retained-report.d.ts +29 -0
  115. package/dist/converge/retained-report.js +218 -0
  116. package/dist/converge/retained-report.js.map +1 -0
  117. package/dist/converge/retry-source.d.ts +25 -0
  118. package/dist/converge/retry-source.js +13 -0
  119. package/dist/converge/retry-source.js.map +1 -0
  120. package/dist/converge/review-cycle.d.ts +49 -0
  121. package/dist/converge/review-cycle.js +26 -0
  122. package/dist/converge/review-cycle.js.map +1 -0
  123. package/dist/converge/round-gap-schema.d.ts +66 -0
  124. package/dist/converge/round-gap-schema.js +51 -0
  125. package/dist/converge/round-gap-schema.js.map +1 -0
  126. package/dist/converge/round-gap.d.ts +38 -0
  127. package/dist/converge/round-gap.js +334 -0
  128. package/dist/converge/round-gap.js.map +1 -0
  129. package/dist/converge/run-state.d.ts +224 -0
  130. package/dist/converge/run-state.js +564 -0
  131. package/dist/converge/run-state.js.map +1 -0
  132. package/dist/converge/stale-report-schema.d.ts +42 -0
  133. package/dist/converge/stale-report-schema.js +79 -0
  134. package/dist/converge/stale-report-schema.js.map +1 -0
  135. package/dist/converge/stale-report-storage.d.ts +47 -0
  136. package/dist/converge/stale-report-storage.js +172 -0
  137. package/dist/converge/stale-report-storage.js.map +1 -0
  138. package/dist/converge/stale-report.d.ts +14 -0
  139. package/dist/converge/stale-report.js +213 -0
  140. package/dist/converge/stale-report.js.map +1 -0
  141. package/dist/converge/target-ownership.d.ts +29 -0
  142. package/dist/converge/target-ownership.js +95 -0
  143. package/dist/converge/target-ownership.js.map +1 -0
  144. package/dist/converge/terminal-rejection-schema.d.ts +42 -0
  145. package/dist/converge/terminal-rejection-schema.js +39 -0
  146. package/dist/converge/terminal-rejection-schema.js.map +1 -0
  147. package/dist/converge/terminal-rejection.d.ts +13 -0
  148. package/dist/converge/terminal-rejection.js +275 -0
  149. package/dist/converge/terminal-rejection.js.map +1 -0
  150. package/dist/coordination/registry-lock.d.ts +82 -0
  151. package/dist/coordination/registry-lock.js +498 -0
  152. package/dist/coordination/registry-lock.js.map +1 -0
  153. package/dist/discuss.d.ts +53 -0
  154. package/dist/discuss.js +112 -0
  155. package/dist/discuss.js.map +1 -0
  156. package/dist/dispatch/adapter.d.ts +32 -0
  157. package/dist/dispatch/adapter.js +2 -0
  158. package/dist/dispatch/adapter.js.map +1 -0
  159. package/dist/dispatch/anthropic.d.ts +10 -0
  160. package/dist/dispatch/anthropic.js +235 -0
  161. package/dist/dispatch/anthropic.js.map +1 -0
  162. package/dist/dispatch/async-lane.d.ts +127 -0
  163. package/dist/dispatch/async-lane.js +414 -0
  164. package/dist/dispatch/async-lane.js.map +1 -0
  165. package/dist/dispatch/capture-council.d.ts +63 -0
  166. package/dist/dispatch/capture-council.js +137 -0
  167. package/dist/dispatch/capture-council.js.map +1 -0
  168. package/dist/dispatch/captured-async.d.ts +67 -0
  169. package/dist/dispatch/captured-async.js +40 -0
  170. package/dist/dispatch/captured-async.js.map +1 -0
  171. package/dist/dispatch/captured-inputs.d.ts +50 -0
  172. package/dist/dispatch/captured-inputs.js +190 -0
  173. package/dist/dispatch/captured-inputs.js.map +1 -0
  174. package/dist/dispatch/checkpoint-async-context.d.ts +27 -0
  175. package/dist/dispatch/checkpoint-async-context.js +51 -0
  176. package/dist/dispatch/checkpoint-async-context.js.map +1 -0
  177. package/dist/dispatch/checkpoint-async-execution.d.ts +22 -0
  178. package/dist/dispatch/checkpoint-async-execution.js +130 -0
  179. package/dist/dispatch/checkpoint-async-execution.js.map +1 -0
  180. package/dist/dispatch/checkpoint-async-store.d.ts +134 -0
  181. package/dist/dispatch/checkpoint-async-store.js +534 -0
  182. package/dist/dispatch/checkpoint-async-store.js.map +1 -0
  183. package/dist/dispatch/checkpoint-async-unknown.d.ts +19 -0
  184. package/dist/dispatch/checkpoint-async-unknown.js +46 -0
  185. package/dist/dispatch/checkpoint-async-unknown.js.map +1 -0
  186. package/dist/dispatch/checkpoint-async.d.ts +252 -0
  187. package/dist/dispatch/checkpoint-async.js +270 -0
  188. package/dist/dispatch/checkpoint-async.js.map +1 -0
  189. package/dist/dispatch/checkpoint-gating-execution.d.ts +26 -0
  190. package/dist/dispatch/checkpoint-gating-execution.js +84 -0
  191. package/dist/dispatch/checkpoint-gating-execution.js.map +1 -0
  192. package/dist/dispatch/checkpoint-phase-limits.d.ts +5 -0
  193. package/dist/dispatch/checkpoint-phase-limits.js +11 -0
  194. package/dist/dispatch/checkpoint-phase-limits.js.map +1 -0
  195. package/dist/dispatch/checkpoint-verification-context.d.ts +15 -0
  196. package/dist/dispatch/checkpoint-verification-context.js +48 -0
  197. package/dist/dispatch/checkpoint-verification-context.js.map +1 -0
  198. package/dist/dispatch/checkpoint-verification.d.ts +213 -0
  199. package/dist/dispatch/checkpoint-verification.js +381 -0
  200. package/dist/dispatch/checkpoint-verification.js.map +1 -0
  201. package/dist/dispatch/checkpoint.d.ts +329 -0
  202. package/dist/dispatch/checkpoint.js +1418 -0
  203. package/dist/dispatch/checkpoint.js.map +1 -0
  204. package/dist/dispatch/google.d.ts +10 -0
  205. package/dist/dispatch/google.js +158 -0
  206. package/dist/dispatch/google.js.map +1 -0
  207. package/dist/dispatch/late-audit.d.ts +23 -0
  208. package/dist/dispatch/late-audit.js +133 -0
  209. package/dist/dispatch/late-audit.js.map +1 -0
  210. package/dist/dispatch/merge.d.ts +29 -0
  211. package/dist/dispatch/merge.js +112 -0
  212. package/dist/dispatch/merge.js.map +1 -0
  213. package/dist/dispatch/openai-compat.d.ts +30 -0
  214. package/dist/dispatch/openai-compat.js +166 -0
  215. package/dist/dispatch/openai-compat.js.map +1 -0
  216. package/dist/dispatch/openai.d.ts +10 -0
  217. package/dist/dispatch/openai.js +159 -0
  218. package/dist/dispatch/openai.js.map +1 -0
  219. package/dist/dispatch/original-execution.d.ts +30 -0
  220. package/dist/dispatch/original-execution.js +91 -0
  221. package/dist/dispatch/original-execution.js.map +1 -0
  222. package/dist/dispatch/original-launch.d.ts +37 -0
  223. package/dist/dispatch/original-launch.js +117 -0
  224. package/dist/dispatch/original-launch.js.map +1 -0
  225. package/dist/dispatch/quorum.d.ts +15 -0
  226. package/dist/dispatch/quorum.js +33 -0
  227. package/dist/dispatch/quorum.js.map +1 -0
  228. package/dist/dispatch/recovery-operation.d.ts +44 -0
  229. package/dist/dispatch/recovery-operation.js +146 -0
  230. package/dist/dispatch/recovery-operation.js.map +1 -0
  231. package/dist/dispatch/recovery-policy.d.ts +54 -0
  232. package/dist/dispatch/recovery-policy.js +157 -0
  233. package/dist/dispatch/recovery-policy.js.map +1 -0
  234. package/dist/dispatch/recovery.d.ts +78 -0
  235. package/dist/dispatch/recovery.js +299 -0
  236. package/dist/dispatch/recovery.js.map +1 -0
  237. package/dist/dispatch/retained-async.d.ts +13 -0
  238. package/dist/dispatch/retained-async.js +101 -0
  239. package/dist/dispatch/retained-async.js.map +1 -0
  240. package/dist/dispatch/reviewer-identity.d.ts +10 -0
  241. package/dist/dispatch/reviewer-identity.js +19 -0
  242. package/dist/dispatch/reviewer-identity.js.map +1 -0
  243. package/dist/dispatch/runner.d.ts +76 -0
  244. package/dist/dispatch/runner.js +415 -0
  245. package/dist/dispatch/runner.js.map +1 -0
  246. package/dist/dispatch/utils.d.ts +159 -0
  247. package/dist/dispatch/utils.js +381 -0
  248. package/dist/dispatch/utils.js.map +1 -0
  249. package/dist/dispatch/verification-execution.d.ts +48 -0
  250. package/dist/dispatch/verification-execution.js +252 -0
  251. package/dist/dispatch/verification-execution.js.map +1 -0
  252. package/dist/dispatch/verification-late-audit.d.ts +17 -0
  253. package/dist/dispatch/verification-late-audit.js +117 -0
  254. package/dist/dispatch/verification-late-audit.js.map +1 -0
  255. package/dist/evidence/claim-recovery/authenticated-carrier-projection.d.ts +30 -0
  256. package/dist/evidence/claim-recovery/authenticated-carrier-projection.js +100 -0
  257. package/dist/evidence/claim-recovery/authenticated-carrier-projection.js.map +1 -0
  258. package/dist/evidence/claim-recovery/authenticated-receipts.d.ts +46 -0
  259. package/dist/evidence/claim-recovery/authenticated-receipts.js +120 -0
  260. package/dist/evidence/claim-recovery/authenticated-receipts.js.map +1 -0
  261. package/dist/evidence/claim-recovery/carrier-inventory.d.ts +49 -0
  262. package/dist/evidence/claim-recovery/carrier-inventory.js +340 -0
  263. package/dist/evidence/claim-recovery/carrier-inventory.js.map +1 -0
  264. package/dist/evidence/claim-recovery/claim-index.d.ts +12 -0
  265. package/dist/evidence/claim-recovery/claim-index.js +56 -0
  266. package/dist/evidence/claim-recovery/claim-index.js.map +1 -0
  267. package/dist/evidence/claim-recovery/confirmation.d.ts +53 -0
  268. package/dist/evidence/claim-recovery/confirmation.js +151 -0
  269. package/dist/evidence/claim-recovery/confirmation.js.map +1 -0
  270. package/dist/evidence/claim-recovery/context.d.ts +15 -0
  271. package/dist/evidence/claim-recovery/context.js +40 -0
  272. package/dist/evidence/claim-recovery/context.js.map +1 -0
  273. package/dist/evidence/claim-recovery/proof-storage.d.ts +6 -0
  274. package/dist/evidence/claim-recovery/proof-storage.js +97 -0
  275. package/dist/evidence/claim-recovery/proof-storage.js.map +1 -0
  276. package/dist/evidence/claim-recovery/validation/anchors.d.ts +20 -0
  277. package/dist/evidence/claim-recovery/validation/anchors.js +22 -0
  278. package/dist/evidence/claim-recovery/validation/anchors.js.map +1 -0
  279. package/dist/evidence/claim-recovery/validation/carrier-projection.d.ts +9 -0
  280. package/dist/evidence/claim-recovery/validation/carrier-projection.js +364 -0
  281. package/dist/evidence/claim-recovery/validation/carrier-projection.js.map +1 -0
  282. package/dist/evidence/claim-recovery/validation/carrier-types.d.ts +83 -0
  283. package/dist/evidence/claim-recovery/validation/carrier-types.js +2 -0
  284. package/dist/evidence/claim-recovery/validation/carrier-types.js.map +1 -0
  285. package/dist/evidence/claim-recovery/validation/claim-split.d.ts +57 -0
  286. package/dist/evidence/claim-recovery/validation/claim-split.js +209 -0
  287. package/dist/evidence/claim-recovery/validation/claim-split.js.map +1 -0
  288. package/dist/evidence/claim-recovery/validation/claims.d.ts +20 -0
  289. package/dist/evidence/claim-recovery/validation/claims.js +108 -0
  290. package/dist/evidence/claim-recovery/validation/claims.js.map +1 -0
  291. package/dist/evidence/claim-recovery/validation/current-projection.d.ts +36 -0
  292. package/dist/evidence/claim-recovery/validation/current-projection.js +424 -0
  293. package/dist/evidence/claim-recovery/validation/current-projection.js.map +1 -0
  294. package/dist/evidence/claim-recovery/validation/lexical.d.ts +18 -0
  295. package/dist/evidence/claim-recovery/validation/lexical.js +91 -0
  296. package/dist/evidence/claim-recovery/validation/lexical.js.map +1 -0
  297. package/dist/evidence/claim-recovery/validation/materials.d.ts +19 -0
  298. package/dist/evidence/claim-recovery/validation/materials.js +160 -0
  299. package/dist/evidence/claim-recovery/validation/materials.js.map +1 -0
  300. package/dist/evidence/claim-recovery/validation/native-material.d.ts +27 -0
  301. package/dist/evidence/claim-recovery/validation/native-material.js +35 -0
  302. package/dist/evidence/claim-recovery/validation/native-material.js.map +1 -0
  303. package/dist/evidence/claim-recovery/validation/native-occurrences.d.ts +79 -0
  304. package/dist/evidence/claim-recovery/validation/native-occurrences.js +187 -0
  305. package/dist/evidence/claim-recovery/validation/native-occurrences.js.map +1 -0
  306. package/dist/evidence/claim-recovery/validation/native-state.d.ts +45 -0
  307. package/dist/evidence/claim-recovery/validation/native-state.js +385 -0
  308. package/dist/evidence/claim-recovery/validation/native-state.js.map +1 -0
  309. package/dist/evidence/claim-recovery/validation/obligations.d.ts +15 -0
  310. package/dist/evidence/claim-recovery/validation/obligations.js +65 -0
  311. package/dist/evidence/claim-recovery/validation/obligations.js.map +1 -0
  312. package/dist/evidence/claim-recovery/validation/occurrence-source.d.ts +45 -0
  313. package/dist/evidence/claim-recovery/validation/occurrence-source.js +208 -0
  314. package/dist/evidence/claim-recovery/validation/occurrence-source.js.map +1 -0
  315. package/dist/evidence/claim-recovery/validation/occurrence-types.d.ts +101 -0
  316. package/dist/evidence/claim-recovery/validation/occurrence-types.js +2 -0
  317. package/dist/evidence/claim-recovery/validation/occurrence-types.js.map +1 -0
  318. package/dist/evidence/claim-recovery/validation/occurrence.d.ts +30 -0
  319. package/dist/evidence/claim-recovery/validation/occurrence.js +227 -0
  320. package/dist/evidence/claim-recovery/validation/occurrence.js.map +1 -0
  321. package/dist/evidence/claim-recovery/validation/primitives.d.ts +11 -0
  322. package/dist/evidence/claim-recovery/validation/primitives.js +41 -0
  323. package/dist/evidence/claim-recovery/validation/primitives.js.map +1 -0
  324. package/dist/evidence/claim-recovery/validation/receipts.d.ts +37 -0
  325. package/dist/evidence/claim-recovery/validation/receipts.js +83 -0
  326. package/dist/evidence/claim-recovery/validation/receipts.js.map +1 -0
  327. package/dist/evidence/claim-recovery/validation/recovery-json.d.ts +8 -0
  328. package/dist/evidence/claim-recovery/validation/recovery-json.js +42 -0
  329. package/dist/evidence/claim-recovery/validation/recovery-json.js.map +1 -0
  330. package/dist/evidence/claim-recovery/validation/semantic-cache.d.ts +3 -0
  331. package/dist/evidence/claim-recovery/validation/semantic-cache.js +22 -0
  332. package/dist/evidence/claim-recovery/validation/semantic-cache.js.map +1 -0
  333. package/dist/evidence/claim-recovery/validation/semantic-validation.d.ts +9 -0
  334. package/dist/evidence/claim-recovery/validation/semantic-validation.js +347 -0
  335. package/dist/evidence/claim-recovery/validation/semantic-validation.js.map +1 -0
  336. package/dist/evidence/claim-recovery/validation/sighting.d.ts +2 -0
  337. package/dist/evidence/claim-recovery/validation/sighting.js +15 -0
  338. package/dist/evidence/claim-recovery/validation/sighting.js.map +1 -0
  339. package/dist/evidence/claim-recovery/validation/sources.d.ts +15 -0
  340. package/dist/evidence/claim-recovery/validation/sources.js +2 -0
  341. package/dist/evidence/claim-recovery/validation/sources.js.map +1 -0
  342. package/dist/evidence/claim-recovery/validation/types.d.ts +126 -0
  343. package/dist/evidence/claim-recovery/validation/types.js +2 -0
  344. package/dist/evidence/claim-recovery/validation/types.js.map +1 -0
  345. package/dist/evidence/claim-split.d.ts +1 -0
  346. package/dist/evidence/claim-split.js +2 -0
  347. package/dist/evidence/claim-split.js.map +1 -0
  348. package/dist/evidence/event-receipts.d.ts +16 -0
  349. package/dist/evidence/event-receipts.js +41 -0
  350. package/dist/evidence/event-receipts.js.map +1 -0
  351. package/dist/evidence/finding-recovery.d.ts +23 -0
  352. package/dist/evidence/finding-recovery.js +74 -0
  353. package/dist/evidence/finding-recovery.js.map +1 -0
  354. package/dist/evidence/finding-retriage.d.ts +14 -0
  355. package/dist/evidence/finding-retriage.js +57 -0
  356. package/dist/evidence/finding-retriage.js.map +1 -0
  357. package/dist/evidence/format.d.ts +18 -0
  358. package/dist/evidence/format.js +152 -0
  359. package/dist/evidence/format.js.map +1 -0
  360. package/dist/evidence/original-run/decode.d.ts +30 -0
  361. package/dist/evidence/original-run/decode.js +208 -0
  362. package/dist/evidence/original-run/decode.js.map +1 -0
  363. package/dist/evidence/original-run/journal.d.ts +31 -0
  364. package/dist/evidence/original-run/journal.js +157 -0
  365. package/dist/evidence/original-run/journal.js.map +1 -0
  366. package/dist/evidence/original-run/lock-path.d.ts +17 -0
  367. package/dist/evidence/original-run/lock-path.js +116 -0
  368. package/dist/evidence/original-run/lock-path.js.map +1 -0
  369. package/dist/evidence/original-run/lock-scope.d.ts +21 -0
  370. package/dist/evidence/original-run/lock-scope.js +61 -0
  371. package/dist/evidence/original-run/lock-scope.js.map +1 -0
  372. package/dist/evidence/original-run/lock.d.ts +33 -0
  373. package/dist/evidence/original-run/lock.js +259 -0
  374. package/dist/evidence/original-run/lock.js.map +1 -0
  375. package/dist/evidence/original-run/remote.d.ts +23 -0
  376. package/dist/evidence/original-run/remote.js +110 -0
  377. package/dist/evidence/original-run/remote.js.map +1 -0
  378. package/dist/evidence/original-run/source.d.ts +61 -0
  379. package/dist/evidence/original-run/source.js +267 -0
  380. package/dist/evidence/original-run/source.js.map +1 -0
  381. package/dist/evidence/reads.d.ts +11 -0
  382. package/dist/evidence/reads.js +16 -0
  383. package/dist/evidence/reads.js.map +1 -0
  384. package/dist/evidence/recover-finding.d.ts +17 -0
  385. package/dist/evidence/recover-finding.js +65 -0
  386. package/dist/evidence/recover-finding.js.map +1 -0
  387. package/dist/evidence/recover-run.d.ts +26 -0
  388. package/dist/evidence/recover-run.js +180 -0
  389. package/dist/evidence/recover-run.js.map +1 -0
  390. package/dist/evidence/retained-time-budget.d.ts +9 -0
  391. package/dist/evidence/retained-time-budget.js +14 -0
  392. package/dist/evidence/retained-time-budget.js.map +1 -0
  393. package/dist/evidence/retriage-finding.d.ts +12 -0
  394. package/dist/evidence/retriage-finding.js +67 -0
  395. package/dist/evidence/retriage-finding.js.map +1 -0
  396. package/dist/evidence/reviewer-lineage.d.ts +35 -0
  397. package/dist/evidence/reviewer-lineage.js +293 -0
  398. package/dist/evidence/reviewer-lineage.js.map +1 -0
  399. package/dist/evidence/reviewer-original.d.ts +67 -0
  400. package/dist/evidence/reviewer-original.js +148 -0
  401. package/dist/evidence/reviewer-original.js.map +1 -0
  402. package/dist/evidence/reviewer-recovery.d.ts +74 -0
  403. package/dist/evidence/reviewer-recovery.js +179 -0
  404. package/dist/evidence/reviewer-recovery.js.map +1 -0
  405. package/dist/evidence/reviewer-status.d.ts +145 -0
  406. package/dist/evidence/reviewer-status.js +302 -0
  407. package/dist/evidence/reviewer-status.js.map +1 -0
  408. package/dist/evidence/show.d.ts +10 -0
  409. package/dist/evidence/show.js +41 -0
  410. package/dist/evidence/show.js.map +1 -0
  411. package/dist/evidence/status.d.ts +38 -0
  412. package/dist/evidence/status.js +73 -0
  413. package/dist/evidence/status.js.map +1 -0
  414. package/dist/evidence/target.d.ts +17 -0
  415. package/dist/evidence/target.js +74 -0
  416. package/dist/evidence/target.js.map +1 -0
  417. package/dist/evidence/types.d.ts +176 -0
  418. package/dist/evidence/types.js +109 -0
  419. package/dist/evidence/types.js.map +1 -0
  420. package/dist/index.d.ts +2 -0
  421. package/dist/index.js +2688 -0
  422. package/dist/index.js.map +1 -0
  423. package/dist/models/seed.d.ts +44 -0
  424. package/dist/models/seed.js +175 -0
  425. package/dist/models/seed.js.map +1 -0
  426. package/dist/models/server-stats.d.ts +80 -0
  427. package/dist/models/server-stats.js +109 -0
  428. package/dist/models/server-stats.js.map +1 -0
  429. package/dist/models/stats-store.d.ts +84 -0
  430. package/dist/models/stats-store.js +572 -0
  431. package/dist/models/stats-store.js.map +1 -0
  432. package/dist/output/artifacts.d.ts +16 -0
  433. package/dist/output/artifacts.js +29 -0
  434. package/dist/output/artifacts.js.map +1 -0
  435. package/dist/output/assembly-refusal.d.ts +8 -0
  436. package/dist/output/assembly-refusal.js +14 -0
  437. package/dist/output/assembly-refusal.js.map +1 -0
  438. package/dist/output/github.d.ts +12 -0
  439. package/dist/output/github.js +214 -0
  440. package/dist/output/github.js.map +1 -0
  441. package/dist/output/json.d.ts +3 -0
  442. package/dist/output/json.js +8 -0
  443. package/dist/output/json.js.map +1 -0
  444. package/dist/output/markdown.d.ts +3 -0
  445. package/dist/output/markdown.js +281 -0
  446. package/dist/output/markdown.js.map +1 -0
  447. package/dist/output/progress.d.ts +55 -0
  448. package/dist/output/progress.js +180 -0
  449. package/dist/output/progress.js.map +1 -0
  450. package/dist/output/sanitize.d.ts +18 -0
  451. package/dist/output/sanitize.js +46 -0
  452. package/dist/output/sanitize.js.map +1 -0
  453. package/dist/output/terminal.d.ts +2 -0
  454. package/dist/output/terminal.js +172 -0
  455. package/dist/output/terminal.js.map +1 -0
  456. package/dist/prepare/chunker.d.ts +18 -0
  457. package/dist/prepare/chunker.js +325 -0
  458. package/dist/prepare/chunker.js.map +1 -0
  459. package/dist/prepare/language.d.ts +1 -0
  460. package/dist/prepare/language.js +99 -0
  461. package/dist/prepare/language.js.map +1 -0
  462. package/dist/prepare/prompt-builder.d.ts +44 -0
  463. package/dist/prepare/prompt-builder.js +102 -0
  464. package/dist/prepare/prompt-builder.js.map +1 -0
  465. package/dist/prepare/unified-diff.d.ts +40 -0
  466. package/dist/prepare/unified-diff.js +135 -0
  467. package/dist/prepare/unified-diff.js.map +1 -0
  468. package/dist/prompts/base.d.ts +3 -0
  469. package/dist/prompts/base.js +53 -0
  470. package/dist/prompts/base.js.map +1 -0
  471. package/dist/prompts/hardening.d.ts +19 -0
  472. package/dist/prompts/hardening.js +67 -0
  473. package/dist/prompts/hardening.js.map +1 -0
  474. package/dist/prompts/languages/elixir.d.ts +1 -0
  475. package/dist/prompts/languages/elixir.js +13 -0
  476. package/dist/prompts/languages/elixir.js.map +1 -0
  477. package/dist/prompts/languages/generic.d.ts +1 -0
  478. package/dist/prompts/languages/generic.js +11 -0
  479. package/dist/prompts/languages/generic.js.map +1 -0
  480. package/dist/prompts/languages/python.d.ts +1 -0
  481. package/dist/prompts/languages/python.js +14 -0
  482. package/dist/prompts/languages/python.js.map +1 -0
  483. package/dist/prompts/languages/typescript.d.ts +1 -0
  484. package/dist/prompts/languages/typescript.js +13 -0
  485. package/dist/prompts/languages/typescript.js.map +1 -0
  486. package/dist/prompts/plan.d.ts +15 -0
  487. package/dist/prompts/plan.js +55 -0
  488. package/dist/prompts/plan.js.map +1 -0
  489. package/dist/report/aggregation-inputs.d.ts +52 -0
  490. package/dist/report/aggregation-inputs.js +114 -0
  491. package/dist/report/aggregation-inputs.js.map +1 -0
  492. package/dist/report/assembly.d.ts +43 -0
  493. package/dist/report/assembly.js +133 -0
  494. package/dist/report/assembly.js.map +1 -0
  495. package/dist/report/blocking-health.d.ts +64 -0
  496. package/dist/report/blocking-health.js +156 -0
  497. package/dist/report/blocking-health.js.map +1 -0
  498. package/dist/report/checkpoint-assembly.d.ts +10 -0
  499. package/dist/report/checkpoint-assembly.js +17 -0
  500. package/dist/report/checkpoint-assembly.js.map +1 -0
  501. package/dist/report/checkpoint-consensus.d.ts +59 -0
  502. package/dist/report/checkpoint-consensus.js +197 -0
  503. package/dist/report/checkpoint-consensus.js.map +1 -0
  504. package/dist/report/checkpoint-gating.d.ts +36 -0
  505. package/dist/report/checkpoint-gating.js +110 -0
  506. package/dist/report/checkpoint-gating.js.map +1 -0
  507. package/dist/report/checkpoint-projection.d.ts +76 -0
  508. package/dist/report/checkpoint-projection.js +122 -0
  509. package/dist/report/checkpoint-projection.js.map +1 -0
  510. package/dist/report/consensus-assembly.d.ts +37 -0
  511. package/dist/report/consensus-assembly.js +33 -0
  512. package/dist/report/consensus-assembly.js.map +1 -0
  513. package/dist/report/recovery-source.d.ts +7 -0
  514. package/dist/report/recovery-source.js +7 -0
  515. package/dist/report/recovery-source.js.map +1 -0
  516. package/dist/report/reviewer-artifact.d.ts +118 -0
  517. package/dist/report/reviewer-artifact.js +381 -0
  518. package/dist/report/reviewer-artifact.js.map +1 -0
  519. package/dist/report/reviewer-evidence-schema.d.ts +42 -0
  520. package/dist/report/reviewer-evidence-schema.js +28 -0
  521. package/dist/report/reviewer-evidence-schema.js.map +1 -0
  522. package/dist/report/reviewer-evidence.d.ts +54 -0
  523. package/dist/report/reviewer-evidence.js +229 -0
  524. package/dist/report/reviewer-evidence.js.map +1 -0
  525. package/dist/report/reviewer-health.d.ts +62 -0
  526. package/dist/report/reviewer-health.js +105 -0
  527. package/dist/report/reviewer-health.js.map +1 -0
  528. package/dist/report/run-header.d.ts +219 -0
  529. package/dist/report/run-header.js +307 -0
  530. package/dist/report/run-header.js.map +1 -0
  531. package/dist/report/supplemental-async.d.ts +19 -0
  532. package/dist/report/supplemental-async.js +85 -0
  533. package/dist/report/supplemental-async.js.map +1 -0
  534. package/dist/report/uuid.d.ts +16 -0
  535. package/dist/report/uuid.js +41 -0
  536. package/dist/report/uuid.js.map +1 -0
  537. package/dist/resolver/git.d.ts +16 -0
  538. package/dist/resolver/git.js +77 -0
  539. package/dist/resolver/git.js.map +1 -0
  540. package/dist/resolver/github-client.d.ts +82 -0
  541. package/dist/resolver/github-client.js +27 -0
  542. package/dist/resolver/github-client.js.map +1 -0
  543. package/dist/resolver/github.d.ts +29 -0
  544. package/dist/resolver/github.js +166 -0
  545. package/dist/resolver/github.js.map +1 -0
  546. package/dist/resolver/local.d.ts +3 -0
  547. package/dist/resolver/local.js +75 -0
  548. package/dist/resolver/local.js.map +1 -0
  549. package/dist/resolver/plan.d.ts +8 -0
  550. package/dist/resolver/plan.js +44 -0
  551. package/dist/resolver/plan.js.map +1 -0
  552. package/dist/resolver/target.d.ts +35 -0
  553. package/dist/resolver/target.js +112 -0
  554. package/dist/resolver/target.js.map +1 -0
  555. package/dist/resolver/types.d.ts +43 -0
  556. package/dist/resolver/types.js +2 -0
  557. package/dist/resolver/types.js.map +1 -0
  558. package/dist/roles/builtin.d.ts +4 -0
  559. package/dist/roles/builtin.js +282 -0
  560. package/dist/roles/builtin.js.map +1 -0
  561. package/dist/roles/dispatcher.d.ts +26 -0
  562. package/dist/roles/dispatcher.js +96 -0
  563. package/dist/roles/dispatcher.js.map +1 -0
  564. package/dist/roles/legacy-4.1.10.d.ts +2 -0
  565. package/dist/roles/legacy-4.1.10.js +320 -0
  566. package/dist/roles/legacy-4.1.10.js.map +1 -0
  567. package/dist/roles/legacy-4.4.9.d.ts +4 -0
  568. package/dist/roles/legacy-4.4.9.js +282 -0
  569. package/dist/roles/legacy-4.4.9.js.map +1 -0
  570. package/dist/roles/loader.d.ts +10 -0
  571. package/dist/roles/loader.js +104 -0
  572. package/dist/roles/loader.js.map +1 -0
  573. package/dist/roles/types.d.ts +13 -0
  574. package/dist/roles/types.js +2 -0
  575. package/dist/roles/types.js.map +1 -0
  576. package/dist/telemetry/abort-signal.d.ts +13 -0
  577. package/dist/telemetry/abort-signal.js +40 -0
  578. package/dist/telemetry/abort-signal.js.map +1 -0
  579. package/dist/telemetry/attest.d.ts +98 -0
  580. package/dist/telemetry/attest.js +331 -0
  581. package/dist/telemetry/attest.js.map +1 -0
  582. package/dist/telemetry/attested-retry.d.ts +79 -0
  583. package/dist/telemetry/attested-retry.js +195 -0
  584. package/dist/telemetry/attested-retry.js.map +1 -0
  585. package/dist/telemetry/attested-reviewer-delivery.d.ts +58 -0
  586. package/dist/telemetry/attested-reviewer-delivery.js +238 -0
  587. package/dist/telemetry/attested-reviewer-delivery.js.map +1 -0
  588. package/dist/telemetry/backfill.d.ts +95 -0
  589. package/dist/telemetry/backfill.js +460 -0
  590. package/dist/telemetry/backfill.js.map +1 -0
  591. package/dist/telemetry/credentials.d.ts +44 -0
  592. package/dist/telemetry/credentials.js +87 -0
  593. package/dist/telemetry/credentials.js.map +1 -0
  594. package/dist/telemetry/deliver.d.ts +152 -0
  595. package/dist/telemetry/deliver.js +662 -0
  596. package/dist/telemetry/deliver.js.map +1 -0
  597. package/dist/telemetry/envelope-validation.d.ts +13 -0
  598. package/dist/telemetry/envelope-validation.js +196 -0
  599. package/dist/telemetry/envelope-validation.js.map +1 -0
  600. package/dist/telemetry/envelope.d.ts +159 -0
  601. package/dist/telemetry/envelope.js +288 -0
  602. package/dist/telemetry/envelope.js.map +1 -0
  603. package/dist/telemetry/events.d.ts +68 -0
  604. package/dist/telemetry/events.js +79 -0
  605. package/dist/telemetry/events.js.map +1 -0
  606. package/dist/telemetry/notice.d.ts +15 -0
  607. package/dist/telemetry/notice.js +67 -0
  608. package/dist/telemetry/notice.js.map +1 -0
  609. package/dist/telemetry/outbox.d.ts +166 -0
  610. package/dist/telemetry/outbox.js +741 -0
  611. package/dist/telemetry/outbox.js.map +1 -0
  612. package/dist/telemetry/quarantine.d.ts +105 -0
  613. package/dist/telemetry/quarantine.js +224 -0
  614. package/dist/telemetry/quarantine.js.map +1 -0
  615. package/dist/telemetry/read-sink.d.ts +31 -0
  616. package/dist/telemetry/read-sink.js +25 -0
  617. package/dist/telemetry/read-sink.js.map +1 -0
  618. package/dist/telemetry/recovery/command.d.ts +16 -0
  619. package/dist/telemetry/recovery/command.js +70 -0
  620. package/dist/telemetry/recovery/command.js.map +1 -0
  621. package/dist/telemetry/recovery/discovery.d.ts +8 -0
  622. package/dist/telemetry/recovery/discovery.js +277 -0
  623. package/dist/telemetry/recovery/discovery.js.map +1 -0
  624. package/dist/telemetry/recovery/files.d.ts +20 -0
  625. package/dist/telemetry/recovery/files.js +123 -0
  626. package/dist/telemetry/recovery/files.js.map +1 -0
  627. package/dist/telemetry/recovery/plan.d.ts +10 -0
  628. package/dist/telemetry/recovery/plan.js +325 -0
  629. package/dist/telemetry/recovery/plan.js.map +1 -0
  630. package/dist/telemetry/recovery/source.d.ts +328 -0
  631. package/dist/telemetry/recovery/source.js +253 -0
  632. package/dist/telemetry/recovery/source.js.map +1 -0
  633. package/dist/telemetry/recovery/types.d.ts +100 -0
  634. package/dist/telemetry/recovery/types.js +2 -0
  635. package/dist/telemetry/recovery/types.js.map +1 -0
  636. package/dist/telemetry/recovery-request-budget.d.ts +39 -0
  637. package/dist/telemetry/recovery-request-budget.js +102 -0
  638. package/dist/telemetry/recovery-request-budget.js.map +1 -0
  639. package/dist/telemetry/report-consistency.d.ts +11 -0
  640. package/dist/telemetry/report-consistency.js +111 -0
  641. package/dist/telemetry/report-consistency.js.map +1 -0
  642. package/dist/telemetry/reviewer-delivery.d.ts +39 -0
  643. package/dist/telemetry/reviewer-delivery.js +279 -0
  644. package/dist/telemetry/reviewer-delivery.js.map +1 -0
  645. package/dist/telemetry/reviewer-preflight.d.ts +13 -0
  646. package/dist/telemetry/reviewer-preflight.js +56 -0
  647. package/dist/telemetry/reviewer-preflight.js.map +1 -0
  648. package/dist/telemetry/scrub.d.ts +53 -0
  649. package/dist/telemetry/scrub.js +205 -0
  650. package/dist/telemetry/scrub.js.map +1 -0
  651. package/dist/telemetry/sink.d.ts +212 -0
  652. package/dist/telemetry/sink.js +813 -0
  653. package/dist/telemetry/sink.js.map +1 -0
  654. package/dist/telemetry/terminal-reviewer-delivery.d.ts +18 -0
  655. package/dist/telemetry/terminal-reviewer-delivery.js +27 -0
  656. package/dist/telemetry/terminal-reviewer-delivery.js.map +1 -0
  657. package/dist/telemetry/transient-input.d.ts +2 -0
  658. package/dist/telemetry/transient-input.js +37 -0
  659. package/dist/telemetry/transient-input.js.map +1 -0
  660. package/dist/telemetry/verification.d.ts +9 -0
  661. package/dist/telemetry/verification.js +12 -0
  662. package/dist/telemetry/verification.js.map +1 -0
  663. package/package.json +73 -0
@@ -0,0 +1,1052 @@
1
+ import { DEFAULT_THRESHOLDS } from '../config/defaults.js';
2
+ /** Title similarity carries more signal than description similarity. */
3
+ const TITLE_WEIGHT = 0.6;
4
+ const DESC_WEIGHT = 0.4;
5
+ /**
6
+ * Common English function words that carry no similarity signal.
7
+ * Negations (no, not, never) are deliberately kept — they distinguish
8
+ * opposite findings.
9
+ */
10
+ const STOPWORDS = new Set([
11
+ 'the', 'a', 'an', 'and', 'or', 'of', 'in', 'on', 'at', 'to', 'for',
12
+ 'is', 'are', 'was', 'were', 'be', 'been', 'being', 'it', 'its',
13
+ 'this', 'that', 'these', 'those', 'with', 'as', 'by', 'from', 'into',
14
+ 'via', 'when', 'which', 'their', 'there', 'than', 'then',
15
+ ]);
16
+ function tokenize(s) {
17
+ return new Set(s
18
+ .toLowerCase()
19
+ // Unicode-aware: keep letters/numbers in all scripts — an ASCII-only
20
+ // filter would tokenize non-English findings to empty sets, which
21
+ // read as identical (similarity 1.0) and merge unrelated findings
22
+ .replace(/[^\p{L}\p{N}\s]/gu, ' ')
23
+ .split(/\s+/)
24
+ .filter((t) => t.length > 1 && !STOPWORDS.has(t)));
25
+ }
26
+ function jaccardOfSets(setA, setB) {
27
+ if (setA.size === 0 && setB.size === 0)
28
+ return 1.0;
29
+ if (setA.size === 0 || setB.size === 0)
30
+ return 0.0;
31
+ let intersection = 0;
32
+ for (const token of setA) {
33
+ if (setB.has(token))
34
+ intersection++;
35
+ }
36
+ const union = setA.size + setB.size - intersection;
37
+ return intersection / union;
38
+ }
39
+ /**
40
+ * Compute Jaccard similarity between two strings (word-level tokenization).
41
+ * Single characters and stopwords are ignored; short signal tokens like
42
+ * "xss", "id", or "no" are kept.
43
+ */
44
+ export function jaccardSimilarity(a, b) {
45
+ return jaccardOfSets(tokenize(a), tokenize(b));
46
+ }
47
+ /**
48
+ * Weighted title+description similarity. Both fields contribute, so a short
49
+ * generic title alone can no longer merge two unrelated findings, and a
50
+ * verbose description alone can't either.
51
+ *
52
+ * A field where either side has no usable tokens carries no signal and is
53
+ * excluded, with weights renormalized over the remaining fields — empty
54
+ * descriptions neither grant free similarity nor penalize a strong title
55
+ * match. If no field has usable tokens on both sides, similarity is 0.
56
+ */
57
+ export function combinedSimilarity(a, b) {
58
+ const fields = [
59
+ { weight: TITLE_WEIGHT, a: tokenize(a.title), b: tokenize(b.title) },
60
+ { weight: DESC_WEIGHT, a: tokenize(a.description), b: tokenize(b.description) },
61
+ ].filter((f) => f.a.size > 0 && f.b.size > 0);
62
+ const totalWeight = fields.reduce((sum, f) => sum + f.weight, 0);
63
+ if (totalWeight === 0)
64
+ return 0;
65
+ return fields.reduce((sum, f) => sum + jaccardOfSets(f.a, f.b) * f.weight, 0) / totalWeight;
66
+ }
67
+ function term(t) {
68
+ return new RegExp(`\\b${t.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`, 'i');
69
+ }
70
+ const OPPOSING_PAIRS = [
71
+ { a: term('missing'), b: term('present'), specific: true },
72
+ { a: term('no'), b: term('has'), specific: false },
73
+ { a: term('lacks'), b: term('has'), specific: false },
74
+ { a: term('not'), b: term('is'), specific: false },
75
+ { a: term('should add'), b: term('should remove'), specific: true },
76
+ { a: term('too complex'), b: term('too simple'), specific: true },
77
+ { a: term('over-engineered'), b: term('under-engineered'), specific: true },
78
+ { a: term('unnecessary'), b: term('necessary'), specific: true },
79
+ { a: term('remove'), b: term('keep'), specific: false },
80
+ { a: term('redundant'), b: term('required'), specific: true },
81
+ { a: term('unsafe'), b: term('safe'), specific: true },
82
+ { a: term('deprecated'), b: term('recommended'), specific: true },
83
+ { a: term('too permissive'), b: term('too restrictive'), specific: true },
84
+ ];
85
+ function textOpposes(textA, textB, inDescription, specificOnly) {
86
+ for (const pair of OPPOSING_PAIRS) {
87
+ if (inDescription && !pair.specific)
88
+ continue;
89
+ if (specificOnly && !pair.specific)
90
+ continue;
91
+ const aHasA = pair.a.test(textA);
92
+ const aHasB = pair.b.test(textA);
93
+ const bHasA = pair.a.test(textB);
94
+ const bHasB = pair.b.test(textB);
95
+ // Exclusive containment: each text must contain exactly one term of the
96
+ // pair, and opposite ones. A text containing both terms ("is not …")
97
+ // takes no position and never counts as opposing.
98
+ if ((aHasA && !aHasB && bHasB && !bHasA) || (aHasB && !aHasA && bHasA && !bHasB)) {
99
+ return true;
100
+ }
101
+ }
102
+ return false;
103
+ }
104
+ /**
105
+ * Detect findings that reach opposite conclusions. Word-boundary matching
106
+ * prevents substring traps ("unsafe" does not match "safe").
107
+ *
108
+ * With `specificOnly`, only high-precision pairs count — used for the merge
109
+ * veto, where a false positive fragments a genuine duplicate group. The
110
+ * default (all pairs) is for dispute flagging, where a false positive just
111
+ * adds a warning.
112
+ */
113
+ export function hasOpposingSentiment(a, b, specificOnly = false) {
114
+ if (textOpposes(a.title, b.title, false, specificOnly))
115
+ return true;
116
+ if (textOpposes(a.description, b.description, true, specificOnly))
117
+ return true;
118
+ return false;
119
+ }
120
+ /**
121
+ * Concept taxonomy for cross-model dedup. Models phrase the same issue so
122
+ * differently that genuine duplicates score only 0.29–0.55 on token
123
+ * similarity (see jaccardThreshold calibration note in config/defaults.ts).
124
+ * When two findings at the SAME location both name the same issue concept,
125
+ * that is stronger evidence of duplication than their wording overlap.
126
+ *
127
+ * Phrases are matched at word boundaries — single generic words ("query",
128
+ * "input", "auth") are deliberately absent because substring taxonomies
129
+ * merge strangers. Calibrated against the fixture corpus in test/fixtures.
130
+ */
131
+ const ISSUE_CONCEPTS = {
132
+ sql_injection: ['sql injection', 'sqli', 'cwe-89'],
133
+ xss: ['xss', 'cross-site scripting', 'cwe-79'],
134
+ command_injection: ['command injection', 'shell injection', 'cwe-78', 'rce'],
135
+ hardcoded_secrets: [
136
+ 'hardcoded secret',
137
+ 'hardcoded credential',
138
+ 'hardcoded password',
139
+ 'hardcoded jwt',
140
+ 'secret key',
141
+ 'api key',
142
+ 'cwe-798',
143
+ ],
144
+ auth_bypass: [
145
+ 'idor',
146
+ 'authorization check',
147
+ 'ownership check',
148
+ 'auth bypass',
149
+ 'authentication bypass',
150
+ 'privilege escalation',
151
+ ],
152
+ weak_crypto: ['weak random', 'math.random', 'weak crypto', 'insecure random', 'cwe-338'],
153
+ race_condition: ['race condition', 'toctou', 'pid reuse'],
154
+ memory_leak: ['memory leak', 'unbounded cache', 'unbounded growth'],
155
+ token_exposure: [
156
+ 'token exposure',
157
+ 'token leak',
158
+ 'key exposure',
159
+ 'exposed in command line',
160
+ 'exfiltration',
161
+ ],
162
+ missing_pagination: ['pagination', 'unbounded query', 'unbounded listing'],
163
+ };
164
+ const CONCEPT_MATCHERS = Object.entries(ISSUE_CONCEPTS).map(([concept, phrases]) => ({
165
+ concept,
166
+ patterns: phrases.map((p) => {
167
+ // \b only works when the phrase starts and ends on word characters —
168
+ // reject taxonomy entries that would silently never (or wrongly) match.
169
+ if (!/^[\p{L}\p{N}].*[\p{L}\p{N}]$/u.test(p)) {
170
+ throw new Error(`ISSUE_CONCEPTS phrase must start and end with a word character: "${p}"`);
171
+ }
172
+ // Spaces match any whitespace run so "cross-site\n scripting" still
173
+ // hits — a missed match only means falling back to token similarity
174
+ // (under-merge), but there's no reason to be brittle about line wraps.
175
+ return new RegExp(`\\b${p.replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replace(/ /g, '\\s+')}\\b`, 'i');
176
+ }),
177
+ }));
178
+ /**
179
+ * Concepts per finding are memoized: the deduper compares every pair, so
180
+ * uncached extraction would run the full taxonomy regex scan O(n²·k) times.
181
+ */
182
+ const conceptCache = new WeakMap();
183
+ function extractConcepts(finding) {
184
+ const cached = conceptCache.get(finding);
185
+ if (cached)
186
+ return cached;
187
+ const text = `${finding.title} ${finding.description}`;
188
+ const out = new Set();
189
+ for (const { concept, patterns } of CONCEPT_MATCHERS) {
190
+ if (patterns.some((re) => re.test(text)))
191
+ out.add(concept);
192
+ }
193
+ conceptCache.set(finding, out);
194
+ return out;
195
+ }
196
+ const CONCEPT_BOOST_BASE = 0.8;
197
+ const CONCEPT_BOOST_PER_EXTRA = 0.05;
198
+ /**
199
+ * Concept-level similarity: 0.8+ when both findings name the same issue
200
+ * concept AND their line ranges strictly overlap. Proximity within the
201
+ * dedup window is deliberately NOT enough — two different SQL injections a
202
+ * few lines apart share the concept but are different findings, and the
203
+ * fixture corpus shows genuine cross-model duplicates point at overlapping
204
+ * ranges. Benchmarked in the RCL-9 PR: fixture recall 0.70 → 1.00 at
205
+ * precision 1.00, no new false merges on adversarial same-concept pairs
206
+ * (the ungated code-council version added one).
207
+ */
208
+ export function conceptSimilarity(a, b) {
209
+ if (!linesOverlap(a, b, 0))
210
+ return 0;
211
+ const conceptsA = extractConcepts(a);
212
+ if (conceptsA.size === 0)
213
+ return 0;
214
+ const conceptsB = extractConcepts(b);
215
+ let overlap = 0;
216
+ for (const c of conceptsA)
217
+ if (conceptsB.has(c))
218
+ overlap++;
219
+ if (overlap === 0)
220
+ return 0;
221
+ return Math.min(1.0, CONCEPT_BOOST_BASE + (overlap - 1) * CONCEPT_BOOST_PER_EXTRA);
222
+ }
223
+ export function linesOverlap(a, b, window) {
224
+ // Ranges overlap when the gap between them is at most `window` lines.
225
+ // Expanding BOTH ranges by the window would double the configured
226
+ // distance (a window of 5 merging findings 10 lines apart).
227
+ return a.startLine - window <= b.endLine && b.startLine - window <= a.endLine;
228
+ }
229
+ function areSameFile(a, b) {
230
+ return a.file === b.file;
231
+ }
232
+ function sameCategory(a, b) {
233
+ return a.category === b.category;
234
+ }
235
+ /**
236
+ * Models routinely disagree on category boundaries (correctness vs
237
+ * best-practices, security vs correctness), so a category mismatch is
238
+ * evidence that findings differ — not proof. Cross-category pairs may still
239
+ * merge, but only on stronger text similarity.
240
+ */
241
+ const CROSS_CATEGORY_FACTOR = 1.5;
242
+ function areDuplicates(a, b, jaccardThreshold, lineWindow) {
243
+ if (!areSameFile(a, b))
244
+ return false;
245
+ if (!linesOverlap(a, b, lineWindow))
246
+ return false;
247
+ // Never merge findings that clearly reach opposite conclusions — they must
248
+ // surface as separate (disputed) groups rather than silently collapse into
249
+ // one. Only specific pairs veto; generic-pair contradictions merge and are
250
+ // flagged as intra-group disputes by the voter instead.
251
+ if (hasOpposingSentiment(a, b, true))
252
+ return false;
253
+ // Cap at 0.9 so a high configured threshold can't silently make
254
+ // cross-category merges (near-)impossible — a bar of 1.0 would only ever
255
+ // match token-identical findings
256
+ const threshold = sameCategory(a, b)
257
+ ? jaccardThreshold
258
+ : Math.min(0.9, jaccardThreshold * CROSS_CATEGORY_FACTOR);
259
+ // max(): the concept boost can add merges token similarity misses, but
260
+ // must never take one away that tokens alone would have made.
261
+ return Math.max(combinedSimilarity(a, b), conceptSimilarity(a, b)) >= threshold;
262
+ }
263
+ /**
264
+ * Choose the representative finding from a group:
265
+ * - Prefer higher severity
266
+ * - Break ties by description length (more detail wins)
267
+ */
268
+ function chooseRepresentative(members) {
269
+ const severityOrder = { critical: 0, important: 1, minor: 2, nitpick: 3 };
270
+ return members.reduce((best, curr) => {
271
+ const bestScore = severityOrder[best.finding.severity];
272
+ const currScore = severityOrder[curr.finding.severity];
273
+ if (currScore < bestScore)
274
+ return curr;
275
+ if (currScore === bestScore && curr.finding.description.length > best.finding.description.length)
276
+ return curr;
277
+ return best;
278
+ });
279
+ }
280
+ function collectMemberContributions(members) {
281
+ const refs = new Map();
282
+ for (const member of members)
283
+ for (const ref of member.contributions ?? []) {
284
+ refs.set(`${ref.reviewIndex}:${ref.findingIndex}`, ref);
285
+ }
286
+ return [...refs.values()].sort((left, right) => left.reviewIndex - right.reviewIndex || left.findingIndex - right.findingIndex);
287
+ }
288
+ /** Only destructive dedupe sites carry lineage onto their chosen survivor. */
289
+ function chooseCollapsedRepresentative(members, collectContributions) {
290
+ const representative = chooseRepresentative(members);
291
+ return collectContributions ? { ...representative, contributions: collectMemberContributions(members) } : representative;
292
+ }
293
+ /**
294
+ * Union-Find grouping with path compression.
295
+ */
296
+ function groupTagged(items, jaccardThreshold, lineWindow) {
297
+ const parent = items.map((_, i) => i);
298
+ function find(i) {
299
+ if (parent[i] !== i)
300
+ parent[i] = find(parent[i]);
301
+ return parent[i];
302
+ }
303
+ for (let i = 0; i < items.length; i++) {
304
+ for (let j = i + 1; j < items.length; j++) {
305
+ if (areDuplicates(items[i].finding, items[j].finding, jaccardThreshold, lineWindow)) {
306
+ const rootI = find(i);
307
+ const rootJ = find(j);
308
+ if (rootI !== rootJ)
309
+ parent[rootJ] = rootI;
310
+ }
311
+ }
312
+ }
313
+ const groups = new Map();
314
+ for (let i = 0; i < items.length; i++) {
315
+ const root = find(i);
316
+ const existing = groups.get(root) ?? [];
317
+ existing.push(items[i]);
318
+ groups.set(root, existing);
319
+ }
320
+ return [...groups.values()];
321
+ }
322
+ /**
323
+ * Union-Find is transitive: A≈B and B≈C put A and C in one group even when
324
+ * A and C are dissimilar. Split such chains by greedily re-clustering around
325
+ * representatives — every member of a final group is a duplicate of its
326
+ * representative, not just of some neighbor.
327
+ */
328
+ function splitIncoherent(members, jaccardThreshold, lineWindow) {
329
+ const result = [];
330
+ let remaining = members;
331
+ while (remaining.length > 0) {
332
+ const rep = chooseRepresentative(remaining);
333
+ const coherent = [];
334
+ const rest = [];
335
+ for (const m of remaining) {
336
+ if (m === rep || areDuplicates(rep.finding, m.finding, jaccardThreshold, lineWindow)) {
337
+ coherent.push(m);
338
+ }
339
+ else {
340
+ rest.push(m);
341
+ }
342
+ }
343
+ result.push(coherent);
344
+ remaining = rest;
345
+ }
346
+ return result;
347
+ }
348
+ /**
349
+ * Cross-model wording diverges much more than the ordinary pairwise dedup
350
+ * threshold allows. A relaxed lexical edge is only safe when it is backed by
351
+ * independent agreement: at least three distinct model-role assignments must participate in
352
+ * the resulting location cluster. This turns agreement into evidence for a
353
+ * merge without weakening the normal threshold for one- and two-model noise.
354
+ *
355
+ * The 0.20 title floor is calibrated against the RCL-17 production corpus.
356
+ * A title edge also needs overlap in either the description or proposed fix;
357
+ * title proximity alone is not enough to bridge separate defects at the same
358
+ * lines. Weighted graph communities and a 2-core keep sparse lexical bridges
359
+ * from merging a whole hunk. A fringe variant can join only when two different
360
+ * reviewer assignments support one unambiguous corroborated community.
361
+ */
362
+ const CORROBORATED_TITLE_THRESHOLD = 0.2; // title-token Jaccard
363
+ const CORROBORATED_DETAIL_THRESHOLD = 0.1; // description/fix token Jaccard
364
+ const CORROBORATED_MIN_REVIEWERS = 3; // distinct model::role assignments
365
+ const CORROBORATED_MIN_DENSITY = 0.2; // observed / possible relaxed edges
366
+ const CORROBORATED_ATTACHMENT_THRESHOLD = 0.12; // weighted title+description score
367
+ const CORROBORATED_ATTACHMENT_FIX_THRESHOLD = 0.15; // fix-token Jaccard for one-anchor variants
368
+ const CORROBORATED_ATTACHMENT_SUPPORT = 2; // distinct model::role assignments
369
+ const CORROBORATED_SINGLE_ANCHOR_SUPPORT = 3; // extra support for one-token bridges
370
+ const CORROBORATED_MAX_ATTACHMENT_ROUNDS = 2; // bound transitive fringe growth
371
+ const CORROBORATED_NEIGHBORHOOD_THRESHOLD = 0.11; // loose combined lexical partition
372
+ const CORROBORATED_NEIGHBORHOOD_DENSITY = 0.5; // observed / possible weak edges
373
+ const CORROBORATED_MAX_NEIGHBORHOOD_SPAN = 50; // broad findings/components cannot bridge
374
+ // A location-only rescue must contain at least two lexical confirmations
375
+ // already collapsed by the strict pass. Requiring that redundancy keeps an
376
+ // all-singleton neighborhood from manufacturing consensus from proximity.
377
+ const CORROBORATED_NEIGHBORHOOD_CONFIRMATIONS = 2;
378
+ const CORROBORATED_TIE_EPSILON = 1e-9;
379
+ const CORROBORATION_STOPWORDS = new Set([
380
+ ...STOPWORDS,
381
+ 'all', 'allow', 'allows', 'any', 'can', 'could', 'did', 'do', 'does',
382
+ 'had', 'has', 'have', 'lacks', 'may', 'might', 'missing', 'new', 'no',
383
+ 'nil', 'not', 'now', 'only', 'same', 'should', 'still', 'use', 'used', 'uses',
384
+ 'using', 'will', 'without', 'would', 'code', 'function', 'issue', 'test', 'tests',
385
+ 'config', 'configuration', 'file', 'files', 'generated',
386
+ ]);
387
+ function corroborationTokens(text) {
388
+ return new Set(text
389
+ .toLowerCase()
390
+ // Preserve underscores inside identifiers. Splitting `trigger_label`
391
+ // into two ordinary words lets a shared function name create a false
392
+ // semantic edge between unrelated findings about that function.
393
+ .match(/[\p{L}\p{N}_]+/gu)
394
+ ?.filter((token) => token.length > 1 && !CORROBORATION_STOPWORDS.has(token)) ?? []);
395
+ }
396
+ function corroborationFieldSimilar(a, b) {
397
+ const tokensA = corroborationTokens(a ?? '');
398
+ const tokensB = corroborationTokens(b ?? '');
399
+ // At least one shared semantic token must remain after generic language is
400
+ // removed. A shared code identifier alone is only evidence that findings
401
+ // discuss the same function, not the same defect within that function.
402
+ const shared = [...tokensA].filter((token) => tokensB.has(token));
403
+ return shared.some((token) => !token.includes('_')) || shared.length >= 2;
404
+ }
405
+ function nonEmptyFieldSimilarity(a, b) {
406
+ const tokensA = tokenize(a ?? '');
407
+ const tokensB = tokenize(b ?? '');
408
+ if (tokensA.size === 0 || tokensB.size === 0)
409
+ return 0;
410
+ return jaccardOfSets(tokensA, tokensB);
411
+ }
412
+ function compatibleClaimKinds(a, b) {
413
+ const conceptsA = extractConcepts(a);
414
+ const conceptsB = extractConcepts(b);
415
+ return !(conceptsA.size > 0 &&
416
+ conceptsB.size > 0 &&
417
+ ![...conceptsA].some((concept) => conceptsB.has(concept)));
418
+ }
419
+ function relaxedLocationDuplicate(a, b, lineWindow) {
420
+ if (!areSameFile(a, b))
421
+ return false;
422
+ if (!linesOverlap(a, b, lineWindow))
423
+ return false;
424
+ if (hasOpposingSentiment(a, b, true))
425
+ return false;
426
+ // Known taxonomy concepts can sit beside one another and repeat the same
427
+ // nouns while requiring different fixes. Do not let the relaxed path use
428
+ // one as a bridge into the other; the ordinary similarity path remains
429
+ // available for genuinely near-identical wording.
430
+ if (!compatibleClaimKinds(a, b))
431
+ return false;
432
+ const descriptionSimilarity = nonEmptyFieldSimilarity(a.description, b.description);
433
+ const fixSimilarity = nonEmptyFieldSimilarity(a.suggestedFix, b.suggestedFix);
434
+ return (jaccardSimilarity(a.title, b.title) >= CORROBORATED_TITLE_THRESHOLD &&
435
+ corroborationFieldSimilar(a.title, b.title) &&
436
+ Math.max(descriptionSimilarity, fixSimilarity) >= CORROBORATED_DETAIL_THRESHOLD);
437
+ }
438
+ function relaxedLocationWeight(a, b, lineWindow) {
439
+ if (!relaxedLocationDuplicate(a, b, lineWindow))
440
+ return 0;
441
+ return (jaccardSimilarity(a.title, b.title) * 0.25 +
442
+ nonEmptyFieldSimilarity(a.description, b.description) * 0.2 +
443
+ nonEmptyFieldSimilarity(a.suggestedFix, b.suggestedFix) * 0.55);
444
+ }
445
+ function groupsRelaxedWeight(a, b, lineWindow) {
446
+ let weight = 0;
447
+ for (const left of a) {
448
+ for (const right of b) {
449
+ if (`${left.model}::${left.role}` === `${right.model}::${right.role}`)
450
+ continue;
451
+ weight = Math.max(weight, relaxedLocationWeight(left.finding, right.finding, lineWindow));
452
+ }
453
+ }
454
+ return weight;
455
+ }
456
+ function groupsShareLocation(a, b, lineWindow) {
457
+ return a.every((left) => b.every((right) => areSameFile(left.finding, right.finding) &&
458
+ linesOverlap(left.finding, right.finding, lineWindow)));
459
+ }
460
+ function clustersClaimsAreCompatible(a, b) {
461
+ return a.every((left) => b.every((right) => left.every((leftMember) => right.every((rightMember) => compatibleClaimKinds(leftMember.finding, rightMember.finding)))));
462
+ }
463
+ function clustersAreCompatible(a, b, lineWindow) {
464
+ return a.every((left) => b.every((right) => groupsShareLocation(left, right, lineWindow))) && clustersClaimsAreCompatible(a, b);
465
+ }
466
+ function clustersShareFile(a, b) {
467
+ const filesA = new Set(a.flat().map((member) => member.finding.file));
468
+ const filesB = new Set(b.flat().map((member) => member.finding.file));
469
+ return filesA.size === filesB.size && [...filesA].every((file) => filesB.has(file));
470
+ }
471
+ function splitCompatibleChains(component, lineWindow, compareText) {
472
+ const clusters = [];
473
+ const ordered = [...component].sort((a, b) => {
474
+ const left = chooseRepresentative(a).finding;
475
+ const right = chooseRepresentative(b).finding;
476
+ return compareText(left.file, right.file) ||
477
+ left.startLine - right.startLine ||
478
+ left.endLine - right.endLine ||
479
+ compareText(left.title, right.title);
480
+ });
481
+ for (const group of ordered) {
482
+ const compatible = clusters.find((cluster) => clustersAreCompatible([group], cluster, lineWindow));
483
+ if (compatible)
484
+ compatible.push(group);
485
+ else
486
+ clusters.push([group]);
487
+ }
488
+ return clusters;
489
+ }
490
+ function distinctReviewers(groups) {
491
+ return new Set(groups.flatMap((group) => group.map((member) => `${member.model}::${member.role}`))).size;
492
+ }
493
+ function relaxedEdgeDensity(groups, indexes, adjacency) {
494
+ if (groups.length < 2)
495
+ return 0;
496
+ let edges = 0;
497
+ for (let i = 0; i < groups.length; i++) {
498
+ for (let j = i + 1; j < groups.length; j++) {
499
+ const leftIndex = indexes.get(groups[i]);
500
+ const rightIndex = indexes.get(groups[j]);
501
+ if (leftIndex === undefined || rightIndex === undefined) {
502
+ throw new Error('Corroborated group is missing from its adjacency index');
503
+ }
504
+ if (adjacency[leftIndex].has(rightIndex))
505
+ edges++;
506
+ }
507
+ }
508
+ return edges / ((groups.length * (groups.length - 1)) / 2);
509
+ }
510
+ /**
511
+ * Split a weak-similarity graph into weighted modularity communities before
512
+ * treating a connected component as one concept. This is the key over-merge
513
+ * boundary: several independently corroborated defects can occupy the same
514
+ * hunk, and a few shared diff tokens must not bridge those concepts together.
515
+ */
516
+ function relaxedCommunities(weights) {
517
+ const count = weights.length;
518
+ const labels = Array.from({ length: count }, (_, index) => index);
519
+ const degrees = weights.map((row) => [...row.values()].reduce((sum, weight) => sum + weight, 0));
520
+ const totalWeight = degrees.reduce((sum, degree) => sum + degree, 0) / 2;
521
+ if (totalWeight === 0)
522
+ return labels;
523
+ const communityDegrees = [...degrees];
524
+ const communitySizes = Array.from({ length: count }, () => 1);
525
+ for (let pass = 0; pass < Math.max(1, count * count); pass++) {
526
+ let moved = false;
527
+ for (let index = 0; index < count; index++) {
528
+ if (degrees[index] === 0)
529
+ continue;
530
+ const current = labels[index];
531
+ const weightByCommunity = new Map();
532
+ for (const [neighbor, weight] of weights[index]) {
533
+ const label = labels[neighbor];
534
+ weightByCommunity.set(label, (weightByCommunity.get(label) ?? 0) + weight);
535
+ }
536
+ // Remove the node before evaluating insertion into each neighboring
537
+ // community. This is the standard first phase of weighted Louvain; the
538
+ // previous implementation compared against stale community totals.
539
+ communityDegrees[current] = Math.max(0, communityDegrees[current] - degrees[index]);
540
+ communitySizes[current]--;
541
+ const emptyCommunity = communitySizes.findIndex((size) => size === 0);
542
+ const candidates = new Set([
543
+ ...weightByCommunity.keys(),
544
+ current,
545
+ ...(emptyCommunity >= 0 ? [emptyCommunity] : []),
546
+ ]);
547
+ let best = current;
548
+ let bestGain = (weightByCommunity.get(current) ?? 0) -
549
+ (communityDegrees[current] * degrees[index]) / (2 * totalWeight);
550
+ for (const candidate of candidates) {
551
+ const gain = (weightByCommunity.get(candidate) ?? 0) -
552
+ (communityDegrees[candidate] * degrees[index]) / (2 * totalWeight);
553
+ if (gain > bestGain + CORROBORATED_TIE_EPSILON ||
554
+ (best !== current &&
555
+ Math.abs(gain - bestGain) <= CORROBORATED_TIE_EPSILON &&
556
+ candidate < best)) {
557
+ best = candidate;
558
+ bestGain = gain;
559
+ }
560
+ }
561
+ communityDegrees[best] += degrees[index];
562
+ communitySizes[best]++;
563
+ labels[index] = best;
564
+ if (best !== current)
565
+ moved = true;
566
+ }
567
+ if (!moved)
568
+ break;
569
+ }
570
+ return labels;
571
+ }
572
+ function conceptAnchors(groups) {
573
+ const members = groups.flat();
574
+ const reviewers = new Map();
575
+ for (const member of members) {
576
+ for (const token of corroborationTokens(member.finding.title)) {
577
+ const tokenReviewers = reviewers.get(token) ?? new Set();
578
+ tokenReviewers.add(`${member.model}::${member.role}`);
579
+ reviewers.set(token, tokenReviewers);
580
+ }
581
+ }
582
+ const reviewerCount = distinctReviewers(groups);
583
+ // A one-reviewer fringe must never manufacture its own semantic anchor;
584
+ // only an independently corroborated target contributes anchor evidence.
585
+ const support = Math.max(2, Math.ceil(reviewerCount * 0.4));
586
+ return new Set([...reviewers]
587
+ .filter(([, tokenReviewers]) => tokenReviewers.size >= support)
588
+ .map(([token]) => token));
589
+ }
590
+ function sharedAnchorCount(a, b) {
591
+ let count = 0;
592
+ for (const token of a)
593
+ if (b.has(token))
594
+ count++;
595
+ return count;
596
+ }
597
+ function agreementClusterKey(cluster) {
598
+ const finding = chooseRepresentative(cluster.groups.flat()).finding;
599
+ const members = cluster.groups.flat()
600
+ .map((member) => `${member.model}::${member.role}::${member.finding.id}`)
601
+ .sort()
602
+ .join('\0');
603
+ return `${finding.file}\0${String(finding.startLine).padStart(10, '0')}\0${String(finding.endLine).padStart(10, '0')}\0${finding.title}\0${finding.id}\0${members}`;
604
+ }
605
+ function compareAgreementClusters(a, b) {
606
+ const left = agreementClusterKey(a);
607
+ const right = agreementClusterKey(b);
608
+ return left < right ? -1 : left > right ? 1 : 0;
609
+ }
610
+ function mergeAnchoredCommunities(clusters, lineWindow) {
611
+ const remaining = [...clusters].sort(compareAgreementClusters);
612
+ let merged = true;
613
+ while (merged) {
614
+ merged = false;
615
+ outer: for (let i = 0; i < remaining.length; i++) {
616
+ const target = remaining[i];
617
+ if (!target.corroborated)
618
+ continue;
619
+ for (let j = i + 1; j < remaining.length; j++) {
620
+ const candidate = remaining[j];
621
+ if (!candidate.corroborated ||
622
+ !clustersAreCompatible(target.groups, candidate.groups, lineWindow)) {
623
+ continue;
624
+ }
625
+ const targetAnchors = conceptAnchors(target.groups);
626
+ const candidateAnchors = conceptAnchors(candidate.groups);
627
+ if (targetAnchors.size === 0 || candidateAnchors.size === 0)
628
+ continue;
629
+ const shared = [...targetAnchors].filter((token) => candidateAnchors.has(token));
630
+ if (shared.length < 2 &&
631
+ !shared.some((token) => token.length >= 6 && !token.includes('_')))
632
+ continue;
633
+ const hasCrossEdge = target.groups.flat().some((left) => candidate.groups.flat().some((right) => `${left.model}::${left.role}` !== `${right.model}::${right.role}` &&
634
+ relaxedLocationWeight(left.finding, right.finding, lineWindow) > 0));
635
+ // Anchors alone only prove that both communities discuss the same
636
+ // code. Require an observed semantic edge as evidence that they make
637
+ // the same claim; this keeps two defects in one helper separate.
638
+ if (!hasCrossEdge)
639
+ continue;
640
+ target.groups.push(...candidate.groups);
641
+ candidate.groups = [];
642
+ remaining.splice(j, 1);
643
+ remaining.sort(compareAgreementClusters);
644
+ merged = true;
645
+ break outer;
646
+ }
647
+ }
648
+ }
649
+ return remaining;
650
+ }
651
+ function agreementClusters(groups, lineWindow, compareText) {
652
+ groups = [...groups].sort((a, b) => {
653
+ const left = agreementClusterKey({ groups: [a], corroborated: false });
654
+ const right = agreementClusterKey({ groups: [b], corroborated: false });
655
+ return left < right ? -1 : left > right ? 1 : 0;
656
+ });
657
+ const weights = groups.map(() => new Map());
658
+ const adjacency = groups.map(() => new Set());
659
+ const indexes = new Map(groups.map((group, index) => [group, index]));
660
+ for (let i = 0; i < groups.length; i++) {
661
+ for (let j = i + 1; j < groups.length; j++) {
662
+ const weight = groupsRelaxedWeight(groups[i], groups[j], lineWindow);
663
+ if (weight === 0)
664
+ continue;
665
+ weights[i].set(j, weight);
666
+ weights[j].set(i, weight);
667
+ adjacency[i].add(j);
668
+ adjacency[j].add(i);
669
+ }
670
+ }
671
+ const communities = relaxedCommunities(weights);
672
+ for (let i = 0; i < groups.length; i++) {
673
+ for (const j of adjacency[i]) {
674
+ if (communities[i] !== communities[j])
675
+ adjacency[i].delete(j);
676
+ }
677
+ }
678
+ // Keep only the graph's 2-core as corroboration evidence. A chain of weak
679
+ // similarities can otherwise pull unrelated findings into one component;
680
+ // every core group must have two independent semantic links. Peeled fringe
681
+ // components remain available for the stricter attachment pass below.
682
+ const inCore = groups.map(() => true);
683
+ const degree = adjacency.map((neighbors) => neighbors.size);
684
+ const queue = degree.flatMap((value, index) => (value < 2 ? [index] : []));
685
+ for (let cursor = 0; cursor < queue.length; cursor++) {
686
+ const index = queue[cursor];
687
+ if (!inCore[index])
688
+ continue;
689
+ inCore[index] = false;
690
+ for (const neighbor of adjacency[index]) {
691
+ if (!inCore[neighbor])
692
+ continue;
693
+ degree[neighbor]--;
694
+ if (degree[neighbor] < 2)
695
+ queue.push(neighbor);
696
+ }
697
+ }
698
+ function connectedComponents(wantedCore) {
699
+ const seen = new Set();
700
+ const out = [];
701
+ for (let start = 0; start < groups.length; start++) {
702
+ if (inCore[start] !== wantedCore || seen.has(start))
703
+ continue;
704
+ const indexes = [];
705
+ const pending = [start];
706
+ seen.add(start);
707
+ while (pending.length > 0) {
708
+ const index = pending.pop();
709
+ indexes.push(index);
710
+ for (const neighbor of adjacency[index]) {
711
+ if (inCore[neighbor] !== wantedCore || seen.has(neighbor))
712
+ continue;
713
+ seen.add(neighbor);
714
+ pending.push(neighbor);
715
+ }
716
+ }
717
+ out.push(indexes.map((index) => groups[index]));
718
+ }
719
+ return out;
720
+ }
721
+ const coreClusters = connectedComponents(true)
722
+ .flatMap((component) => splitCompatibleChains(component, lineWindow, compareText))
723
+ .map((component) => ({
724
+ groups: component,
725
+ corroborated: distinctReviewers(component) >= CORROBORATED_MIN_REVIEWERS &&
726
+ relaxedEdgeDensity(component, indexes, adjacency) >= CORROBORATED_MIN_DENSITY,
727
+ }));
728
+ const fringeClusters = connectedComponents(false).map((component) => ({ groups: component, corroborated: false }));
729
+ return mergeAnchoredCommunities([...coreClusters, ...fringeClusters], lineWindow);
730
+ }
731
+ function attachmentEvidence(singleton, cluster, lineWindow) {
732
+ const supporters = new Map();
733
+ const targetAnchors = conceptAnchors(cluster.groups);
734
+ const singletonAnchors = corroborationTokens(`${singleton.finding.title} ${singleton.finding.description ?? ''} ${singleton.finding.suggestedFix ?? ''}`);
735
+ const anchorCount = sharedAnchorCount(singletonAnchors, targetAnchors);
736
+ const singletonReviewer = `${singleton.model}::${singleton.role}`;
737
+ const fixSupport = new Set(cluster.groups.flat()
738
+ .filter((member) => `${member.model}::${member.role}` !== singletonReviewer &&
739
+ nonEmptyFieldSimilarity(singleton.finding.suggestedFix, member.finding.suggestedFix) >=
740
+ CORROBORATED_ATTACHMENT_FIX_THRESHOLD)
741
+ .map((member) => `${member.model}::${member.role}`)).size;
742
+ if (anchorCount < 2 && !(anchorCount === 1 && fixSupport >= 2)) {
743
+ return { support: 0, strength: 0 };
744
+ }
745
+ for (const group of cluster.groups) {
746
+ for (const member of group) {
747
+ if (!areSameFile(singleton.finding, member.finding))
748
+ continue;
749
+ if (!linesOverlap(singleton.finding, member.finding, lineWindow))
750
+ continue;
751
+ if (hasOpposingSentiment(singleton.finding, member.finding, true))
752
+ continue;
753
+ if (!compatibleClaimKinds(singleton.finding, member.finding))
754
+ continue;
755
+ if (!corroborationFieldSimilar(singleton.finding.title, member.finding.title))
756
+ continue;
757
+ const key = `${member.model}::${member.role}`;
758
+ if (key === singletonReviewer)
759
+ continue;
760
+ const similarity = combinedSimilarity(singleton.finding, member.finding);
761
+ if (similarity >= CORROBORATED_ATTACHMENT_THRESHOLD) {
762
+ supporters.set(key, Math.max(supporters.get(key) ?? 0, similarity));
763
+ }
764
+ }
765
+ }
766
+ if (anchorCount === 1 && supporters.size < CORROBORATED_SINGLE_ANCHOR_SUPPORT) {
767
+ return { support: 0, strength: 0 };
768
+ }
769
+ return {
770
+ support: supporters.size,
771
+ strength: [...supporters.values()].reduce((sum, value) => sum + value, 0),
772
+ };
773
+ }
774
+ function mergeCorroboratedLocationClusters(groups, lineWindow, compareText) {
775
+ const clusters = agreementClusters(groups, lineWindow, compareText);
776
+ // A lone wording variant can sit just below the calibrated title floor.
777
+ // Attach it only to one unambiguous corroborated cluster, with support from
778
+ // at least two distinct reviewer assignments. Multi-member components are
779
+ // absorbed only when every member supports the same target; this keeps a
780
+ // nearby second concept such as ao-7536's log-capture cluster separate.
781
+ for (let round = 0; round < Math.min(clusters.length, CORROBORATED_MAX_ATTACHMENT_ROUNDS); round++) {
782
+ const attachments = [];
783
+ for (const candidate of clusters) {
784
+ if (candidate.corroborated)
785
+ continue;
786
+ // A strict group already backed by several reviewers is independent
787
+ // evidence for a concept, even when it has no relaxed graph core of its
788
+ // own. Do not absorb that consensus into a neighboring community.
789
+ if (distinctReviewers(candidate.groups) >= CORROBORATED_MIN_REVIEWERS)
790
+ continue;
791
+ const candidateMembers = candidate.groups.flat();
792
+ if (candidateMembers.length === 0)
793
+ continue;
794
+ const ranked = clusters
795
+ .filter((target) => target !== candidate &&
796
+ target.corroborated &&
797
+ clustersShareFile(candidate.groups, target.groups) &&
798
+ clustersClaimsAreCompatible(candidate.groups, target.groups))
799
+ .map((target) => {
800
+ const evidence = candidateMembers.map((member) => attachmentEvidence(member, target, lineWindow));
801
+ const supports = evidence.map((item) => item.support);
802
+ return {
803
+ target,
804
+ support: Math.min(...supports),
805
+ strength: evidence.reduce((sum, item) => sum + item.strength, 0),
806
+ size: target.groups.flat().length,
807
+ key: chooseRepresentative(target.groups.flat()).finding.title,
808
+ };
809
+ })
810
+ .filter(({ support }) => support >= CORROBORATED_ATTACHMENT_SUPPORT)
811
+ .sort((a, b) => b.support - a.support ||
812
+ b.strength - a.strength ||
813
+ b.size - a.size ||
814
+ compareText(a.key, b.key));
815
+ if (ranked.length === 0)
816
+ continue;
817
+ if (ranked[1] &&
818
+ ranked[0].support === ranked[1].support &&
819
+ Math.abs(ranked[0].strength - ranked[1].strength) <= CORROBORATED_TIE_EPSILON &&
820
+ ranked[0].size === ranked[1].size) {
821
+ continue;
822
+ }
823
+ attachments.push({ candidate, target: ranked[0].target });
824
+ }
825
+ if (attachments.length === 0)
826
+ break;
827
+ // Apply a round only after every candidate was ranked against the same
828
+ // frozen state. Later rounds may use the newly attached evidence without
829
+ // making peers in the same round order-dependent.
830
+ for (const { candidate, target } of attachments) {
831
+ target.groups.push(...candidate.groups);
832
+ candidate.groups = [];
833
+ }
834
+ }
835
+ // Fringe evidence can provide the first real lexical edge between two
836
+ // independently corroborated communities. Revisit anchor merging once
837
+ // after attachment so that bridge is considered, while still requiring an
838
+ // observed cross-community edge rather than anchors alone.
839
+ const mergedClusters = mergeAnchoredCommunities(clusters.filter((cluster) => cluster.groups.length > 0), lineWindow);
840
+ return mergedClusters.flatMap((cluster) => {
841
+ if (cluster.groups.length === 0)
842
+ return [];
843
+ if (!cluster.corroborated)
844
+ return cluster.groups;
845
+ return [cluster.groups.flat()];
846
+ });
847
+ }
848
+ /**
849
+ * Rescue agreement that has no lexical center at all. This pass only considers
850
+ * weak one- or two-reviewer groups: a group already backed by three reviewers
851
+ * is evidence for an independent concept and is never absorbed by proximity.
852
+ * This deliberately narrow fallback activates only when the agreement gate is
853
+ * at least five reviewers. It merges a dense component only when it hits that
854
+ * gate exactly and contains two fewer groups than reviewers, proving that at
855
+ * least two lexical confirmations already collapsed in the strict pass. An
856
+ * all-singleton location neighborhood is therefore never enough by itself.
857
+ * This is the vocabulary-independent signal RCL-17 was missing, while the
858
+ * redundancy guard preserves nearby minority concepts such as ao-7467's
859
+ * typespec finding.
860
+ */
861
+ function mergeAgreementNeighborhoods(groups, lineWindow, minimumReviewers) {
862
+ function groupsTouch(a, b) {
863
+ return a.some((left) => b.some((right) => areSameFile(left.finding, right.finding) &&
864
+ linesOverlap(left.finding, right.finding, lineWindow)));
865
+ }
866
+ function weakSimilarity(a, b) {
867
+ let similarity = 0;
868
+ for (const left of a) {
869
+ for (const right of b) {
870
+ // One reviewer can emit multiple wording variants. Its own variants
871
+ // must not manufacture the edge that turns proximity into agreement.
872
+ if (`${left.model}::${left.role}` === `${right.model}::${right.role}`) {
873
+ continue;
874
+ }
875
+ similarity = Math.max(similarity, combinedSimilarity(left.finding, right.finding));
876
+ }
877
+ }
878
+ return similarity;
879
+ }
880
+ function claimsCanShareNeighborhood(a, b) {
881
+ return a.every((left) => b.every((right) => compatibleClaimKinds(left.finding, right.finding) &&
882
+ !hasOpposingSentiment(left.finding, right.finding, true)));
883
+ }
884
+ // With a smaller gate, subtracting the two required lexical confirmations
885
+ // leaves fewer than three groups, which is not enough to establish a dense
886
+ // neighborhood. The ordinary corroborated pass still handles those runs.
887
+ if (minimumReviewers <
888
+ CORROBORATED_MIN_REVIEWERS + CORROBORATED_NEIGHBORHOOD_CONFIRMATIONS) {
889
+ return groups;
890
+ }
891
+ const strong = groups.filter((group) => distinctReviewers([group]) >= CORROBORATED_MIN_REVIEWERS);
892
+ const eligible = groups.map((group) => distinctReviewers([group]) < CORROBORATED_MIN_REVIEWERS &&
893
+ group.every((member) => member.finding.endLine - member.finding.startLine <=
894
+ CORROBORATED_MAX_NEIGHBORHOOD_SPAN) &&
895
+ !strong.some((established) => groupsTouch(group, established)));
896
+ const parent = groups.map((_, index) => index);
897
+ const adjacency = groups.map(() => new Set());
898
+ function find(index) {
899
+ if (parent[index] !== index)
900
+ parent[index] = find(parent[index]);
901
+ return parent[index];
902
+ }
903
+ for (let i = 0; i < groups.length; i++) {
904
+ if (!eligible[i])
905
+ continue;
906
+ for (let j = i + 1; j < groups.length; j++) {
907
+ if (!eligible[j])
908
+ continue;
909
+ if (!groupsTouch(groups[i], groups[j]))
910
+ continue;
911
+ if (!claimsCanShareNeighborhood(groups[i], groups[j]))
912
+ continue;
913
+ if (weakSimilarity(groups[i], groups[j]) < CORROBORATED_NEIGHBORHOOD_THRESHOLD) {
914
+ continue;
915
+ }
916
+ const rootI = find(i);
917
+ const rootJ = find(j);
918
+ if (rootI !== rootJ)
919
+ parent[rootJ] = rootI;
920
+ adjacency[i].add(j);
921
+ adjacency[j].add(i);
922
+ }
923
+ }
924
+ const components = new Map();
925
+ for (let index = 0; index < groups.length; index++) {
926
+ if (!eligible[index])
927
+ continue;
928
+ const root = find(index);
929
+ const component = components.get(root) ?? [];
930
+ component.push(index);
931
+ components.set(root, component);
932
+ }
933
+ const mergedAt = new Map();
934
+ const consumed = new Set();
935
+ for (const indexes of components.values()) {
936
+ const component = indexes.map((index) => groups[index]);
937
+ if (component.length < CORROBORATED_MIN_REVIEWERS ||
938
+ component.length >
939
+ minimumReviewers - CORROBORATED_NEIGHBORHOOD_CONFIRMATIONS) {
940
+ continue;
941
+ }
942
+ const componentMembers = component.flat();
943
+ const componentSpan = Math.max(...componentMembers.map((member) => member.finding.endLine)) -
944
+ Math.min(...componentMembers.map((member) => member.finding.startLine));
945
+ if (componentSpan > CORROBORATED_MAX_NEIGHBORHOOD_SPAN)
946
+ continue;
947
+ // Union-find connectivity is transitive, but compatibility is not. A
948
+ // neutral bridge must not join findings that make opposing claims.
949
+ if (component.some((group, index) => component.slice(index + 1).some((other) => !claimsCanShareNeighborhood(group, other)))) {
950
+ continue;
951
+ }
952
+ // Exact support is intentional here. Larger, loosely connected location
953
+ // neighborhoods are more likely to contain multiple defects; stronger
954
+ // agreement must be established by the semantic corroboration pass.
955
+ if (distinctReviewers(component) !== minimumReviewers)
956
+ continue;
957
+ let edges = 0;
958
+ for (let i = 0; i < indexes.length; i++) {
959
+ for (let j = i + 1; j < indexes.length; j++) {
960
+ if (!adjacency[indexes[i]].has(indexes[j]))
961
+ continue;
962
+ edges++;
963
+ }
964
+ }
965
+ const possibleEdges = (indexes.length * (indexes.length - 1)) / 2;
966
+ if (edges / possibleEdges < CORROBORATED_NEIGHBORHOOD_DENSITY)
967
+ continue;
968
+ mergedAt.set(indexes[0], component.flat());
969
+ for (const index of indexes.slice(1))
970
+ consumed.add(index);
971
+ }
972
+ return groups.flatMap((group, index) => {
973
+ const merged = mergedAt.get(index);
974
+ if (merged)
975
+ return [merged];
976
+ return consumed.has(index) ? [] : [group];
977
+ });
978
+ }
979
+ /**
980
+ * Intra-review dedup is pairwise, so two dissimilar variants from one review
981
+ * can still land in the same final group via a bridge finding from another
982
+ * review. Collapse same-(model, role) members so a single reviewer never
983
+ * counts twice in consensus scores or the elevation support guard.
984
+ */
985
+ function collapseSameReviewer(members, collectContributions) {
986
+ const byReviewer = new Map();
987
+ for (const m of members) {
988
+ const key = `${m.model}::${m.role}`;
989
+ const existing = byReviewer.get(key) ?? [];
990
+ existing.push(m);
991
+ byReviewer.set(key, existing);
992
+ }
993
+ return [...byReviewer.values()].map((group) => group.length === 1 && !collectContributions ? group[0] : chooseCollapsedRepresentative(group, collectContributions));
994
+ }
995
+ /**
996
+ * A single model sometimes emits the same finding more than once. Collapse
997
+ * those first so repeats can't masquerade as independent confirmations and
998
+ * inflate consensus scores.
999
+ */
1000
+ function dedupeWithinReview(review, reviewIndex, jaccardThreshold, lineWindow, collectContributions) {
1001
+ const tagged = review.findings.map((finding, findingIndex) => ({
1002
+ finding,
1003
+ model: review.model,
1004
+ role: review.role,
1005
+ ...(collectContributions ? { contributions: [{ reviewIndex, findingIndex }] } : {}),
1006
+ }));
1007
+ return groupTagged(tagged, jaccardThreshold, lineWindow).map((group) => chooseCollapsedRepresentative(group, collectContributions));
1008
+ }
1009
+ export function deduplicateFindings(reviews, jaccardThreshold = DEFAULT_THRESHOLDS.jaccardThreshold, lineWindow = DEFAULT_THRESHOLDS.dedupeLineWindow, minConsensusScore = DEFAULT_THRESHOLDS.minConsensusScore, collectContributions = false, ordering = 'legacy') {
1010
+ if (ordering !== 'legacy' && ordering !== 'utf16')
1011
+ throw new Error('deduper_invalid_ordering');
1012
+ const compareText = ordering === 'utf16'
1013
+ ? (left, right) => left < right ? -1 : left > right ? 1 : 0
1014
+ : (left, right) => left.localeCompare(right);
1015
+ // Flatten all findings with attribution, deduplicating within each review first
1016
+ const all = [];
1017
+ for (const [reviewIndex, review] of reviews.entries()) {
1018
+ if (review.status !== 'success')
1019
+ continue;
1020
+ all.push(...dedupeWithinReview(review, reviewIndex, jaccardThreshold, lineWindow, collectContributions));
1021
+ }
1022
+ if (all.length === 0)
1023
+ return [];
1024
+ const strictGroups = [];
1025
+ for (const members of groupTagged(all, jaccardThreshold, lineWindow)) {
1026
+ for (const coherent of splitIncoherent(members, jaccardThreshold, lineWindow)) {
1027
+ strictGroups.push(collapseSameReviewer(coherent, collectContributions));
1028
+ }
1029
+ }
1030
+ const corroborated = mergeCorroboratedLocationClusters(strictGroups, lineWindow, compareText);
1031
+ const successfulReviews = reviews.filter((review) => review.status === 'success').length;
1032
+ const minimumReviewers = Math.max(CORROBORATED_MIN_REVIEWERS, Math.ceil(successfulReviews * minConsensusScore));
1033
+ const result = mergeAgreementNeighborhoods(corroborated, lineWindow, minimumReviewers).map((members) => {
1034
+ const collapsed = collapseSameReviewer(members, collectContributions);
1035
+ return {
1036
+ representative: chooseRepresentative(collapsed).finding,
1037
+ members: collapsed.map(({ finding, model, role }) => ({ finding, model, role })),
1038
+ ...(collectContributions ? { contributions: collectMemberContributions(collapsed) } : {}),
1039
+ };
1040
+ });
1041
+ // Sort by severity then file
1042
+ const severityOrder = { critical: 0, important: 1, minor: 2, nitpick: 3 };
1043
+ result.sort((a, b) => {
1044
+ const sevDiff = severityOrder[a.representative.severity] -
1045
+ severityOrder[b.representative.severity];
1046
+ if (sevDiff !== 0)
1047
+ return sevDiff;
1048
+ return compareText(a.representative.file, b.representative.file);
1049
+ });
1050
+ return result;
1051
+ }
1052
+ //# sourceMappingURL=deduper.js.map