From 06822aed63a20ebda18bf8cc5b1589bf14761838 Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Sat, 24 Oct 2026 09:28:17 +0800 Subject: [PATCH] fix(native): require canonical dispatch inventory rows Remove the undocumented name alias from list_names fallback and validate the canonical value rows with the existing strict policy. Keep record defaults in the public input contract and require explicit decoder budgets. Preserve provenance, partial coverage, source coordinates and typed failures in active application and MCP tests. --- .../EnhancedNativeInvestigation.test.ts | 14 ++++---- .../NativeDispatchMetadataInspection.ts | 21 ++++-------- src/domain/native/objcSwiftMetadata.test.ts | 34 ++++++++++++------- src/domain/native/objcSwiftMetadata.ts | 2 +- .../mcp/nativeDispatchAvailability.test.ts | 17 ++++++---- .../boundary/mcp/nativeUiAvailability.test.ts | 9 ++--- .../mcp/nativeUiPartialCallRoutesMcp.test.ts | 2 +- 7 files changed, 53 insertions(+), 46 deletions(-) diff --git a/src/application/native/EnhancedNativeInvestigation.test.ts b/src/application/native/EnhancedNativeInvestigation.test.ts index b598ea2ad8..0dd818372d 100644 --- a/src/application/native/EnhancedNativeInvestigation.test.ts +++ b/src/application/native/EnhancedNativeInvestigation.test.ts @@ -200,7 +200,7 @@ const testAnalysis = ( }); case "list_names": return execution(operation, [ - { address: "0x1000", name: "-[BuildViewController buildTapped:]" }, + { address: "0x1000", value: "-[BuildViewController buildTapped:]" }, ]); case "procedure_callees": return execution( @@ -535,10 +535,6 @@ describe("native dispatch inventory", () => { it.each([ { label: "plain value", entry: { address: "0x1000", value: methodName } }, { label: "value with symbol facts", entry: methodInventoryEntry }, - { - label: "legacy name with symbol facts", - entry: { address: "0x1000", name: methodName, symbol: methodSymbol }, - }, ])( "binds $label inventory to the target without overstating dispatch coverage", async ({ entry }) => { @@ -583,6 +579,10 @@ describe("native dispatch inventory", () => { ); it.each([ + { + label: "legacy name with symbol facts", + entry: { address: "0x1000", name: methodName, symbol: methodSymbol }, + }, { label: "malformed symbol flag", entry: { @@ -836,11 +836,11 @@ describe("native UI trace boundaries", () => { it("matches handlers after the dispatch inventory's former default limit", async () => { const names = Array.from({ length: 5_000 }, (_, index) => ({ address: `0x${(index + 1).toString(16)}`, - name: `unrelated_symbol_${index}`, + value: `unrelated_symbol_${index}`, })); names.push({ address: "0x2000", - name: "-[BuildViewController buildTapped:]", + value: "-[BuildViewController buildTapped:]", }); const result = await new EnhancedTools( testAnalysis({ list_names: () => execution("list_names", names) }), diff --git a/src/application/native/NativeDispatchMetadataInspection.ts b/src/application/native/NativeDispatchMetadataInspection.ts index fd384594a8..c56f64c89b 100644 --- a/src/application/native/NativeDispatchMetadataInspection.ts +++ b/src/application/native/NativeDispatchMetadataInspection.ts @@ -1,5 +1,4 @@ -import { z } from "zod"; -import { addressedValueSchema } from "../../domain/native/nativeInventory.js"; +import { nativeInventoryRowsSchemas } from "../../domain/native/nativeInventory.js"; import { nativeDispatchMetadataResultSchema, inspectNativeDispatchMetadata, @@ -10,6 +9,10 @@ import { err, ok } from "../../domain/result.js"; import type { AnalysisOperationPort } from "../AnalysisProvider.js"; import type { EnhancedResult } from "../EnhancedToolTypes.js"; +const dispatchNameInventorySchema = nativeInventoryRowsSchemas.names.element + .strict() + .array(); + /** Source operations for this workflow, shared with capability discovery. */ export const NATIVE_DISPATCH_METADATA_SOURCES = { binary: "inspect_native_dispatch_metadata", @@ -64,17 +67,7 @@ export const inspectNativeDispatch = async ( signal === undefined ? {} : { signal }, ); if (!execution.ok) return err(execution.error); - const names = z - .array( - z.union([ - addressedValueSchema.strict(), - addressedValueSchema - .omit({ value: true }) - .extend({ name: z.string() }) - .strict(), - ]), - ) - .safeParse(execution.value.result); + const names = dispatchNameInventorySchema.safeParse(execution.value.result); if (!names.success) return err( new AnalysisOutputError( @@ -85,7 +78,7 @@ export const inspectNativeDispatch = async ( const result = inspectNativeDispatchMetadata( names.data.map((entry) => ({ address: entry.address, - name: "value" in entry ? entry.value : entry.name, + name: entry.value, })), maxRecords, ); diff --git a/src/domain/native/objcSwiftMetadata.test.ts b/src/domain/native/objcSwiftMetadata.test.ts index 15327c15f7..7f5dfcc726 100644 --- a/src/domain/native/objcSwiftMetadata.test.ts +++ b/src/domain/native/objcSwiftMetadata.test.ts @@ -20,9 +20,10 @@ describe("symbol-only Objective-C implementation identity", () => { ])( "preserves $source without inventing a numeric method identity", ({ source, canonical, numeric }) => { - const result = inspectNativeDispatchMetadata([ - { address: source, name: "-[StoreController buildTapped:]" }, - ]); + const result = inspectNativeDispatchMetadata( + [{ address: source, name: "-[StoreController buildTapped:]" }], + 1, + ); expect(result.objc_classes[0]?.methods[0]?.address).toBe(numeric); expect(result.objc_dispatch_implementations[0]).toMatchObject({ implementation_address: canonical, @@ -34,10 +35,13 @@ describe("symbol-only Objective-C implementation identity", () => { it("retains unparseable symbol coordinates as reported evidence, without claiming a location", () => { const source = "invalid::0x2000"; - const result = inspectNativeDispatchMetadata([ - { address: source, name: "-[StoreController buildTapped:]" }, - { address: source, name: "$s5Store7launchyyF" }, - ]); + const result = inspectNativeDispatchMetadata( + [ + { address: source, name: "-[StoreController buildTapped:]" }, + { address: source, name: "$s5Store7launchyyF" }, + ], + 2, + ); expect(result.objc_dispatch_implementations[0]).toMatchObject({ implementation_address: null, location: { address: null }, @@ -57,6 +61,7 @@ describe("symbol-only Objective-C implementation identity", () => { address, name: "-[StoreController buildTapped:]", })), + ordered.length, ); expect(result.objc_dispatch_implementations).toHaveLength(1); expect(result.objc_dispatch_implementations[0]).toMatchObject({ @@ -87,6 +92,7 @@ describe("symbol-only Objective-C implementation identity", () => { address, name: "-[StoreController buildTapped:]", })), + 2, ); expect(result.objc_dispatch_implementations).toHaveLength(2); expect(result.objc_classes[0]?.methods).toEqual([ @@ -176,11 +182,14 @@ describe("ObjC/Swift metadata", () => { }); it("retains unlocated Swift and Objective-C symbols with explicit unknown addresses and source evidence", () => { - const result = inspectNativeDispatchMetadata([ - { name: "$s4Test7MyClassC", address: "" }, - { name: "-[StoreController buildTapped:]", address: "" }, - { name: "$s5Store7launchyyF", address: "ram:0x3000" }, - ]); + const result = inspectNativeDispatchMetadata( + [ + { name: "$s4Test7MyClassC", address: "" }, + { name: "-[StoreController buildTapped:]", address: "" }, + { name: "$s5Store7launchyyF", address: "ram:0x3000" }, + ], + 3, + ); expect(result.swift_symbols).toMatchObject([ { mangled_name: "$s4Test7MyClassC", @@ -226,6 +235,7 @@ it("derives classes only from complete class symbol leaves", () => { "_OBJC_METACLASS_$_Local", "OwnController::viewDidLoad", ].map((name, index) => ({ address: `0x${String(index + 1)}`, name })), + 7, ); expect(result.objc_classes.map(({ name }) => name)).toEqual([ "Local", diff --git a/src/domain/native/objcSwiftMetadata.ts b/src/domain/native/objcSwiftMetadata.ts index fc61c26cdf..baa3368ec1 100644 --- a/src/domain/native/objcSwiftMetadata.ts +++ b/src/domain/native/objcSwiftMetadata.ts @@ -474,7 +474,7 @@ export const nativeDispatchMetadataResultSchema = z.strictObject({ /** Decode only facts visible in a provider's existing name inventory. */ export const inspectNativeDispatchMetadata = ( names: readonly { readonly address: string; readonly name: string }[], - maxRecords = DEFAULT_DISPATCH_METADATA_RECORDS, + maxRecords: number, ): ObjcSwiftMetadata => { const symbolEvidence = ( entry: (typeof names)[number], diff --git a/tests/boundary/mcp/nativeDispatchAvailability.test.ts b/tests/boundary/mcp/nativeDispatchAvailability.test.ts index 793fc28cd0..6ba0abae62 100644 --- a/tests/boundary/mcp/nativeDispatchAvailability.test.ts +++ b/tests/boundary/mcp/nativeDispatchAvailability.test.ts @@ -35,14 +35,17 @@ const identity = { name: "Fixture analysis provider", version: "1", }; -const dispatchMetadata = inspectNativeDispatchMetadata([ - { - address: "0x1000", - name: "-[BuildViewController buildTapped:]", - }, -]); +const dispatchMetadata = inspectNativeDispatchMetadata( + [ + { + address: "0x1000", + name: "-[BuildViewController buildTapped:]", + }, + ], + 1, +); const symbolInventory = [ - { address: "0x1000", name: "-[BuildViewController buildTapped:]" }, + { address: "0x1000", value: "-[BuildViewController buildTapped:]" }, ]; type Operation = "inspect_native_dispatch_metadata" | "list_names"; diff --git a/tests/boundary/mcp/nativeUiAvailability.test.ts b/tests/boundary/mcp/nativeUiAvailability.test.ts index 6e208f4434..113e54894c 100644 --- a/tests/boundary/mcp/nativeUiAvailability.test.ts +++ b/tests/boundary/mcp/nativeUiAvailability.test.ts @@ -64,11 +64,12 @@ const controllerName = "BuildViewController"; const selector = "buildTapped:"; const handlerAddress = "0x1000"; const calleeAddress = "0x2000"; -const dispatchMetadata = inspectNativeDispatchMetadata([ - { address: handlerAddress, name: `-[${controllerName} ${selector}]` }, -]); +const dispatchMetadata = inspectNativeDispatchMetadata( + [{ address: handlerAddress, name: `-[${controllerName} ${selector}]` }], + 1, +); const symbolInventory = [ - { address: handlerAddress, name: `-[${controllerName} ${selector}]` }, + { address: handlerAddress, value: `-[${controllerName} ${selector}]` }, ]; const interfaceBuilderFor = (targetSha256: string) => buildInterfaceBuilderAnalysis({ diff --git a/tests/boundary/mcp/nativeUiPartialCallRoutesMcp.test.ts b/tests/boundary/mcp/nativeUiPartialCallRoutesMcp.test.ts index baa844025e..15e8e16a28 100644 --- a/tests/boundary/mcp/nativeUiPartialCallRoutesMcp.test.ts +++ b/tests/boundary/mcp/nativeUiPartialCallRoutesMcp.test.ts @@ -137,7 +137,7 @@ function publicTrace(plan: Plan, maxNodes = 12, maxEdges = 500) { target_sha256: sha256, provider: identity, analysis_profile_digest: null, - result: inspectNativeDispatchMetadata([]), + result: inspectNativeDispatchMetadata([], 1), }, identity, { subject },