fix(functions): preserve coverage diagnostics separately from blockers

This commit is contained in:
morluto committed 2026-10-15 03:06:26 +08:00
1 parent 92e17ccfce
commit b9539e9c1c
7 files changed
+230 -53

No files matched your search

+24
View File
@@ -1,6 +1,7 @@
import { describe, expect, it } from "vitest";
import {
assessFunctionCallCoverage,
combineFunctionRelationshipCoverage,
isCallerOnlyIncompleteCoverage,
} from "./functionCallCoverage.js";
@@ -88,3 +89,26 @@ describe("isCallerOnlyIncompleteCoverage", () => {
).toBe(false);
});
});
describe("assessFunctionCallCoverage", () => {
it("keeps incomplete coverage and unresolved calls as equality blockers", () => {
expect(
assessFunctionCallCoverage(
{
status: "partial",
limitations: ["Read limit reached."],
diagnostics: ["Indirect calls are unresolved."],
},
{
unresolved_calls: [{ address: "0x1004", reason: "Computed target." }],
},
),
).toEqual({
blockers: [
"Read limit reached.",
"Unresolved call at 0x1004: Computed target.",
],
diagnostics: ["Indirect calls are unresolved."],
});
});
});
+14 -7
View File
@@ -78,16 +78,23 @@ export const isCallerOnlyIncompleteCoverage = (
);
};
/** Explain why two resolved call sets cannot establish call-set equality. */
export const functionCallCoverageLimitations = (
export interface FunctionCallCoverageAssessment {
/** Reasons the observed call sets cannot establish equality. */
readonly blockers: readonly string[];
/** Provider caveats that remain relevant even with complete coverage. */
readonly diagnostics: readonly string[];
}
/** Keep observed-set blockers separate from descriptive provider caveats. */
export const assessFunctionCallCoverage = (
coverage: RelationshipCoverage | null,
dossier: Pick<FunctionDossier, "unresolved_calls">,
): string[] => {
return uniqueSorted([
): FunctionCallCoverageAssessment => ({
blockers: uniqueSorted([
...relationshipCoverageLimitations(coverage),
...(coverage?.status === "complete" ? [] : (coverage?.diagnostics ?? [])),
...(dossier.unresolved_calls ?? []).map(
({ address, reason }) => `Unresolved call at ${address}: ${reason}`,
),
]);
};
]),
diagnostics: uniqueSorted(coverage?.diagnostics ?? []),
});
+92 -18
View File
@@ -291,25 +291,53 @@ describe("function comparison normalized identity", () => {
).toMatchObject({ status: "unknown" });
},
);
});
describe("function comparison address-derived call names", () => {
it.each([
{ name: "sub_deallocate", status: "unchanged" },
{ name: "sub_deadbeef", status: "unknown" },
{ name: "FUN_00401000", status: "unknown" },
{ name: "FUN_initialize", status: "unchanged" },
])("preserves the calls dimension for callee $name", ({ name, status }) => {
const calling = (base: "0x1000" | "0x2000") =>
functionDossierSchema.parse({
...dossier("return helper();", base),
callees: [procedure(base === "0x1000" ? "0x1010" : "0x2010", name)],
});
const result = compareFunctions(
observe("6", calling("0x1000")),
observe("7", calling("0x2000")),
);
expect(
result.dimensions.find(({ dimension }) => dimension === "calls"),
).toMatchObject({ status });
});
{ name: "sub_deallocate", status: "unchanged", diagnostic: undefined },
{
name: "sub_deadbeef",
status: "unknown",
diagnostic: "Provider retained an unresolved call target.",
},
{ name: "FUN_00401000", status: "unknown", diagnostic: undefined },
{ name: "FUN_initialize", status: "unchanged", diagnostic: undefined },
])(
"preserves the calls dimension for callee $name",
({ name, status, diagnostic }) => {
const calling = (base: "0x1000" | "0x2000") =>
functionDossierSchema.parse({
...dossier("return helper();", base),
callees: [
procedure(base === "0x1000" ? "0x1010" : "0x2010", name),
],
});
const result = compareFunctions(
observe("6", calling("0x1000")),
observe(
"7",
calling("0x2000"),
"rea-workflow",
diagnostic === undefined
? { status: "complete" }
: { status: "complete", diagnostics: [diagnostic] },
),
);
const calls = result.dimensions.find(
({ dimension }) => dimension === "calls",
);
expect(calls).toMatchObject({ status });
if (diagnostic !== undefined)
expect(calls).toMatchObject({
status: "unknown",
limitations: expect.arrayContaining([
"Address-derived callee or caller names cannot be matched safely.",
`Right: ${diagnostic}`,
]),
});
},
);
});
describe("function call relationship coverage", () => {
@@ -681,6 +709,52 @@ describe("function comparison unresolved text", () => {
});
describe("function comparison", () => {
it("retains complete call coverage diagnostics without blocking observed-set equality", () => {
const diagnostic = "Indirect calls remain unresolved.";
const left = observe("a", dossier("return 0;", "0x1000"), "rea-workflow", {
status: "complete",
diagnostics: [diagnostic],
});
const right = observe("b", dossier("return 0;", "0x2000"), "rea-workflow", {
status: "complete",
diagnostics: [diagnostic],
});
const comparison = compareFunctions(left, right);
expect(
comparison.dimensions.find(({ dimension }) => dimension === "calls"),
).toMatchObject({
status: "unchanged",
limitations: [`Left: ${diagnostic}`, `Right: ${diagnostic}`],
});
});
it("retains diagnostics alongside incomplete call coverage blockers", () => {
const diagnostic = "Indirect calls remain unresolved.";
const left = observe("c", dossier("return 0;", "0x1000"), "rea-workflow", {
status: "partial",
limitations: ["Provider read limit reached."],
diagnostics: [diagnostic],
});
const right = observe("d", dossier("return 0;", "0x2000"), "rea-workflow", {
status: "partial",
limitations: ["Provider read limit reached."],
diagnostics: [diagnostic],
});
expect(
compareFunctions(left, right).dimensions.find(
({ dimension }) => dimension === "calls",
),
).toMatchObject({
status: "unknown",
limitations: expect.arrayContaining([
"Left: Provider read limit reached.",
`Left: ${diagnostic}`,
"Right: Provider read limit reached.",
`Right: ${diagnostic}`,
]),
});
});
it("preserves text changes alongside complete inline dossier facets", () => {
const result = compareFunctions(
FUNCTION_COMPARISON_EXAMPLE.left,
+11 -4
View File
@@ -333,8 +333,12 @@ const compareCalls = (
const leftValues = callProjection(left);
const rightValues = callProjection(right);
const coverageLimitations = [
...left.callCoverageLimitations.map((reason) => `Left: ${reason}`),
...right.callCoverageLimitations.map((reason) => `Right: ${reason}`),
...left.callCoverageBlockers.map((reason) => `Left: ${reason}`),
...right.callCoverageBlockers.map((reason) => `Right: ${reason}`),
];
const coverageDiagnostics = [
...left.callCoverageDiagnostics.map((reason) => `Left: ${reason}`),
...right.callCoverageDiagnostics.map((reason) => `Right: ${reason}`),
];
if (coverageLimitations.length > 0)
return unresolvedDimension({
@@ -342,7 +346,7 @@ const compareCalls = (
links: context.links,
leftCount: leftValues.length,
rightCount: rightValues.length,
limitations: coverageLimitations,
limitations: [...coverageLimitations, ...coverageDiagnostics],
});
if ([...leftValues, ...rightValues].some(({ name }) => isAutoName(name)))
return unresolvedDimension({
@@ -352,6 +356,7 @@ const compareCalls = (
rightCount: rightValues.length,
limitations: [
"Address-derived callee or caller names cannot be matched safely.",
...coverageDiagnostics,
],
});
return compareValues(
@@ -359,6 +364,7 @@ const compareCalls = (
sorted(leftValues),
sorted(rightValues),
context,
coverageDiagnostics,
);
};
@@ -400,6 +406,7 @@ const compareValues = (
left: unknown,
right: unknown,
context: ComparisonContext,
limitations: readonly string[] = [],
): FunctionDimension => {
const leftJson = canonicalJson(left, "Function comparison");
const rightJson = canonicalJson(right, "Function comparison");
@@ -412,6 +419,6 @@ const compareValues = (
leftCount: Array.isArray(left) ? left.length : null,
rightCount: Array.isArray(right) ? right.length : null,
textDelta: null,
limitations: [],
limitations,
});
};
+6 -4
View File
@@ -1,7 +1,7 @@
import { parseEvidence, type Evidence } from "./evidence.js";
import { compareUnicodeCodePoints } from "./unicodeCodePointOrder.js";
import { functionDossierSchema, type FunctionDossier } from "./hopperValues.js";
import { functionCallCoverageLimitations } from "./functionCallCoverage.js";
import { assessFunctionCallCoverage } from "./functionCallCoverage.js";
/** Complete function observation parsed from one analyze_function Evidence record. */
export interface FunctionSnapshot {
@@ -9,7 +9,8 @@ export interface FunctionSnapshot {
readonly subject: NonNullable<Evidence["subject"]>;
readonly dossier: FunctionDossier;
readonly limitations: readonly string[];
readonly callCoverageLimitations: readonly string[];
readonly callCoverageBlockers: readonly string[];
readonly callCoverageDiagnostics: readonly string[];
}
/** Parse one complete analyze_function Evidence record for comparison workflows. */
@@ -22,7 +23,7 @@ export const parseFunctionEvidence = (input: unknown): FunctionSnapshot => {
if (evidence.subject === null)
throw new TypeError("Function comparison requires artifact-bound Evidence");
const dossier = functionDossierSchema.parse(evidence.normalized_result);
const callCoverageLimitations = functionCallCoverageLimitations(
const callCoverage = assessFunctionCallCoverage(
evidence.relationship_coverage,
dossier,
);
@@ -33,6 +34,7 @@ export const parseFunctionEvidence = (input: unknown): FunctionSnapshot => {
limitations: [
...new Set([...evidence.limitations, ...dossier.limitations]),
].sort((left, right) => compareUnicodeCodePoints(left, right)),
callCoverageLimitations,
callCoverageBlockers: callCoverage.blockers,
callCoverageDiagnostics: callCoverage.diagnostics,
};
};
@@ -323,11 +323,60 @@ describe("historical calls-only reconstruction changes", () => {
});
describe("function reconstruction call coverage", () => {
it("preserves complete call diagnostics while passing equal call claims", () => {
const diagnostic = "Indirect calls remain unresolved.";
const leftWithDiagnostic = source("a", {
relationshipCoverage: { status: "complete", diagnostics: [diagnostic] },
});
const rightWithDiagnostic = source("b", {
relationshipCoverage: { status: "complete", diagnostics: [diagnostic] },
});
const observed = compareFunctions(leftWithDiagnostic, rightWithDiagnostic);
expect(
observed.dimensions.find(({ dimension }) => dimension === "calls"),
).toMatchObject({
status: "unchanged",
limitations: [`Left: ${diagnostic}`, `Right: ${diagnostic}`],
});
const evidence = comparison(
{
left_evidence_id: leftWithDiagnostic.evidence_id,
right_evidence_id: rightWithDiagnostic.evidence_id,
},
{},
[leftWithDiagnostic, rightWithDiagnostic],
{},
observed,
);
expect(
admittedVerification(
evidence,
[leftWithDiagnostic, rightWithDiagnostic],
"calls",
)(),
).toMatchObject({
status: "pass",
claims: {
items: [
expect.objectContaining({
status: "pass",
observed_status: "unchanged",
limitations: expect.arrayContaining([
`Source ${leftWithDiagnostic.evidence_id}: ${diagnostic}`,
`Source ${rightWithDiagnostic.evidence_id}: ${diagnostic}`,
]),
}),
],
},
});
});
it("rechecks old unchanged comparisons without affecting unrelated finite dimensions", () => {
const incomplete = source("7", {
relationshipCoverage: {
status: "unknown",
reason: "Call graph unavailable.",
diagnostics: ["Indirect calls remain unresolved."],
},
});
const incompleteSides = {
@@ -352,6 +401,7 @@ describe("function reconstruction call coverage", () => {
observed_status: "unchanged",
limitations: expect.arrayContaining([
`Source ${incomplete.evidence_id}: Call graph unavailable.`,
`Source ${incomplete.evidence_id}: Indirect calls remain unresolved.`,
]),
}),
],
+33 -20
View File
@@ -19,7 +19,7 @@ import {
isProcessEvidenceProvider,
} from "./process/processEvidenceProvider.js";
import { functionDossierSchema } from "./hopperValues.js";
import { functionCallCoverageLimitations } from "./functionCallCoverage.js";
import { assessFunctionCallCoverage } from "./functionCallCoverage.js";
import {
deriveProcessComparisonStatus,
PROCESS_COMPARISON_DIMENSIONS,
@@ -64,7 +64,8 @@ const providers = {
interface FunctionSourceFacts {
readonly evidence_id: string;
readonly callCoverageLimitations: readonly string[];
readonly callCoverageBlockers: readonly string[];
readonly callCoverageDiagnostics: readonly string[];
}
const acceptsPredicate = (
@@ -149,13 +150,14 @@ const evaluateClaim = (
const functionSourceFacts = readValidatedSourceFacts(claim, sources);
const observation = comparisonObservation(claim, comparison);
const observed = observation.status;
const limitations = [
const callCoverage = sourceCallCoverage(
claim,
functionSourceFacts,
observation.hasIndependentFunctionChange === true,
);
const blockers = [
...authorityLimitations(claim, sources, sides),
...sourceCallCoverageLimitations(
claim,
functionSourceFacts,
observation.hasIndependentFunctionChange === true,
),
...callCoverage.blockers,
];
const relatedUnknowns = reconstructionClaimUnknowns(
heads,
@@ -164,7 +166,7 @@ const evaluateClaim = (
claim.dimension,
);
const relevantUnknowns = relatedUnknowns;
const status = classify(observed, limitations, relevantUnknowns);
const status = classify(observed, blockers, relevantUnknowns);
const displayedUnknowns = relevantUnknowns;
return reconstructionClaimResultSchema.parse({
claim_id: claim.claim_id,
@@ -180,7 +182,8 @@ const evaluateClaim = (
limitations: uniqueSorted([
...comparison.limitations,
...observation.limitations,
...limitations,
...blockers,
...callCoverage.diagnostics,
...displayedUnknowns.map(
({ question }) => `Residual unknown: ${question}`,
),
@@ -214,12 +217,14 @@ const readValidatedSourceFacts = (
parseProcessCapture(source.normalized_result);
else if (claim.kind === "structural-function") {
const dossier = functionDossierSchema.parse(source.normalized_result);
const callCoverage = assessFunctionCallCoverage(
source.relationship_coverage,
dossier,
);
functionSources.push({
evidence_id: source.evidence_id,
callCoverageLimitations: functionCallCoverageLimitations(
source.relationship_coverage,
dossier,
),
callCoverageBlockers: callCoverage.blockers,
callCoverageDiagnostics: callCoverage.diagnostics,
});
} else artifactInventoryResultSchema.parse(source.normalized_result);
}
@@ -366,20 +371,28 @@ const authorityLimitations = (
return limitations;
};
const sourceCallCoverageLimitations = (
const sourceCallCoverage = (
claim: Claim,
functionSources: readonly FunctionSourceFacts[],
hasIndependentFunctionChange: boolean,
): string[] => {
): { blockers: string[]; diagnostics: string[] } => {
if (
claim.kind !== "structural-function" ||
(claim.dimension !== "calls" &&
!(claim.dimension === "overall" && !hasIndependentFunctionChange))
)
return [];
return functionSources.flatMap(({ evidence_id, callCoverageLimitations }) =>
callCoverageLimitations.map((reason) => `Source ${evidence_id}: ${reason}`),
);
return { blockers: [], diagnostics: [] };
return {
blockers: functionSources.flatMap(({ evidence_id, callCoverageBlockers }) =>
callCoverageBlockers.map((reason) => `Source ${evidence_id}: ${reason}`),
),
diagnostics: functionSources.flatMap(
({ evidence_id, callCoverageDiagnostics }) =>
callCoverageDiagnostics.map(
(reason) => `Source ${evidence_id}: ${reason}`,
),
),
};
};
const classify = (