@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.
- package/LICENSE +21 -0
- package/README.md +1415 -0
- package/dist/ci.d.ts +16 -0
- package/dist/ci.js +42 -0
- package/dist/ci.js.map +1 -0
- package/dist/config/data-dir.d.ts +6 -0
- package/dist/config/data-dir.js +12 -0
- package/dist/config/data-dir.js.map +1 -0
- package/dist/config/defaults.d.ts +82 -0
- package/dist/config/defaults.js +89 -0
- package/dist/config/defaults.js.map +1 -0
- package/dist/config/harness.d.ts +40 -0
- package/dist/config/harness.js +151 -0
- package/dist/config/harness.js.map +1 -0
- package/dist/config/loader.d.ts +13 -0
- package/dist/config/loader.js +131 -0
- package/dist/config/loader.js.map +1 -0
- package/dist/config/provider-concurrency.d.ts +4 -0
- package/dist/config/provider-concurrency.js +6 -0
- package/dist/config/provider-concurrency.js.map +1 -0
- package/dist/config/providers.d.ts +3 -0
- package/dist/config/providers.js +9 -0
- package/dist/config/providers.js.map +1 -0
- package/dist/config/schema.d.ts +179 -0
- package/dist/config/schema.js +115 -0
- package/dist/config/schema.js.map +1 -0
- package/dist/consensus/claim-contract.d.ts +9 -0
- package/dist/consensus/claim-contract.js +307 -0
- package/dist/consensus/claim-contract.js.map +1 -0
- package/dist/consensus/claim-identity.d.ts +5 -0
- package/dist/consensus/claim-identity.js +39 -0
- package/dist/consensus/claim-identity.js.map +1 -0
- package/dist/consensus/deduper.d.ts +43 -0
- package/dist/consensus/deduper.js +1052 -0
- package/dist/consensus/deduper.js.map +1 -0
- package/dist/consensus/finding-identity.d.ts +20 -0
- package/dist/consensus/finding-identity.js +49 -0
- package/dist/consensus/finding-identity.js.map +1 -0
- package/dist/consensus/gating.d.ts +215 -0
- package/dist/consensus/gating.js +699 -0
- package/dist/consensus/gating.js.map +1 -0
- package/dist/consensus/parser.d.ts +22 -0
- package/dist/consensus/parser.js +255 -0
- package/dist/consensus/parser.js.map +1 -0
- package/dist/consensus/semantic-deduper.d.ts +10 -0
- package/dist/consensus/semantic-deduper.js +70 -0
- package/dist/consensus/semantic-deduper.js.map +1 -0
- package/dist/consensus/types.d.ts +196 -0
- package/dist/consensus/types.js +2 -0
- package/dist/consensus/types.js.map +1 -0
- package/dist/consensus/verification-contract.d.ts +8 -0
- package/dist/consensus/verification-contract.js +95 -0
- package/dist/consensus/verification-contract.js.map +1 -0
- package/dist/consensus/voter.d.ts +29 -0
- package/dist/consensus/voter.js +394 -0
- package/dist/consensus/voter.js.map +1 -0
- package/dist/converge/attempt-budget.d.ts +88 -0
- package/dist/converge/attempt-budget.js +717 -0
- package/dist/converge/attempt-budget.js.map +1 -0
- package/dist/converge/bound-fix-recovery-source.d.ts +24 -0
- package/dist/converge/bound-fix-recovery-source.js +25 -0
- package/dist/converge/bound-fix-recovery-source.js.map +1 -0
- package/dist/converge/bound-fix-recovery.d.ts +15 -0
- package/dist/converge/bound-fix-recovery.js +65 -0
- package/dist/converge/bound-fix-recovery.js.map +1 -0
- package/dist/converge/correction-anchors.d.ts +1 -0
- package/dist/converge/correction-anchors.js +2 -0
- package/dist/converge/correction-anchors.js.map +1 -0
- package/dist/converge/cycle-remote.d.ts +6 -0
- package/dist/converge/cycle-remote.js +41 -0
- package/dist/converge/cycle-remote.js.map +1 -0
- package/dist/converge/delivery-reconciliation.d.ts +17 -0
- package/dist/converge/delivery-reconciliation.js +53 -0
- package/dist/converge/delivery-reconciliation.js.map +1 -0
- package/dist/converge/finding-identity.d.ts +6 -0
- package/dist/converge/finding-identity.js +7 -0
- package/dist/converge/finding-identity.js.map +1 -0
- package/dist/converge/fresh-review.d.ts +37 -0
- package/dist/converge/fresh-review.js +351 -0
- package/dist/converge/fresh-review.js.map +1 -0
- package/dist/converge/launch-guard.d.ts +55 -0
- package/dist/converge/launch-guard.js +363 -0
- package/dist/converge/launch-guard.js.map +1 -0
- package/dist/converge/launch-preflight.d.ts +5 -0
- package/dist/converge/launch-preflight.js +41 -0
- package/dist/converge/launch-preflight.js.map +1 -0
- package/dist/converge/launch-record.d.ts +136 -0
- package/dist/converge/launch-record.js +86 -0
- package/dist/converge/launch-record.js.map +1 -0
- package/dist/converge/legacy-launch-health.d.ts +311 -0
- package/dist/converge/legacy-launch-health.js +159 -0
- package/dist/converge/legacy-launch-health.js.map +1 -0
- package/dist/converge/legacy-roster.d.ts +29 -0
- package/dist/converge/legacy-roster.js +268 -0
- package/dist/converge/legacy-roster.js.map +1 -0
- package/dist/converge/native-lock.d.ts +18 -0
- package/dist/converge/native-lock.js +122 -0
- package/dist/converge/native-lock.js.map +1 -0
- package/dist/converge/ordinary-pending-export.d.ts +67 -0
- package/dist/converge/ordinary-pending-export.js +175 -0
- package/dist/converge/ordinary-pending-export.js.map +1 -0
- package/dist/converge/ordinary-pending-package.d.ts +28 -0
- package/dist/converge/ordinary-pending-package.js +36 -0
- package/dist/converge/ordinary-pending-package.js.map +1 -0
- package/dist/converge/pending-legacy-resume.d.ts +119 -0
- package/dist/converge/pending-legacy-resume.js +720 -0
- package/dist/converge/pending-legacy-resume.js.map +1 -0
- package/dist/converge/pending-recovery-source.d.ts +131 -0
- package/dist/converge/pending-recovery-source.js +71 -0
- package/dist/converge/pending-recovery-source.js.map +1 -0
- package/dist/converge/recovery-launch.d.ts +66 -0
- package/dist/converge/recovery-launch.js +518 -0
- package/dist/converge/recovery-launch.js.map +1 -0
- package/dist/converge/retained-report.d.ts +29 -0
- package/dist/converge/retained-report.js +218 -0
- package/dist/converge/retained-report.js.map +1 -0
- package/dist/converge/retry-source.d.ts +25 -0
- package/dist/converge/retry-source.js +13 -0
- package/dist/converge/retry-source.js.map +1 -0
- package/dist/converge/review-cycle.d.ts +49 -0
- package/dist/converge/review-cycle.js +26 -0
- package/dist/converge/review-cycle.js.map +1 -0
- package/dist/converge/round-gap-schema.d.ts +66 -0
- package/dist/converge/round-gap-schema.js +51 -0
- package/dist/converge/round-gap-schema.js.map +1 -0
- package/dist/converge/round-gap.d.ts +38 -0
- package/dist/converge/round-gap.js +334 -0
- package/dist/converge/round-gap.js.map +1 -0
- package/dist/converge/run-state.d.ts +224 -0
- package/dist/converge/run-state.js +564 -0
- package/dist/converge/run-state.js.map +1 -0
- package/dist/converge/stale-report-schema.d.ts +42 -0
- package/dist/converge/stale-report-schema.js +79 -0
- package/dist/converge/stale-report-schema.js.map +1 -0
- package/dist/converge/stale-report-storage.d.ts +47 -0
- package/dist/converge/stale-report-storage.js +172 -0
- package/dist/converge/stale-report-storage.js.map +1 -0
- package/dist/converge/stale-report.d.ts +14 -0
- package/dist/converge/stale-report.js +213 -0
- package/dist/converge/stale-report.js.map +1 -0
- package/dist/converge/target-ownership.d.ts +29 -0
- package/dist/converge/target-ownership.js +95 -0
- package/dist/converge/target-ownership.js.map +1 -0
- package/dist/converge/terminal-rejection-schema.d.ts +42 -0
- package/dist/converge/terminal-rejection-schema.js +39 -0
- package/dist/converge/terminal-rejection-schema.js.map +1 -0
- package/dist/converge/terminal-rejection.d.ts +13 -0
- package/dist/converge/terminal-rejection.js +275 -0
- package/dist/converge/terminal-rejection.js.map +1 -0
- package/dist/coordination/registry-lock.d.ts +82 -0
- package/dist/coordination/registry-lock.js +498 -0
- package/dist/coordination/registry-lock.js.map +1 -0
- package/dist/discuss.d.ts +53 -0
- package/dist/discuss.js +112 -0
- package/dist/discuss.js.map +1 -0
- package/dist/dispatch/adapter.d.ts +32 -0
- package/dist/dispatch/adapter.js +2 -0
- package/dist/dispatch/adapter.js.map +1 -0
- package/dist/dispatch/anthropic.d.ts +10 -0
- package/dist/dispatch/anthropic.js +235 -0
- package/dist/dispatch/anthropic.js.map +1 -0
- package/dist/dispatch/async-lane.d.ts +127 -0
- package/dist/dispatch/async-lane.js +414 -0
- package/dist/dispatch/async-lane.js.map +1 -0
- package/dist/dispatch/capture-council.d.ts +63 -0
- package/dist/dispatch/capture-council.js +137 -0
- package/dist/dispatch/capture-council.js.map +1 -0
- package/dist/dispatch/captured-async.d.ts +67 -0
- package/dist/dispatch/captured-async.js +40 -0
- package/dist/dispatch/captured-async.js.map +1 -0
- package/dist/dispatch/captured-inputs.d.ts +50 -0
- package/dist/dispatch/captured-inputs.js +190 -0
- package/dist/dispatch/captured-inputs.js.map +1 -0
- package/dist/dispatch/checkpoint-async-context.d.ts +27 -0
- package/dist/dispatch/checkpoint-async-context.js +51 -0
- package/dist/dispatch/checkpoint-async-context.js.map +1 -0
- package/dist/dispatch/checkpoint-async-execution.d.ts +22 -0
- package/dist/dispatch/checkpoint-async-execution.js +130 -0
- package/dist/dispatch/checkpoint-async-execution.js.map +1 -0
- package/dist/dispatch/checkpoint-async-store.d.ts +134 -0
- package/dist/dispatch/checkpoint-async-store.js +534 -0
- package/dist/dispatch/checkpoint-async-store.js.map +1 -0
- package/dist/dispatch/checkpoint-async-unknown.d.ts +19 -0
- package/dist/dispatch/checkpoint-async-unknown.js +46 -0
- package/dist/dispatch/checkpoint-async-unknown.js.map +1 -0
- package/dist/dispatch/checkpoint-async.d.ts +252 -0
- package/dist/dispatch/checkpoint-async.js +270 -0
- package/dist/dispatch/checkpoint-async.js.map +1 -0
- package/dist/dispatch/checkpoint-gating-execution.d.ts +26 -0
- package/dist/dispatch/checkpoint-gating-execution.js +84 -0
- package/dist/dispatch/checkpoint-gating-execution.js.map +1 -0
- package/dist/dispatch/checkpoint-phase-limits.d.ts +5 -0
- package/dist/dispatch/checkpoint-phase-limits.js +11 -0
- package/dist/dispatch/checkpoint-phase-limits.js.map +1 -0
- package/dist/dispatch/checkpoint-verification-context.d.ts +15 -0
- package/dist/dispatch/checkpoint-verification-context.js +48 -0
- package/dist/dispatch/checkpoint-verification-context.js.map +1 -0
- package/dist/dispatch/checkpoint-verification.d.ts +213 -0
- package/dist/dispatch/checkpoint-verification.js +381 -0
- package/dist/dispatch/checkpoint-verification.js.map +1 -0
- package/dist/dispatch/checkpoint.d.ts +329 -0
- package/dist/dispatch/checkpoint.js +1418 -0
- package/dist/dispatch/checkpoint.js.map +1 -0
- package/dist/dispatch/google.d.ts +10 -0
- package/dist/dispatch/google.js +158 -0
- package/dist/dispatch/google.js.map +1 -0
- package/dist/dispatch/late-audit.d.ts +23 -0
- package/dist/dispatch/late-audit.js +133 -0
- package/dist/dispatch/late-audit.js.map +1 -0
- package/dist/dispatch/merge.d.ts +29 -0
- package/dist/dispatch/merge.js +112 -0
- package/dist/dispatch/merge.js.map +1 -0
- package/dist/dispatch/openai-compat.d.ts +30 -0
- package/dist/dispatch/openai-compat.js +166 -0
- package/dist/dispatch/openai-compat.js.map +1 -0
- package/dist/dispatch/openai.d.ts +10 -0
- package/dist/dispatch/openai.js +159 -0
- package/dist/dispatch/openai.js.map +1 -0
- package/dist/dispatch/original-execution.d.ts +30 -0
- package/dist/dispatch/original-execution.js +91 -0
- package/dist/dispatch/original-execution.js.map +1 -0
- package/dist/dispatch/original-launch.d.ts +37 -0
- package/dist/dispatch/original-launch.js +117 -0
- package/dist/dispatch/original-launch.js.map +1 -0
- package/dist/dispatch/quorum.d.ts +15 -0
- package/dist/dispatch/quorum.js +33 -0
- package/dist/dispatch/quorum.js.map +1 -0
- package/dist/dispatch/recovery-operation.d.ts +44 -0
- package/dist/dispatch/recovery-operation.js +146 -0
- package/dist/dispatch/recovery-operation.js.map +1 -0
- package/dist/dispatch/recovery-policy.d.ts +54 -0
- package/dist/dispatch/recovery-policy.js +157 -0
- package/dist/dispatch/recovery-policy.js.map +1 -0
- package/dist/dispatch/recovery.d.ts +78 -0
- package/dist/dispatch/recovery.js +299 -0
- package/dist/dispatch/recovery.js.map +1 -0
- package/dist/dispatch/retained-async.d.ts +13 -0
- package/dist/dispatch/retained-async.js +101 -0
- package/dist/dispatch/retained-async.js.map +1 -0
- package/dist/dispatch/reviewer-identity.d.ts +10 -0
- package/dist/dispatch/reviewer-identity.js +19 -0
- package/dist/dispatch/reviewer-identity.js.map +1 -0
- package/dist/dispatch/runner.d.ts +76 -0
- package/dist/dispatch/runner.js +415 -0
- package/dist/dispatch/runner.js.map +1 -0
- package/dist/dispatch/utils.d.ts +159 -0
- package/dist/dispatch/utils.js +381 -0
- package/dist/dispatch/utils.js.map +1 -0
- package/dist/dispatch/verification-execution.d.ts +48 -0
- package/dist/dispatch/verification-execution.js +252 -0
- package/dist/dispatch/verification-execution.js.map +1 -0
- package/dist/dispatch/verification-late-audit.d.ts +17 -0
- package/dist/dispatch/verification-late-audit.js +117 -0
- package/dist/dispatch/verification-late-audit.js.map +1 -0
- package/dist/evidence/claim-recovery/authenticated-carrier-projection.d.ts +30 -0
- package/dist/evidence/claim-recovery/authenticated-carrier-projection.js +100 -0
- package/dist/evidence/claim-recovery/authenticated-carrier-projection.js.map +1 -0
- package/dist/evidence/claim-recovery/authenticated-receipts.d.ts +46 -0
- package/dist/evidence/claim-recovery/authenticated-receipts.js +120 -0
- package/dist/evidence/claim-recovery/authenticated-receipts.js.map +1 -0
- package/dist/evidence/claim-recovery/carrier-inventory.d.ts +49 -0
- package/dist/evidence/claim-recovery/carrier-inventory.js +340 -0
- package/dist/evidence/claim-recovery/carrier-inventory.js.map +1 -0
- package/dist/evidence/claim-recovery/claim-index.d.ts +12 -0
- package/dist/evidence/claim-recovery/claim-index.js +56 -0
- package/dist/evidence/claim-recovery/claim-index.js.map +1 -0
- package/dist/evidence/claim-recovery/confirmation.d.ts +53 -0
- package/dist/evidence/claim-recovery/confirmation.js +151 -0
- package/dist/evidence/claim-recovery/confirmation.js.map +1 -0
- package/dist/evidence/claim-recovery/context.d.ts +15 -0
- package/dist/evidence/claim-recovery/context.js +40 -0
- package/dist/evidence/claim-recovery/context.js.map +1 -0
- package/dist/evidence/claim-recovery/proof-storage.d.ts +6 -0
- package/dist/evidence/claim-recovery/proof-storage.js +97 -0
- package/dist/evidence/claim-recovery/proof-storage.js.map +1 -0
- package/dist/evidence/claim-recovery/validation/anchors.d.ts +20 -0
- package/dist/evidence/claim-recovery/validation/anchors.js +22 -0
- package/dist/evidence/claim-recovery/validation/anchors.js.map +1 -0
- package/dist/evidence/claim-recovery/validation/carrier-projection.d.ts +9 -0
- package/dist/evidence/claim-recovery/validation/carrier-projection.js +364 -0
- package/dist/evidence/claim-recovery/validation/carrier-projection.js.map +1 -0
- package/dist/evidence/claim-recovery/validation/carrier-types.d.ts +83 -0
- package/dist/evidence/claim-recovery/validation/carrier-types.js +2 -0
- package/dist/evidence/claim-recovery/validation/carrier-types.js.map +1 -0
- package/dist/evidence/claim-recovery/validation/claim-split.d.ts +57 -0
- package/dist/evidence/claim-recovery/validation/claim-split.js +209 -0
- package/dist/evidence/claim-recovery/validation/claim-split.js.map +1 -0
- package/dist/evidence/claim-recovery/validation/claims.d.ts +20 -0
- package/dist/evidence/claim-recovery/validation/claims.js +108 -0
- package/dist/evidence/claim-recovery/validation/claims.js.map +1 -0
- package/dist/evidence/claim-recovery/validation/current-projection.d.ts +36 -0
- package/dist/evidence/claim-recovery/validation/current-projection.js +424 -0
- package/dist/evidence/claim-recovery/validation/current-projection.js.map +1 -0
- package/dist/evidence/claim-recovery/validation/lexical.d.ts +18 -0
- package/dist/evidence/claim-recovery/validation/lexical.js +91 -0
- package/dist/evidence/claim-recovery/validation/lexical.js.map +1 -0
- package/dist/evidence/claim-recovery/validation/materials.d.ts +19 -0
- package/dist/evidence/claim-recovery/validation/materials.js +160 -0
- package/dist/evidence/claim-recovery/validation/materials.js.map +1 -0
- package/dist/evidence/claim-recovery/validation/native-material.d.ts +27 -0
- package/dist/evidence/claim-recovery/validation/native-material.js +35 -0
- package/dist/evidence/claim-recovery/validation/native-material.js.map +1 -0
- package/dist/evidence/claim-recovery/validation/native-occurrences.d.ts +79 -0
- package/dist/evidence/claim-recovery/validation/native-occurrences.js +187 -0
- package/dist/evidence/claim-recovery/validation/native-occurrences.js.map +1 -0
- package/dist/evidence/claim-recovery/validation/native-state.d.ts +45 -0
- package/dist/evidence/claim-recovery/validation/native-state.js +385 -0
- package/dist/evidence/claim-recovery/validation/native-state.js.map +1 -0
- package/dist/evidence/claim-recovery/validation/obligations.d.ts +15 -0
- package/dist/evidence/claim-recovery/validation/obligations.js +65 -0
- package/dist/evidence/claim-recovery/validation/obligations.js.map +1 -0
- package/dist/evidence/claim-recovery/validation/occurrence-source.d.ts +45 -0
- package/dist/evidence/claim-recovery/validation/occurrence-source.js +208 -0
- package/dist/evidence/claim-recovery/validation/occurrence-source.js.map +1 -0
- package/dist/evidence/claim-recovery/validation/occurrence-types.d.ts +101 -0
- package/dist/evidence/claim-recovery/validation/occurrence-types.js +2 -0
- package/dist/evidence/claim-recovery/validation/occurrence-types.js.map +1 -0
- package/dist/evidence/claim-recovery/validation/occurrence.d.ts +30 -0
- package/dist/evidence/claim-recovery/validation/occurrence.js +227 -0
- package/dist/evidence/claim-recovery/validation/occurrence.js.map +1 -0
- package/dist/evidence/claim-recovery/validation/primitives.d.ts +11 -0
- package/dist/evidence/claim-recovery/validation/primitives.js +41 -0
- package/dist/evidence/claim-recovery/validation/primitives.js.map +1 -0
- package/dist/evidence/claim-recovery/validation/receipts.d.ts +37 -0
- package/dist/evidence/claim-recovery/validation/receipts.js +83 -0
- package/dist/evidence/claim-recovery/validation/receipts.js.map +1 -0
- package/dist/evidence/claim-recovery/validation/recovery-json.d.ts +8 -0
- package/dist/evidence/claim-recovery/validation/recovery-json.js +42 -0
- package/dist/evidence/claim-recovery/validation/recovery-json.js.map +1 -0
- package/dist/evidence/claim-recovery/validation/semantic-cache.d.ts +3 -0
- package/dist/evidence/claim-recovery/validation/semantic-cache.js +22 -0
- package/dist/evidence/claim-recovery/validation/semantic-cache.js.map +1 -0
- package/dist/evidence/claim-recovery/validation/semantic-validation.d.ts +9 -0
- package/dist/evidence/claim-recovery/validation/semantic-validation.js +347 -0
- package/dist/evidence/claim-recovery/validation/semantic-validation.js.map +1 -0
- package/dist/evidence/claim-recovery/validation/sighting.d.ts +2 -0
- package/dist/evidence/claim-recovery/validation/sighting.js +15 -0
- package/dist/evidence/claim-recovery/validation/sighting.js.map +1 -0
- package/dist/evidence/claim-recovery/validation/sources.d.ts +15 -0
- package/dist/evidence/claim-recovery/validation/sources.js +2 -0
- package/dist/evidence/claim-recovery/validation/sources.js.map +1 -0
- package/dist/evidence/claim-recovery/validation/types.d.ts +126 -0
- package/dist/evidence/claim-recovery/validation/types.js +2 -0
- package/dist/evidence/claim-recovery/validation/types.js.map +1 -0
- package/dist/evidence/claim-split.d.ts +1 -0
- package/dist/evidence/claim-split.js +2 -0
- package/dist/evidence/claim-split.js.map +1 -0
- package/dist/evidence/event-receipts.d.ts +16 -0
- package/dist/evidence/event-receipts.js +41 -0
- package/dist/evidence/event-receipts.js.map +1 -0
- package/dist/evidence/finding-recovery.d.ts +23 -0
- package/dist/evidence/finding-recovery.js +74 -0
- package/dist/evidence/finding-recovery.js.map +1 -0
- package/dist/evidence/finding-retriage.d.ts +14 -0
- package/dist/evidence/finding-retriage.js +57 -0
- package/dist/evidence/finding-retriage.js.map +1 -0
- package/dist/evidence/format.d.ts +18 -0
- package/dist/evidence/format.js +152 -0
- package/dist/evidence/format.js.map +1 -0
- package/dist/evidence/original-run/decode.d.ts +30 -0
- package/dist/evidence/original-run/decode.js +208 -0
- package/dist/evidence/original-run/decode.js.map +1 -0
- package/dist/evidence/original-run/journal.d.ts +31 -0
- package/dist/evidence/original-run/journal.js +157 -0
- package/dist/evidence/original-run/journal.js.map +1 -0
- package/dist/evidence/original-run/lock-path.d.ts +17 -0
- package/dist/evidence/original-run/lock-path.js +116 -0
- package/dist/evidence/original-run/lock-path.js.map +1 -0
- package/dist/evidence/original-run/lock-scope.d.ts +21 -0
- package/dist/evidence/original-run/lock-scope.js +61 -0
- package/dist/evidence/original-run/lock-scope.js.map +1 -0
- package/dist/evidence/original-run/lock.d.ts +33 -0
- package/dist/evidence/original-run/lock.js +259 -0
- package/dist/evidence/original-run/lock.js.map +1 -0
- package/dist/evidence/original-run/remote.d.ts +23 -0
- package/dist/evidence/original-run/remote.js +110 -0
- package/dist/evidence/original-run/remote.js.map +1 -0
- package/dist/evidence/original-run/source.d.ts +61 -0
- package/dist/evidence/original-run/source.js +267 -0
- package/dist/evidence/original-run/source.js.map +1 -0
- package/dist/evidence/reads.d.ts +11 -0
- package/dist/evidence/reads.js +16 -0
- package/dist/evidence/reads.js.map +1 -0
- package/dist/evidence/recover-finding.d.ts +17 -0
- package/dist/evidence/recover-finding.js +65 -0
- package/dist/evidence/recover-finding.js.map +1 -0
- package/dist/evidence/recover-run.d.ts +26 -0
- package/dist/evidence/recover-run.js +180 -0
- package/dist/evidence/recover-run.js.map +1 -0
- package/dist/evidence/retained-time-budget.d.ts +9 -0
- package/dist/evidence/retained-time-budget.js +14 -0
- package/dist/evidence/retained-time-budget.js.map +1 -0
- package/dist/evidence/retriage-finding.d.ts +12 -0
- package/dist/evidence/retriage-finding.js +67 -0
- package/dist/evidence/retriage-finding.js.map +1 -0
- package/dist/evidence/reviewer-lineage.d.ts +35 -0
- package/dist/evidence/reviewer-lineage.js +293 -0
- package/dist/evidence/reviewer-lineage.js.map +1 -0
- package/dist/evidence/reviewer-original.d.ts +67 -0
- package/dist/evidence/reviewer-original.js +148 -0
- package/dist/evidence/reviewer-original.js.map +1 -0
- package/dist/evidence/reviewer-recovery.d.ts +74 -0
- package/dist/evidence/reviewer-recovery.js +179 -0
- package/dist/evidence/reviewer-recovery.js.map +1 -0
- package/dist/evidence/reviewer-status.d.ts +145 -0
- package/dist/evidence/reviewer-status.js +302 -0
- package/dist/evidence/reviewer-status.js.map +1 -0
- package/dist/evidence/show.d.ts +10 -0
- package/dist/evidence/show.js +41 -0
- package/dist/evidence/show.js.map +1 -0
- package/dist/evidence/status.d.ts +38 -0
- package/dist/evidence/status.js +73 -0
- package/dist/evidence/status.js.map +1 -0
- package/dist/evidence/target.d.ts +17 -0
- package/dist/evidence/target.js +74 -0
- package/dist/evidence/target.js.map +1 -0
- package/dist/evidence/types.d.ts +176 -0
- package/dist/evidence/types.js +109 -0
- package/dist/evidence/types.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2688 -0
- package/dist/index.js.map +1 -0
- package/dist/models/seed.d.ts +44 -0
- package/dist/models/seed.js +175 -0
- package/dist/models/seed.js.map +1 -0
- package/dist/models/server-stats.d.ts +80 -0
- package/dist/models/server-stats.js +109 -0
- package/dist/models/server-stats.js.map +1 -0
- package/dist/models/stats-store.d.ts +84 -0
- package/dist/models/stats-store.js +572 -0
- package/dist/models/stats-store.js.map +1 -0
- package/dist/output/artifacts.d.ts +16 -0
- package/dist/output/artifacts.js +29 -0
- package/dist/output/artifacts.js.map +1 -0
- package/dist/output/assembly-refusal.d.ts +8 -0
- package/dist/output/assembly-refusal.js +14 -0
- package/dist/output/assembly-refusal.js.map +1 -0
- package/dist/output/github.d.ts +12 -0
- package/dist/output/github.js +214 -0
- package/dist/output/github.js.map +1 -0
- package/dist/output/json.d.ts +3 -0
- package/dist/output/json.js +8 -0
- package/dist/output/json.js.map +1 -0
- package/dist/output/markdown.d.ts +3 -0
- package/dist/output/markdown.js +281 -0
- package/dist/output/markdown.js.map +1 -0
- package/dist/output/progress.d.ts +55 -0
- package/dist/output/progress.js +180 -0
- package/dist/output/progress.js.map +1 -0
- package/dist/output/sanitize.d.ts +18 -0
- package/dist/output/sanitize.js +46 -0
- package/dist/output/sanitize.js.map +1 -0
- package/dist/output/terminal.d.ts +2 -0
- package/dist/output/terminal.js +172 -0
- package/dist/output/terminal.js.map +1 -0
- package/dist/prepare/chunker.d.ts +18 -0
- package/dist/prepare/chunker.js +325 -0
- package/dist/prepare/chunker.js.map +1 -0
- package/dist/prepare/language.d.ts +1 -0
- package/dist/prepare/language.js +99 -0
- package/dist/prepare/language.js.map +1 -0
- package/dist/prepare/prompt-builder.d.ts +44 -0
- package/dist/prepare/prompt-builder.js +102 -0
- package/dist/prepare/prompt-builder.js.map +1 -0
- package/dist/prepare/unified-diff.d.ts +40 -0
- package/dist/prepare/unified-diff.js +135 -0
- package/dist/prepare/unified-diff.js.map +1 -0
- package/dist/prompts/base.d.ts +3 -0
- package/dist/prompts/base.js +53 -0
- package/dist/prompts/base.js.map +1 -0
- package/dist/prompts/hardening.d.ts +19 -0
- package/dist/prompts/hardening.js +67 -0
- package/dist/prompts/hardening.js.map +1 -0
- package/dist/prompts/languages/elixir.d.ts +1 -0
- package/dist/prompts/languages/elixir.js +13 -0
- package/dist/prompts/languages/elixir.js.map +1 -0
- package/dist/prompts/languages/generic.d.ts +1 -0
- package/dist/prompts/languages/generic.js +11 -0
- package/dist/prompts/languages/generic.js.map +1 -0
- package/dist/prompts/languages/python.d.ts +1 -0
- package/dist/prompts/languages/python.js +14 -0
- package/dist/prompts/languages/python.js.map +1 -0
- package/dist/prompts/languages/typescript.d.ts +1 -0
- package/dist/prompts/languages/typescript.js +13 -0
- package/dist/prompts/languages/typescript.js.map +1 -0
- package/dist/prompts/plan.d.ts +15 -0
- package/dist/prompts/plan.js +55 -0
- package/dist/prompts/plan.js.map +1 -0
- package/dist/report/aggregation-inputs.d.ts +52 -0
- package/dist/report/aggregation-inputs.js +114 -0
- package/dist/report/aggregation-inputs.js.map +1 -0
- package/dist/report/assembly.d.ts +43 -0
- package/dist/report/assembly.js +133 -0
- package/dist/report/assembly.js.map +1 -0
- package/dist/report/blocking-health.d.ts +64 -0
- package/dist/report/blocking-health.js +156 -0
- package/dist/report/blocking-health.js.map +1 -0
- package/dist/report/checkpoint-assembly.d.ts +10 -0
- package/dist/report/checkpoint-assembly.js +17 -0
- package/dist/report/checkpoint-assembly.js.map +1 -0
- package/dist/report/checkpoint-consensus.d.ts +59 -0
- package/dist/report/checkpoint-consensus.js +197 -0
- package/dist/report/checkpoint-consensus.js.map +1 -0
- package/dist/report/checkpoint-gating.d.ts +36 -0
- package/dist/report/checkpoint-gating.js +110 -0
- package/dist/report/checkpoint-gating.js.map +1 -0
- package/dist/report/checkpoint-projection.d.ts +76 -0
- package/dist/report/checkpoint-projection.js +122 -0
- package/dist/report/checkpoint-projection.js.map +1 -0
- package/dist/report/consensus-assembly.d.ts +37 -0
- package/dist/report/consensus-assembly.js +33 -0
- package/dist/report/consensus-assembly.js.map +1 -0
- package/dist/report/recovery-source.d.ts +7 -0
- package/dist/report/recovery-source.js +7 -0
- package/dist/report/recovery-source.js.map +1 -0
- package/dist/report/reviewer-artifact.d.ts +118 -0
- package/dist/report/reviewer-artifact.js +381 -0
- package/dist/report/reviewer-artifact.js.map +1 -0
- package/dist/report/reviewer-evidence-schema.d.ts +42 -0
- package/dist/report/reviewer-evidence-schema.js +28 -0
- package/dist/report/reviewer-evidence-schema.js.map +1 -0
- package/dist/report/reviewer-evidence.d.ts +54 -0
- package/dist/report/reviewer-evidence.js +229 -0
- package/dist/report/reviewer-evidence.js.map +1 -0
- package/dist/report/reviewer-health.d.ts +62 -0
- package/dist/report/reviewer-health.js +105 -0
- package/dist/report/reviewer-health.js.map +1 -0
- package/dist/report/run-header.d.ts +219 -0
- package/dist/report/run-header.js +307 -0
- package/dist/report/run-header.js.map +1 -0
- package/dist/report/supplemental-async.d.ts +19 -0
- package/dist/report/supplemental-async.js +85 -0
- package/dist/report/supplemental-async.js.map +1 -0
- package/dist/report/uuid.d.ts +16 -0
- package/dist/report/uuid.js +41 -0
- package/dist/report/uuid.js.map +1 -0
- package/dist/resolver/git.d.ts +16 -0
- package/dist/resolver/git.js +77 -0
- package/dist/resolver/git.js.map +1 -0
- package/dist/resolver/github-client.d.ts +82 -0
- package/dist/resolver/github-client.js +27 -0
- package/dist/resolver/github-client.js.map +1 -0
- package/dist/resolver/github.d.ts +29 -0
- package/dist/resolver/github.js +166 -0
- package/dist/resolver/github.js.map +1 -0
- package/dist/resolver/local.d.ts +3 -0
- package/dist/resolver/local.js +75 -0
- package/dist/resolver/local.js.map +1 -0
- package/dist/resolver/plan.d.ts +8 -0
- package/dist/resolver/plan.js +44 -0
- package/dist/resolver/plan.js.map +1 -0
- package/dist/resolver/target.d.ts +35 -0
- package/dist/resolver/target.js +112 -0
- package/dist/resolver/target.js.map +1 -0
- package/dist/resolver/types.d.ts +43 -0
- package/dist/resolver/types.js +2 -0
- package/dist/resolver/types.js.map +1 -0
- package/dist/roles/builtin.d.ts +4 -0
- package/dist/roles/builtin.js +282 -0
- package/dist/roles/builtin.js.map +1 -0
- package/dist/roles/dispatcher.d.ts +26 -0
- package/dist/roles/dispatcher.js +96 -0
- package/dist/roles/dispatcher.js.map +1 -0
- package/dist/roles/legacy-4.1.10.d.ts +2 -0
- package/dist/roles/legacy-4.1.10.js +320 -0
- package/dist/roles/legacy-4.1.10.js.map +1 -0
- package/dist/roles/legacy-4.4.9.d.ts +4 -0
- package/dist/roles/legacy-4.4.9.js +282 -0
- package/dist/roles/legacy-4.4.9.js.map +1 -0
- package/dist/roles/loader.d.ts +10 -0
- package/dist/roles/loader.js +104 -0
- package/dist/roles/loader.js.map +1 -0
- package/dist/roles/types.d.ts +13 -0
- package/dist/roles/types.js +2 -0
- package/dist/roles/types.js.map +1 -0
- package/dist/telemetry/abort-signal.d.ts +13 -0
- package/dist/telemetry/abort-signal.js +40 -0
- package/dist/telemetry/abort-signal.js.map +1 -0
- package/dist/telemetry/attest.d.ts +98 -0
- package/dist/telemetry/attest.js +331 -0
- package/dist/telemetry/attest.js.map +1 -0
- package/dist/telemetry/attested-retry.d.ts +79 -0
- package/dist/telemetry/attested-retry.js +195 -0
- package/dist/telemetry/attested-retry.js.map +1 -0
- package/dist/telemetry/attested-reviewer-delivery.d.ts +58 -0
- package/dist/telemetry/attested-reviewer-delivery.js +238 -0
- package/dist/telemetry/attested-reviewer-delivery.js.map +1 -0
- package/dist/telemetry/backfill.d.ts +95 -0
- package/dist/telemetry/backfill.js +460 -0
- package/dist/telemetry/backfill.js.map +1 -0
- package/dist/telemetry/credentials.d.ts +44 -0
- package/dist/telemetry/credentials.js +87 -0
- package/dist/telemetry/credentials.js.map +1 -0
- package/dist/telemetry/deliver.d.ts +152 -0
- package/dist/telemetry/deliver.js +662 -0
- package/dist/telemetry/deliver.js.map +1 -0
- package/dist/telemetry/envelope-validation.d.ts +13 -0
- package/dist/telemetry/envelope-validation.js +196 -0
- package/dist/telemetry/envelope-validation.js.map +1 -0
- package/dist/telemetry/envelope.d.ts +159 -0
- package/dist/telemetry/envelope.js +288 -0
- package/dist/telemetry/envelope.js.map +1 -0
- package/dist/telemetry/events.d.ts +68 -0
- package/dist/telemetry/events.js +79 -0
- package/dist/telemetry/events.js.map +1 -0
- package/dist/telemetry/notice.d.ts +15 -0
- package/dist/telemetry/notice.js +67 -0
- package/dist/telemetry/notice.js.map +1 -0
- package/dist/telemetry/outbox.d.ts +166 -0
- package/dist/telemetry/outbox.js +741 -0
- package/dist/telemetry/outbox.js.map +1 -0
- package/dist/telemetry/quarantine.d.ts +105 -0
- package/dist/telemetry/quarantine.js +224 -0
- package/dist/telemetry/quarantine.js.map +1 -0
- package/dist/telemetry/read-sink.d.ts +31 -0
- package/dist/telemetry/read-sink.js +25 -0
- package/dist/telemetry/read-sink.js.map +1 -0
- package/dist/telemetry/recovery/command.d.ts +16 -0
- package/dist/telemetry/recovery/command.js +70 -0
- package/dist/telemetry/recovery/command.js.map +1 -0
- package/dist/telemetry/recovery/discovery.d.ts +8 -0
- package/dist/telemetry/recovery/discovery.js +277 -0
- package/dist/telemetry/recovery/discovery.js.map +1 -0
- package/dist/telemetry/recovery/files.d.ts +20 -0
- package/dist/telemetry/recovery/files.js +123 -0
- package/dist/telemetry/recovery/files.js.map +1 -0
- package/dist/telemetry/recovery/plan.d.ts +10 -0
- package/dist/telemetry/recovery/plan.js +325 -0
- package/dist/telemetry/recovery/plan.js.map +1 -0
- package/dist/telemetry/recovery/source.d.ts +328 -0
- package/dist/telemetry/recovery/source.js +253 -0
- package/dist/telemetry/recovery/source.js.map +1 -0
- package/dist/telemetry/recovery/types.d.ts +100 -0
- package/dist/telemetry/recovery/types.js +2 -0
- package/dist/telemetry/recovery/types.js.map +1 -0
- package/dist/telemetry/recovery-request-budget.d.ts +39 -0
- package/dist/telemetry/recovery-request-budget.js +102 -0
- package/dist/telemetry/recovery-request-budget.js.map +1 -0
- package/dist/telemetry/report-consistency.d.ts +11 -0
- package/dist/telemetry/report-consistency.js +111 -0
- package/dist/telemetry/report-consistency.js.map +1 -0
- package/dist/telemetry/reviewer-delivery.d.ts +39 -0
- package/dist/telemetry/reviewer-delivery.js +279 -0
- package/dist/telemetry/reviewer-delivery.js.map +1 -0
- package/dist/telemetry/reviewer-preflight.d.ts +13 -0
- package/dist/telemetry/reviewer-preflight.js +56 -0
- package/dist/telemetry/reviewer-preflight.js.map +1 -0
- package/dist/telemetry/scrub.d.ts +53 -0
- package/dist/telemetry/scrub.js +205 -0
- package/dist/telemetry/scrub.js.map +1 -0
- package/dist/telemetry/sink.d.ts +212 -0
- package/dist/telemetry/sink.js +813 -0
- package/dist/telemetry/sink.js.map +1 -0
- package/dist/telemetry/terminal-reviewer-delivery.d.ts +18 -0
- package/dist/telemetry/terminal-reviewer-delivery.js +27 -0
- package/dist/telemetry/terminal-reviewer-delivery.js.map +1 -0
- package/dist/telemetry/transient-input.d.ts +2 -0
- package/dist/telemetry/transient-input.js +37 -0
- package/dist/telemetry/transient-input.js.map +1 -0
- package/dist/telemetry/verification.d.ts +9 -0
- package/dist/telemetry/verification.js +12 -0
- package/dist/telemetry/verification.js.map +1 -0
- 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
|