fix(hopper): preserve unknown relationship completeness

This commit is contained in:
morluto committed 2026-10-24 04:19:12 +08:00
1 parent ecfac46c8d
commit 23664efdbb
3 files changed
+74 -3

No files matched your search

+6 -1
View File
@@ -219,7 +219,12 @@ Function dossiers and `trace_feature` string matches retain this same string
evidence, including decoding uncertainty; procedure matches keep their own
named observation fields without invented string qualifications. Native call edges retain
Hopper's partial `CallReference` classification and exact endpoints; detailed
reference flags remain unavailable rather than being invented.
reference flags remain unavailable rather than being invented. Caller and callee
procedure inventories come from independent Hopper APIs; REA retains observed
relationships but reports their completeness as unknown. In particular, a resolved
tail call observed in one direction does not prove that the reverse inventory is
complete. An empty Hopper inventory cannot establish that no such call exists.
No whole-binary reverse scan is performed.
Regex searches use ECMAScript Unicode syntax in a cancellable worker with a
matching deadline and a worker old-generation heap. Deadline or cancellation
stops matching while leaving the Hopper API available. The deadline is
+55 -1
View File
@@ -1,7 +1,9 @@
import { describe, expect, it } from "vitest";
import { describe, expect, it, vi } from "vitest";
import { parseConfig } from "../config/parseConfig.js";
import { silentLogger } from "../logger.js";
import { ok } from "../domain/result.js";
import { HopperClient } from "./HopperClient.js";
import { HopperProvider } from "./HopperProvider.js";
describe("Hopper provider capabilities", () => {
@@ -86,3 +88,55 @@ describe("Hopper provider capabilities", () => {
});
});
});
describe("Hopper relationship coverage", () => {
it.each([
["procedure_callers", ["callers"]],
["procedure_callees", ["callees"]],
["analyze_function", ["callers", "callees"]],
] as const)(
"retains %s observations without claiming exhaustive relationships",
async (operation, directions) => {
const config = parseConfig({});
if (!config.ok) throw config.error;
const source =
operation === "analyze_function"
? {
callers: [],
callees: [{ address: "0x2000", name: "tail_target" }],
}
: operation === "procedure_callees"
? ["0x2000"]
: [];
const callTool = vi
.spyOn(HopperClient.prototype, "callTool")
.mockResolvedValue(ok(source));
const client = new HopperProvider(
config.value,
silentLogger,
{},
"linux",
).createClient({
path: "/tmp/fixture.hop",
sha256: "a".repeat(64),
kind: "database",
format: "analysis-database",
});
try {
const observed = await client.execute(operation, {
procedure: "0x1000",
});
if (!observed.ok) throw observed.error;
expect(observed.value.result).toEqual(source);
expect(observed.value.relationshipCoverage).toMatchObject({
status: "unknown",
limited_directions: directions,
reason: expect.stringContaining("resolved tail calls"),
});
} finally {
await client.close();
callTool.mockRestore();
}
},
);
});
+13 -1
View File
@@ -347,7 +347,19 @@ export class HopperProvider implements AnalysisProviderCandidate {
...(operation === "procedure_callers" ||
operation === "procedure_callees" ||
operation === "analyze_function"
? { relationshipCoverage: { status: "complete" as const } }
? {
relationshipCoverage: {
status: "unknown" as const,
limited_directions:
operation === "procedure_callers"
? (["callers"] as const)
: operation === "procedure_callees"
? (["callees"] as const)
: (["callers", "callees"] as const),
reason:
"Hopper reports caller and callee procedures through independent public API inventories. Their completeness, including resolved tail calls and agreement between reverse and forward relationships, has not been established. Observed relationships are retained.",
},
}
: {}),
limitations: [
...(target.kind !== "executable"