fix(comparison): recheck historical calls-only changes (#2223)
* fix(comparison): preserve incomplete function call evidence Keep missing, unknown, partial and unresolved call relationships unknown in function comparisons and reconstruction claims. Aggregate IDA caller and callee coverage without overwriting uncertainty. Validate with 144 focused tests, typecheck/lint, docs checks and test discovery. Real providers were not verified; the full headless IDA session suite requires unavailable Windows native controls. * fix(comparison): recheck historical calls-only changes --------- Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
This commit is contained in:
parent
3c39a3f536
commit
688defca0f
|
|
@ -53,6 +53,7 @@ const source = (
|
|||
digit: string,
|
||||
observation: Partial<EvidenceObservation> = {},
|
||||
hasSubject = true,
|
||||
hasCoverage = true,
|
||||
): Evidence =>
|
||||
createEvidence(
|
||||
hasSubject
|
||||
|
|
@ -69,7 +70,9 @@ const source = (
|
|||
result: dossier,
|
||||
confidence: "derived",
|
||||
authority: "shipped-artifact",
|
||||
relationshipCoverage: { status: "complete" },
|
||||
...(hasCoverage
|
||||
? { relationshipCoverage: { status: "complete" as const } }
|
||||
: {}),
|
||||
...observation,
|
||||
},
|
||||
);
|
||||
|
|
@ -197,6 +200,118 @@ describe("function reconstruction source parameters", () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe("historical calls-only reconstruction changes", () => {
|
||||
it.each(["missing", "unknown", "partial", "unresolved"] as const)(
|
||||
"keeps historical calls-only changes unknown with %s call coverage",
|
||||
(coverage) => {
|
||||
const changedDossier = functionDossierSchema.parse({
|
||||
...functionDossierSchema.parse(dossier),
|
||||
callees: [
|
||||
{
|
||||
address: "0x2000",
|
||||
name: "puts",
|
||||
classification: null,
|
||||
body: { available: false, reason: "External function." },
|
||||
},
|
||||
],
|
||||
});
|
||||
const complete = source("8", {
|
||||
result: jsonValueSchema.parse(changedDossier),
|
||||
});
|
||||
const incomplete = source(
|
||||
"9",
|
||||
{
|
||||
result: jsonValueSchema.parse({
|
||||
...changedDossier,
|
||||
...(coverage === "unresolved"
|
||||
? {
|
||||
unresolved_calls: [
|
||||
{
|
||||
address: "0x1004",
|
||||
reason: "Computed target unresolved.",
|
||||
},
|
||||
],
|
||||
}
|
||||
: {}),
|
||||
}),
|
||||
...(coverage === "missing"
|
||||
? {}
|
||||
: {
|
||||
relationshipCoverage:
|
||||
coverage === "unknown"
|
||||
? { status: "unknown", reason: "Call graph unavailable." }
|
||||
: coverage === "partial"
|
||||
? {
|
||||
status: "partial",
|
||||
limitations: ["Additional edges omitted."],
|
||||
}
|
||||
: { status: "complete" },
|
||||
}),
|
||||
},
|
||||
true,
|
||||
coverage !== "missing",
|
||||
);
|
||||
const historicalResult = compareFunctions(left, complete);
|
||||
expect(historicalResult).toMatchObject({
|
||||
status: "changed",
|
||||
summary: { changed: 1 },
|
||||
});
|
||||
expect(historicalResult.changes).toMatchObject([
|
||||
{ dimension: "calls", status: "changed" },
|
||||
]);
|
||||
const completeComparison = comparison(
|
||||
{
|
||||
left_evidence_id: left.evidence_id,
|
||||
right_evidence_id: complete.evidence_id,
|
||||
},
|
||||
{},
|
||||
[left, complete],
|
||||
{},
|
||||
historicalResult,
|
||||
);
|
||||
expect(
|
||||
admittedVerification(completeComparison, [left, complete], "overall")()
|
||||
.status,
|
||||
).toBe("fail");
|
||||
const evidence = comparison(
|
||||
{
|
||||
left_evidence_id: left.evidence_id,
|
||||
right_evidence_id: incomplete.evidence_id,
|
||||
},
|
||||
{},
|
||||
[left, incomplete],
|
||||
{},
|
||||
historicalResult,
|
||||
);
|
||||
const before = JSON.stringify(evidence);
|
||||
for (const dimension of ["calls", "overall"] as const) {
|
||||
expect(
|
||||
admittedVerification(evidence, [left, incomplete], dimension)(),
|
||||
).toMatchObject({
|
||||
status: "unknown",
|
||||
summary: { total: 1, passed: 0, failed: 0, unknown: 1 },
|
||||
claims: {
|
||||
items: [
|
||||
expect.objectContaining({
|
||||
status: "unknown",
|
||||
observed_status: "changed",
|
||||
limitations: expect.arrayContaining([
|
||||
expect.stringContaining(`Source ${incomplete.evidence_id}:`),
|
||||
]),
|
||||
}),
|
||||
],
|
||||
},
|
||||
});
|
||||
}
|
||||
expect(
|
||||
admittedVerification(evidence, [left, incomplete], "pseudocode")()
|
||||
.status,
|
||||
).toBe("pass");
|
||||
expect(JSON.stringify(evidence)).toBe(before);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe("function reconstruction call coverage", () => {
|
||||
it("rechecks old unchanged comparisons without affecting unrelated finite dimensions", () => {
|
||||
const incomplete = source("7", {
|
||||
|
|
|
|||
|
|
@ -144,10 +144,15 @@ const evaluateClaim = (
|
|||
return source;
|
||||
});
|
||||
const functionSourceFacts = readValidatedSourceFacts(claim, sources);
|
||||
const observed = observedStatus(claim, comparison);
|
||||
const observation = comparisonObservation(claim, comparison);
|
||||
const observed = observation.status;
|
||||
const limitations = [
|
||||
...authorityLimitations(claim, sources, sides),
|
||||
...sourceCallCoverageLimitations(claim, functionSourceFacts, observed),
|
||||
...sourceCallCoverageLimitations(
|
||||
claim,
|
||||
functionSourceFacts,
|
||||
observation.hasIndependentFunctionChange === true,
|
||||
),
|
||||
];
|
||||
const relatedUnknowns = reconstructionClaimUnknowns(
|
||||
heads,
|
||||
|
|
@ -235,21 +240,40 @@ const validateComparisonIdentity = (claim: Claim, evidence: Evidence): void => {
|
|||
throw new TypeError("Comparison Evidence identity or authority disagrees");
|
||||
};
|
||||
|
||||
const observedStatus = (claim: Claim, evidence: Evidence): ObservedStatus => {
|
||||
const comparisonObservation = (
|
||||
claim: Claim,
|
||||
evidence: Evidence,
|
||||
): {
|
||||
readonly status: ObservedStatus;
|
||||
readonly hasIndependentFunctionChange?: boolean;
|
||||
} => {
|
||||
if (claim.kind === "behavioral") {
|
||||
const result = processCaptureComparisonSchema.parse(
|
||||
evidence.normalized_result,
|
||||
);
|
||||
validateProcessStatus(result);
|
||||
return claim.dimension === "overall"
|
||||
? result.status
|
||||
: result[claim.dimension];
|
||||
return {
|
||||
status:
|
||||
claim.dimension === "overall" ? result.status : result[claim.dimension],
|
||||
};
|
||||
}
|
||||
if (claim.kind === "structural-function") {
|
||||
const result = functionComparisonResultSchema.parse(
|
||||
evidence.normalized_result,
|
||||
);
|
||||
if (claim.dimension === "overall") return result.status;
|
||||
if (claim.dimension === "overall")
|
||||
return {
|
||||
status: result.status,
|
||||
// Historical overall changes may come solely from incomplete call sets.
|
||||
hasIndependentFunctionChange:
|
||||
result.function_match.status === "mismatched" ||
|
||||
(result.function_match.status === "matched" &&
|
||||
result.dimensions.some(
|
||||
(dimension) =>
|
||||
dimension.dimension !== "calls" &&
|
||||
dimension.status === "changed",
|
||||
)),
|
||||
};
|
||||
const dimension = result.dimensions.find(
|
||||
(item) => item.dimension === claim.dimension,
|
||||
);
|
||||
|
|
@ -263,7 +287,7 @@ const observedStatus = (claim: Claim, evidence: Evidence): ObservedStatus => {
|
|||
throw new TypeError(
|
||||
"Function dimension cites Evidence outside comparison closure",
|
||||
);
|
||||
return dimension.status;
|
||||
return { status: dimension.status };
|
||||
}
|
||||
const result = artifactComparisonResultSchema.parse(
|
||||
evidence.normalized_result,
|
||||
|
|
@ -276,7 +300,7 @@ const observedStatus = (claim: Claim, evidence: Evidence): ObservedStatus => {
|
|||
throw new TypeError(
|
||||
"Artifact change cites Evidence outside comparison closure",
|
||||
);
|
||||
return result.status;
|
||||
return { status: result.status };
|
||||
};
|
||||
|
||||
const validateProcessStatus = (
|
||||
|
|
@ -340,12 +364,12 @@ const authorityLimitations = (
|
|||
const sourceCallCoverageLimitations = (
|
||||
claim: Claim,
|
||||
functionSources: readonly FunctionSourceFacts[],
|
||||
observed: ObservedStatus,
|
||||
hasIndependentFunctionChange: boolean,
|
||||
): string[] => {
|
||||
if (
|
||||
claim.kind !== "structural-function" ||
|
||||
(claim.dimension !== "calls" &&
|
||||
!(claim.dimension === "overall" && observed !== "changed"))
|
||||
!(claim.dimension === "overall" && !hasIndependentFunctionChange))
|
||||
)
|
||||
return [];
|
||||
return functionSources.flatMap(({ evidence_id, callCoverageLimitations }) =>
|
||||
|
|
|
|||
|
|
@ -49,6 +49,9 @@ const observeAgain = (
|
|||
result,
|
||||
confidence: source.confidence,
|
||||
authority: source.authority,
|
||||
...(source.relationship_coverage === null
|
||||
? {}
|
||||
: { relationshipCoverage: source.relationship_coverage }),
|
||||
},
|
||||
);
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue