From 23664efdbbf855d6d0490ae0b2ee4348084e7eda Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Sat, 24 Oct 2026 04:17:28 +0800 Subject: [PATCH] fix(hopper): preserve unknown relationship completeness --- docs/native-investigation.md | 7 +++- src/hopper/HopperProvider.test.ts | 56 ++++++++++++++++++++++++++++++- src/hopper/HopperProvider.ts | 14 +++++++- 3 files changed, 74 insertions(+), 3 deletions(-) diff --git a/docs/native-investigation.md b/docs/native-investigation.md index 80f1f8831f..84e2e1cc79 100644 --- a/docs/native-investigation.md +++ b/docs/native-investigation.md @@ -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 diff --git a/src/hopper/HopperProvider.test.ts b/src/hopper/HopperProvider.test.ts index a0eba852ee..addbce5bbf 100644 --- a/src/hopper/HopperProvider.test.ts +++ b/src/hopper/HopperProvider.test.ts @@ -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(); + } + }, + ); +}); diff --git a/src/hopper/HopperProvider.ts b/src/hopper/HopperProvider.ts index 7c73c89cd2..14668718be 100644 --- a/src/hopper/HopperProvider.ts +++ b/src/hopper/HopperProvider.ts @@ -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"