feat(filter): finding-class priors + witness-based corroboration (#468)#499
feat(filter): finding-class priors + witness-based corroboration (#468)#499justn-hyeok merged 1 commit intomainfrom
Conversation
Follow-up to the #468 evidence-quality scorer using two research-backed techniques to attack the remaining FP class that the n=3 baseline runs captured against fp-moderator-regex and quota-manager-dual. ### Check 7 — finding-class priors New packages/core/src/pipeline/finding-class-scorer.ts holds an ordered table of empirically FP-heavy claim patterns: - redos / catastrophic backtracking ×0.6 - zero-width / invisible unicode character ×0.5 - "may throw" / uncaught exception ×0.7 - missing input validation / sanitization ×0.7 - generic "potential" security concern ×0.85 Match on issueTitle + problem text, first match wins. The multiplier is folded into doc.confidence the same way checks 3–6 do and recorded on confidenceTrace.classPrior for explainability. Rule-source docs bypass the whole filter as before. ### Witness-based echo dampener (confidence.ts) In the L1 corroboration formula, when 3+ co-located findings share the same evidence fingerprint (first 80 normalized chars of joined evidence[]), the "agreement" is almost always multiple reviewers echoing the same superficial cue rather than independent witnessing. Cap the boost with a ×0.75 dampener. Triggers only when: 1. 3+ co-located docs have non-empty evidence 2. 3+ distinct fingerprints exist (well-formed data) 3. Majority (≥half rounded down) share the same fingerprint Existing tests pass unchanged — they use empty evidence[] arrays so the dampener cannot fire. Six new tests in confidence-witness.test.ts exercise each branch. ### Trace / types - confidenceTrace.classPrior: string (optional) - Trace formatter renders "class prior: <id>" line below stage rows - Rule-source docs still bypass the filter entirely ### Tests - 17 finding-class-scorer tests including the real FP samples from 2026-04-20 bench-fn runs + negative cases for real bugs - 6 witness-based echo-dampener tests - hallucination-filter.test.ts richProblem() padding rewritten to avoid accidentally matching may-throw ("without error handling") or missing-validation ("unvalidated argument") patterns - Repo: 3402 → 3425 tests No live bench run in this session — measurement deferred to next session's budget. Unit coverage uses captured 2026-04-20 FP samples as fixtures so the regression is pinned.
There was a problem hiding this comment.
🟠 CodeAgora: NEEDS HUMAN REVIEW
1 verify · 4 ignore | 5 reviewers · 1 debates
The only discussed CRITICAL issue has mixed confidence and only reaches 48%, with one reviewer explicitly disputing that it is a blocking security defect and framing it as a robustness concern rather than a proven vulnerability. The evidence presented suggests a possible missing guard around
activeReviewersincomputeL1Confidence, but it is not sufficiently substantiated to confidently reject the change, especially given the triage guidance that low-confidence critical findings should be routed for human review rather than treated as blocking. The unconfirmed single-reviewer issues also remain unvalidated, so the overall change is not clearly unsafe, but it is not proven safe enough to merge without human confirmation.
Verify
| File | Issue | Confidence | |
|---|---|---|---|
| 🔴 | packages/core/src/pipeline/confidence.ts:68 |
Missing null guard for activeReviewers in computeL1Confidence | 🟡 48% |
4 suggestion(s)
packages/core/src/pipeline/hallucination-filter.ts:238— Potential undefined reference in hallucination-filter.tspackages/core/src/tests/hallucination-filter.test.ts:87— Test inconsistency in hallucination-filter.test.tspackages/core/src/pipeline/finding-class-scorer.ts:114— Potential null/undefined string concatenation in matchFindingClasspackages/core/src/pipeline/confidence.ts:120— Echo dampener condition may be too lenient
Issue distribution (4 file(s))
| File | Issues |
|---|---|
packages/core/src/pipeline/confidence.ts |
████████████ 2 |
packages/core/src/pipeline/hallucination-filter.ts |
██████ 1 |
packages/core/src/tests/hallucination-filter.test.ts |
██████ 1 |
packages/core/src/pipeline/finding-class-scorer.ts |
██████ 1 |
Agent consensus log (1 discussion(s))
✅ d001 — 1 round(s), consensus → CRITICAL
Verdict: CRITICAL — Majority consensus (2/3 agree)
CodeAgora · Session: 2026-04-20/001
| ); | ||
| const agreeing = coLocated.length; | ||
| const agreementRate = Math.min(100, Math.round((agreeing / activeReviewers) * 100)); | ||
|
|
There was a problem hiding this comment.
🔴 CRITICAL — Missing null guard for activeReviewers in computeL1Confidence
Confidence: 🟡 48%
Problem: In packages/core/src/pipeline/confidence.ts:68
The function computeL1Confidence calculates agreementRate using division by activeReviewers:
const agreementRate = Math.min(100, Math.round((agreeing / activeReviewers) * 100));If activeReviewers is passed as 0 or undefined, this produces NaN or Infinity, corrupting downstream confidence calculations.
Evidence:
- Line 68 shows division by
activeReviewerswithout guard - No validation at function entry
- The function is exported and could be called with any values
| if (typeof activeReviewers !== 'number' || activeReviewers <= 0) { | |
| return doc.confidence ?? 50; | |
| } |
✅ Discussion d001 — 1 round(s), consensus
Verdict: CRITICAL — Majority consensus (2/3 agree)
Flagged by: r3 | CodeAgora
| // uncertain-bucket docs also carry the trace. Pass-through (no | ||
| // penalties) → filtered === raw. | ||
| if (doc.confidence !== undefined) { | ||
| doc.confidenceTrace = { |
There was a problem hiding this comment.
🟡 WARNING — Potential undefined reference in hallucination-filter.ts
Confidence: 🔴 24%
Problem: In packages/core/src/pipeline/hallucination-filter.ts:238-244
The classMatch variable may be used after it's potentially used in the confidence penalty logic, but there's no null check when building the confidenceTrace object.
Evidence:
- Line 228:
const classMatch = matchFindingClass(doc); - Lines 229-231: Uses
classMatchwith a conditional check - Lines 238-244: Builds confidenceTrace with
classMatchspread
Suggestion: Either:
- Only add
classPriorto trace when a penalty was actually applied (whenclassMatch.multiplier < 1.0), OR - Keep the current behavior but update the trace output to clarify "matched" vs "applied"
Flagged by: r3 | CodeAgora
| @@ -87,10 +87,10 @@ function richProblem(snippet: string): string { | |||
| return ( | |||
| `At src/utils.ts:10 inside the introduced call: ${snippet} ` + | |||
There was a problem hiding this comment.
🟡 WARNING — Test inconsistency in hallucination-filter.test.ts
Confidence: 🔴 22%
Problem: In packages/core/src/tests/hallucination-filter.test.ts:87-94 and 282-289
The test text was changed from "unvalidated argument" to "raw numeric argument" to avoid false positive matching on the missing-validation check. However:
-
The
zero-widthprior includes pattern/\bu\+200b\b/i- the new text "raw numeric" contains "u" followed by "+" followed by "200b" in sequence ("raw numeric argument into setTimeoutWrapperu + 200b")? No, this doesn't match. -
Actually, the real issue: The test change weakens the test coverage - now a finding that says "raw numeric argument" would NOT be caught by missing-validation, but a finding saying "unvalidated argument" WOULD be. This could allow real FP findings to slip through.
Evidence:
- Old text: "unvalidated argument" - matches
/\bunvalidated\b/i - New text: "raw numeric argument" - does NOT match any pattern
- The test expects 80 confidence (no penalty), but this change means the test doesn't actually verify the missing-validation check is working
| `At src/utils.ts:10 inside the introduced call: ${snippet} ` + | |
| const classMatch = matchFindingClass(doc); | |
| if (classMatch && classMatch.multiplier < 1.0) { | |
| const penalized = Math.round((doc.confidence ?? 50) * classMatch.multiplier); | |
| doc.confidence = penalized; | |
| } | |
| // ... later ... | |
| doc.confidenceTrace = { | |
| ...(classMatch ? { classPrior: classMatch.id } : {}), | |
| }; |
Flagged by: r3 | CodeAgora
| */ | ||
| export function matchFindingClass(doc: EvidenceDocument): MatchedClass | null { | ||
| const haystack = `${doc.issueTitle}\n${doc.problem}`; | ||
| for (const prior of FINDING_CLASS_PRIORS) { |
There was a problem hiding this comment.
🟡 WARNING — Potential null/undefined string concatenation in matchFindingClass
Confidence: 🔴 30%
Problem: In packages/core/src/pipeline/finding-class-scorer.ts:114
The matchFindingClass function concatenates issueTitle and problem without checking if they are null or undefined. If either field is null/undefined, it would create literal "null" or "undefined" strings in the haystack, potentially leading to unexpected regex matches.
Evidence:
- Line 114 constructs haystack as:
const haystack =${doc.issueTitle}\n${doc.problem}; - No null/undefined checks before string interpolation
- If
doc.issueTitleis null, haystack becomes "null\n[problem]" - A regex like
/null/i(not present but could be added later) would match unexpectedly
| for (const prior of FINDING_CLASS_PRIORS) { | |
| const haystack = `${doc.issueTitle ?? ''}\n${doc.problem ?? ''}`; |
Flagged by: r5 | CodeAgora
| if (fingerprints.length >= 3) { | ||
| const distinctCount = new Set(fingerprints).size; | ||
| if (distinctCount <= Math.floor(fingerprints.length / 2)) { | ||
| base = Math.round(base * 0.75); |
There was a problem hiding this comment.
🟡 WARNING — Echo dampener condition may be too lenient
Confidence: 🔴 20%
Problem: In packages/core/src/pipeline/confidence.ts:120
The echo dampener triggers when distinctCount <= Math.floor(fingerprints.length / 2). For 3 fingerprints, this allows 1 distinct fingerprint (2 identical) to trigger the dampener, which may not represent a true "majority" echo cascade.
Evidence:
- For 3 fingerprints: Math.floor(3/2) = 1, so distinctCount <= 1 triggers
- This means 2 identical + 1 different triggers dampener
- The comment says "majority share the same" but 2/3 is not a majority
Suggestion: Change condition to require strict majority: distinctCount < Math.ceil(fingerprints.length / 2) or distinctCount * 2 < fingerprints.length
Flagged by: r5 | CodeAgora
Summary
Follow-up to #468. Two research-backed additions targeting the FP class captured in today's n=3 baseline runs against `fp-moderator-regex` and `quota-manager-dual`.
Check 7 — finding-class priors
`packages/core/src/pipeline/finding-class-scorer.ts` holds an ordered table of empirically FP-heavy claim patterns with category-specific multipliers:
Each class's multiplier is folded into `doc.confidence` and the matched class id is recorded on `confidenceTrace.classPrior`. First match wins (specific classes before catch-all).
Witness-based echo dampener
Modifies `computeL1Confidence` — when 3+ co-located findings share the same evidence fingerprint (first 80 normalised chars of joined `evidence[]`), apply a ×0.75 dampener. Catches "correlated failure cascades" where multiple reviewers latch onto the same superficial cue instead of corroborating independently. Research ref: Microsoft "Tree of Reviews" (2024).
Triggers only when:
Existing tests use empty evidence arrays → dampener stays off → zero behaviour change for prior test suite.
Measurement
No live bench run this session — budget-conscious. Unit tests pin the regression against actual 2026-04-20 FP samples captured from runs 24669968011 / 24670411622 / 24670855655 (all against `fp-moderator-regex`). Next session's first bench run will measure real impact.
Test plan
Risks