const { normalizeText } = require('./scanGrounding'); // Agreement must be judged on the RAW model answers, not the grounded ones: // catalog grounding has a genus-level fallback that can collapse two different // species onto the same catalog entry and fake an agreement. const reviewAgreesWithPrimary = (rawPrimary, rawReview) => { if (!rawPrimary || !rawReview) return false; const primaryBotanical = normalizeText(rawPrimary.botanicalName); const primaryName = normalizeText(rawPrimary.name); const botanicalMatch = Boolean(primaryBotanical) && primaryBotanical === normalizeText(rawReview.botanicalName); const nameMatch = Boolean(primaryName) && primaryName === normalizeText(rawReview.name); return botanicalMatch || nameMatch; }; // The review runs on a stronger model chain than the primary, and models are // not calibrated against each other — so on disagreement the review wins even // when it trails the primary by up to this margin. const REVIEW_DISAGREEMENT_MARGIN = 0.05; // A second low-confidence guess is not a verification: the review may only // replace the primary when it agrees with it or is (near-)competitively // confident. On agreement the higher-confidence variant wins (tie goes to // the review, which runs on the stronger model chain). const decideReviewOutcome = ({ primaryResult, reviewResult, agrees }) => { const primaryConfidence = primaryResult?.confidence || 0; const reviewConfidence = reviewResult?.confidence || 0; if (agrees) { return { accept: true, replace: reviewConfidence >= primaryConfidence, reason: 'review-confirmed-primary', }; } if (reviewConfidence >= primaryConfidence - REVIEW_DISAGREEMENT_MARGIN) { return { accept: true, replace: true, reason: 'review-overrode-primary' }; } return { accept: false, replace: false, reason: 'review-rejected-low-confidence' }; }; module.exports = { decideReviewOutcome, reviewAgreesWithPrimary, };