{"record":{"id":"d218baa66473d405","repo":"can1357/oh-my-pi","slug":"security-comparison-produced-an-invalid-finding-re","errorCode":null,"errorMessage":"Security comparison produced an invalid finding reference","messagePattern":"Security comparison produced an invalid finding reference","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/coding-agent/src/security/comparison.ts","lineNumber":227,"sourceCode":"\nexport function compareSecurityLineage(\n\tbefore: SecurityScanBundle,\n\tafter: SecurityScanBundle,\n): SecurityComparisonReport {\n\t// An incomplete after-scan proves nothing about unmatched earlier findings;\n\t// claiming them \"resolved\" against a cancelled/partial/failed run would be a lie.\n\tif (after.scan.status !== \"completed\") {\n\t\tthrow new Error(\n\t\t\t`Security lineage comparison requires a completed after-scan; ${after.scan.id} is ${after.scan.status}`,\n\t\t);\n\t}\n\tconst differential = compareSecurityProducers(before, after);\n\tconst beforeById = new Map(before.findings.map(finding => [finding.id, finding]));\n\tconst afterById = new Map(after.findings.map(finding => [finding.id, finding]));\n\tconst matches: SecurityFindingMatch[] = differential.matches.map(match => {\n\t\tconst beforeFinding = beforeById.get(match.referenceFindingId);\n\t\tconst afterFinding = afterById.get(match.candidateFindingId);\n\t\tif (!beforeFinding || !afterFinding) throw new Error(\"Security comparison produced an invalid finding reference\");\n\t\treturn {\n\t\t\tbeforeFindingId: beforeFinding.id,\n\t\t\tafterFindingId: afterFinding.id,\n\t\t\tfingerprint: beforeFinding.fingerprint,\n\t\t\tstatus: \"unchanged\",\n\t\t\tmatchBasis: match.basis,\n\t\t};\n\t});\n\tfor (const findingId of differential.referenceOnlyFindingIds) {\n\t\tconst finding = beforeById.get(findingId);\n\t\tif (!finding) continue;\n\t\tmatches.push({ beforeFindingId: finding.id, fingerprint: finding.fingerprint, status: \"resolved\" });\n\t}\n\tfor (const findingId of differential.candidateOnlyFindingIds) {\n\t\tconst finding = afterById.get(findingId);\n\t\tif (!finding) continue;\n\t\tmatches.push({ afterFindingId: finding.id, fingerprint: finding.fingerprint, status: \"new\" });\n\t}","sourceCodeStart":209,"sourceCodeEnd":245,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/coding-agent/src/security/comparison.ts#L209-L245","documentation":"compareSecurityLineage maps the producer-differential's matches back to actual finding objects by id. If a match references a finding id absent from either bundle's findings array, the differential produced an internally inconsistent pairing; the function throws instead of emitting a match with dangling references.","triggerScenarios":"A before/after bundle pair where compareSecurityProducers returns a match whose referenceFindingId or candidateFindingId is not present in the respective bundle's findings list — e.g. hand-assembled or partially filtered bundles passed as before/after.","commonSituations":"Building bundles programmatically and filtering findings without updating the differential inputs; external tooling rewriting finding ids between scans (breaking fingerprint/id pairing); a bug or version skew between the producer that generated matches and the bundle serializer.","solutions":["Pass the complete, unmodified bundles (as produced by readBundle/parseSecurityScanBundle) to compareSecurityLineage","Regenerate the comparison from freshly read bundles instead of reusing partial in-memory copies","Verify finding ids were not rewritten between the two scans (fingerprints should be stable)","If reproducible with intact bundles, report the upstream differential bug"],"exampleFix":"// before\nconst filtered = { ...after, findings: after.findings.filter(f => f.severity === \"high\") };\nconst report = compareSecurityLineage(before, filtered);\n// after\nconst report = compareSecurityLineage(before, after); // compare full bundles, filter the report","handlingStrategy":"validation","validationCode":"const bIds = new Set(before.findings.map(f => f.id));\nconst aIds = new Set(after.findings.map(f => f.id));\nif (before.scan.findingIds.some(id => !bIds.has(id)) || after.scan.findingIds.some(id => !aIds.has(id))) {\n\tthrow new Error(\"Bundle inconsistent; re-read bundles before comparing\");\n}","typeGuard":null,"tryCatchPattern":"try {\n\treport = compareSecurityLineage(before, after);\n} catch (err) {\n\tif (err instanceof Error && err.message === \"Security comparison produced an invalid finding reference\") {\n\t\t// reload bundles from disk and retry the comparison once\n\t} else throw err;\n}","preventionTips":["Pass bundles exactly as parsed — never filter findings without regenerating the comparison inputs","Keep finding ids and fingerprints stable across scans","Avoid hand-assembling bundles in memory","Re-read bundles from the store instead of reusing mutated copies"],"tags":["data-integrity","security-scan","internal-invariant"],"backgroundTag":"invalid-finding-reference","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}