From 7784fdbad78f8b384d513f0e7dae8e983cd48ef8 Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Sat, 24 Oct 2026 02:24:13 +0800 Subject: [PATCH] fix(native): retain search row qualifications in feature traces --- docs/native-investigation.md | 4 +- src/application/EnhancedLiteralTracing.ts | 47 +++++++------------ src/application/EnhancedToolTypes.ts | 6 +-- src/contracts/toolOutputSchemaGroups.ts | 9 +--- src/domain/native/literalFeatureMatch.ts | 10 ++++ ...enhancedTools.traceFeatureCoverage.test.ts | 31 ++++++++++++ 6 files changed, 63 insertions(+), 44 deletions(-) create mode 100644 src/domain/native/literalFeatureMatch.ts diff --git a/docs/native-investigation.md b/docs/native-investigation.md index e2a3f6ef06..2b40d0d7d0 100644 --- a/docs/native-investigation.md +++ b/docs/native-investigation.md @@ -207,7 +207,9 @@ observed source encoding. Hopper can split long literals into adjacent unterminated objects; search matches each object's decoded bytes independently. Undecodable objects retain native display text with `decoding.available: false` and a reason, so one uncertain object does not block unrelated inspections. -Function dossiers retain this same string evidence. Native call edges retain +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. Regex searches use ECMAScript Unicode syntax in a cancellable worker with a diff --git a/src/application/EnhancedLiteralTracing.ts b/src/application/EnhancedLiteralTracing.ts index 79684ce0fa..4ce765f033 100644 --- a/src/application/EnhancedLiteralTracing.ts +++ b/src/application/EnhancedLiteralTracing.ts @@ -1,4 +1,4 @@ -import { z } from "zod"; +import { nativeInventoryRowsSchemas } from "../domain/native/nativeInventory.js"; import type { AnalysisExecution, AnalysisOperation, @@ -93,9 +93,11 @@ const literalMatches = async ( signal, ); if (!result.ok) return result; - const items = z - .array(z.object({ address: z.string(), value: z.string() })) - .safeParse(result.value); + const items = ( + type === "string" + ? nativeInventoryRowsSchemas.strings + : nativeInventoryRowsSchemas.names + ).safeParse(result.value); if (!items.success) return err( new AnalysisOutputError( @@ -103,37 +105,20 @@ const literalMatches = async ( "provider returned an invalid search result", ), ); - addPageMatches({ - items: items.data.map(({ address, value: name }) => ({ - address, - name, - })), - type, - matches, - matched, - }); + for (const item of items.data) { + const key = `${type}\u0000${item.address}`; + if (matched.has(key)) continue; + matches.push( + type === "string" + ? { ...item, type: "string" } + : { ...item, type: "procedure" }, + ); + matched.add(key); + } } return ok({ matches }); }; -const addPageMatches = (input: { - readonly items: readonly { - readonly address: string; - readonly name: string; - }[]; - readonly type: TraceMatch["type"]; - readonly matches: TraceMatch[]; - readonly matched: Set; -}): void => { - const { items, type, matches, matched } = input; - for (const item of items) { - const key = `${type}\u0000${item.address}`; - if (matched.has(key)) continue; - matches.push({ type, address: item.address, value: item.name }); - matched.add(key); - } -}; - const traceReferences = async ( call: AnalysisCall, observe: ObserveCall, diff --git a/src/application/EnhancedToolTypes.ts b/src/application/EnhancedToolTypes.ts index 740e29ac4d..25121bc100 100644 --- a/src/application/EnhancedToolTypes.ts +++ b/src/application/EnhancedToolTypes.ts @@ -10,11 +10,7 @@ import { } from "../contracts/enhancedInputs.js"; export type EnhancedResult = Promise>; -export type TraceMatch = { - readonly type: string; - readonly address: string; - readonly value: string; -}; +export type { LiteralFeatureMatch as TraceMatch } from "../domain/native/literalFeatureMatch.js"; export type TraceReference = { readonly target_address: string; readonly source_address: string; diff --git a/src/contracts/toolOutputSchemaGroups.ts b/src/contracts/toolOutputSchemaGroups.ts index 16add16138..4279d1a283 100644 --- a/src/contracts/toolOutputSchemaGroups.ts +++ b/src/contracts/toolOutputSchemaGroups.ts @@ -1,3 +1,4 @@ +import { literalFeatureMatchSchema } from "../domain/native/literalFeatureMatch.js"; import { byteEntropySchema } from "../domain/native/byteEntropy.js"; import { byteEntropySummaryViewResultSchema } from "../domain/analysisView/analysisView.js"; import { nativeFunctionMatchingSchema } from "../domain/native/functionMatching.js"; @@ -231,13 +232,7 @@ const literalTraceOutput = evidenceResultOf( z.object({ query: z.string(), search_mode: z.literal("literal"), - matches: z.array( - z.object({ - type: z.enum(["string", "procedure"]), - address: z.string(), - value: z.string(), - }), - ), + matches: z.array(literalFeatureMatchSchema), references: z.array( z.object({ target_address: z.string(), diff --git a/src/domain/native/literalFeatureMatch.ts b/src/domain/native/literalFeatureMatch.ts new file mode 100644 index 0000000000..0e5fc195af --- /dev/null +++ b/src/domain/native/literalFeatureMatch.ts @@ -0,0 +1,10 @@ +import { z } from "zod"; +import { analysisStringSchema } from "../hopperValues.js"; +import { addressedValueSchema } from "./nativeInventory.js"; + +/** Tracing reuses the search row's qualifications without reinterpreting them. */ +export const literalFeatureMatchSchema = z.discriminatedUnion("type", [ + analysisStringSchema.extend({ type: z.literal("string") }), + addressedValueSchema.extend({ type: z.literal("procedure") }), +]); +export type LiteralFeatureMatch = z.infer; diff --git a/tests/boundary/mcp/enhancedTools.traceFeatureCoverage.test.ts b/tests/boundary/mcp/enhancedTools.traceFeatureCoverage.test.ts index 3b07990b73..c38ec1403e 100644 --- a/tests/boundary/mcp/enhancedTools.traceFeatureCoverage.test.ts +++ b/tests/boundary/mcp/enhancedTools.traceFeatureCoverage.test.ts @@ -267,3 +267,34 @@ describe("trace_feature reference coverage", () => { expect(calls.filter((call) => call.startsWith("xrefs:"))).toEqual([]); }); }); + +it("preserves string search qualifications in trace matches without fabricating them for procedures", async () => { + const baseline = provider({ duplicateProcedure: true }); + const row = { + address: "0x10", + value: "hello", + provider_value: "provider display", + decoding: { available: false, reason: "Typed bytes were unavailable." }, + string: { + encoding: "utf-8", + encoding_status: "inferred", + termination: "missing", + byte_length: 5, + }, + } as const; + const output = await trace({ + execute: (operation, input, options) => + operation === "search_strings" + ? Promise.resolve(ok(createAnalysisExecution([row], PROVIDER))) + : baseline.execute(operation, input, options), + }); + expect(output.normalized_result).toMatchObject({ + matches: [ + { ...row, type: "string" }, + { type: "procedure", address: "0x10", value: "hello" }, + ], + }); + expect( + (output.normalized_result as { matches: object[] }).matches[1], + ).not.toHaveProperty("string"); +});