{"record":{"id":"c04ccb545b8a4812","repo":"can1357/oh-my-pi","slug":"security-lineage-comparison-requires-a-completed-a","errorCode":null,"errorMessage":"Security lineage comparison requires a completed after-scan; ${after.scan.id} is ${after.scan.status}","messagePattern":"Security lineage comparison requires a completed after-scan; (.+?) is (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"packages/coding-agent/src/security/comparison.ts","lineNumber":217,"sourceCode":"\t\t),\n\t\tcandidateOnlyFindingIds,\n\t\treferenceFindingCount: reference.findings.length,\n\t\tcandidateFindingCount: candidate.findings.length,\n\t\tmatchedFindingCount: matches.length,\n\t\trecallAgainstReference: ratio(matches.length, reference.findings.length),\n\t\tprecisionAgainstReference: ratio(matches.length, candidate.findings.length),\n\t\tjaccardOverlap: ratio(matches.length, unionSize),\n\t};\n}\n\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});","sourceCodeStart":199,"sourceCodeEnd":235,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/coding-agent/src/security/comparison.ts#L199-L235","documentation":"compareSecurityLineage only produces trustworthy 'resolved' claims when the after-scan finished. If the after bundle's scan status is anything other than 'completed' (cancelled, failed, partial), unmatched earlier findings might simply not have been re-scanned, so reporting them as resolved would be false. The function throws with the scan id and actual status.","triggerScenarios":"Calling compareSecurityLineage(before, after) (directly or via compare/report) where after.scan.status is 'cancelled', 'failed', 'partial', 'running', etc.","commonSituations":"User cancelled a long scan then asked for a before/after report; a scan crashed mid-run and its partial bundle was persisted; comparing against a scan still in progress; importing a stale/incomplete bundle as the 'after' side.","solutions":["Wait for the after-scan to reach 'completed' before comparing (poll status or await the scan promise)","Re-run the security scan to produce a completed bundle","Check after.scan.status before calling and surface a 'scan incomplete' message in your UI instead","Compare against a different, previously completed bundle"],"exampleFix":"// before\nconst report = compareSecurityLineage(before, afterMaybeIncomplete);\n// after\nif (afterMaybeIncomplete.scan.status !== \"completed\") {\n\tthrow new Error(`Re-run scan ${afterMaybeIncomplete.scan.id}; it did not complete.`);\n}\nconst report = compareSecurityLineage(before, afterMaybeIncomplete);","handlingStrategy":"validation","validationCode":"if (after.scan.status !== \"completed\") {\n\tthrow new Error(`After-scan ${after.scan.id} is ${after.scan.status}; rerun before comparing`);\n}","typeGuard":"const isCompleted = (b: SecurityScanBundle): boolean => b.scan.status === \"completed\";","tryCatchPattern":"try {\n\treport = compareSecurityLineage(before, after);\n} catch (err) {\n\tif (err instanceof Error && err.message.startsWith(\"Security lineage comparison requires a completed after-scan\")) {\n\t\t// queue a rescan and retry later; do not report 'resolved' counts\n\t} else throw err;\n}","preventionTips":["Gate comparisons on after.scan.status === \"completed\" in your pipeline","Never compare against cancelled/failed/running scans","Await the scan promise (or poll status) before generating reports","Treat partially imported bundles as non-comparable"],"tags":["security-scan","state-validation","incomplete-data"],"backgroundTag":"scan-not-completed","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}