diff --git a/docs/native-investigation.md b/docs/native-investigation.md index ebcf02774..6d0dffc33 100644 --- a/docs/native-investigation.md +++ b/docs/native-investigation.md @@ -398,6 +398,10 @@ complete observation. coroutine tables, and inherited overrides, have explicit unsupported or partial coverage. Other providers retain the existing symbol-based inventory with its narrower coverage. + Symbols without a reported location remain in that inventory: Swift symbol + `address` and Objective-C `implementation_address` are null. An empty provider + address is retained as `reported_address` in the symbol's evidence, whose + location also remains unknown. Nonempty provider address syntax is preserved. ## Native desktop observation diff --git a/src/domain/native/objcSwiftMetadata.test.ts b/src/domain/native/objcSwiftMetadata.test.ts index 2babf2161..b7b475097 100644 --- a/src/domain/native/objcSwiftMetadata.test.ts +++ b/src/domain/native/objcSwiftMetadata.test.ts @@ -74,4 +74,43 @@ 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" }, + ]); + expect(result.swift_symbols).toMatchObject([ + { + mangled_name: "$s4Test7MyClassC", + address: null, + evidence: [{ location: { address: null }, reported_address: "" }], + }, + { mangled_name: "$s5Store7launchyyF", address: "ram:0x3000" }, + ]); + expect(result.swift_symbols[1]?.evidence[0]).not.toHaveProperty( + "reported_address", + ); + expect(result.objc_dispatch_implementations).toMatchObject([ + { + class_name: "StoreController", + implementation_address: null, + location: { address: null }, + evidence: [{ location: { address: null }, reported_address: "" }], + }, + ]); + expect(result.objc_classes[0]?.methods[0]).toMatchObject({ + selector: "buildTapped:", + address: null, + }); + expect(result.coverage).toContainEqual( + expect.objectContaining({ + facet: "swift_declarations", + status: "partial", + examined: 2, + decoded: 0, + }), + ); + }); }); diff --git a/src/domain/native/objcSwiftMetadata.ts b/src/domain/native/objcSwiftMetadata.ts index 6c75b9b1f..89afb599e 100644 --- a/src/domain/native/objcSwiftMetadata.ts +++ b/src/domain/native/objcSwiftMetadata.ts @@ -124,6 +124,8 @@ export const nativeMetadataEvidenceSchema = z.strictObject({ description: z.string().min(1), provider: providerSchema.optional(), location: nativeMetadataLocationSchema, + /** Preserve a provider's source value when it does not establish a location. */ + reported_address: z.string().optional(), artifact_path: z.string().min(1).nullable().default(null), artifact_sha256: z .string() @@ -229,7 +231,7 @@ export const relativePointerSchema = z.strictObject({ /** A raw Swift-mangled symbol when a provider has not decoded its metadata. */ export const swiftSymbolSchema = z.strictObject({ mangled_name: z.string().min(1), - address: z.string().min(1), + address: z.string().min(1).nullable(), decode: nativeMetadataDecodeSchema, evidence: z.array(nativeMetadataEvidenceSchema).min(1), }); @@ -338,11 +340,15 @@ export const inspectNativeDispatchMetadata = ( names: readonly { readonly address: string; readonly name: string }[], maxRecords = 5_000, ): ObjcSwiftMetadata => { - const symbolEvidence = (name: string, address: string) => [ + const symbolEvidence = ( + entry: (typeof names)[number], + address: string | null, + ) => [ { kind: "symbol" as const, - description: `Provider symbol ${name}`, + description: `Provider symbol ${entry.name}`, location: { address, file_offset: null }, + ...(address === null ? { reported_address: entry.address } : {}), artifact_path: null, artifact_sha256: null, }, @@ -353,6 +359,7 @@ export const inspectNativeDispatchMetadata = ( const examinedNames = names.slice(0, maxRecords); for (const entry of examinedNames) { + const address = entry.address === "" ? null : entry.address; const classMatch = /(?:OBJC_CLASS_|OBJC_METACLASS_)\$_([^\s]+)/u.exec( entry.name, ); @@ -366,10 +373,10 @@ export const inspectNativeDispatchMetadata = ( class_name: methodMatch[2], selector, method_type: methodType, - implementation_address: entry.address, - location: { address: entry.address, file_offset: null }, + implementation_address: address, + location: { address, file_offset: null }, decode: { status: "partial", reason: "symbol_name_only" }, - evidence: symbolEvidence(entry.name, entry.address), + evidence: symbolEvidence(entry, address), }); classNames.add(methodMatch[2]); } @@ -377,9 +384,9 @@ export const inspectNativeDispatchMetadata = ( if (isSwiftMangledSymbolName(entry.name) || entry.name.startsWith("_Tt")) swiftSymbols.push({ mangled_name: entry.name, - address: entry.address, + address, decode: { status: "partial", reason: "mangled_symbol_not_demangled" }, - evidence: symbolEvidence(entry.name, entry.address), + evidence: symbolEvidence(entry, address), }); } diff --git a/tests/boundary/mcp/enhancedTools.01-lists-the-complete-target-open.test.ts b/tests/boundary/mcp/enhancedTools.01-lists-the-complete-target-open.test.ts index add15a6b2..22ace43d7 100644 --- a/tests/boundary/mcp/enhancedTools.01-lists-the-complete-target-open.test.ts +++ b/tests/boundary/mcp/enhancedTools.01-lists-the-complete-target-open.test.ts @@ -425,3 +425,45 @@ describe("enhanced MCP tools", () => { }); }); }); + +it("retains unlocated provider symbols through the advertised native dispatch MCP contract", async () => { + const ordinary = fixturePort(); + const client = await connect({ + execute: (operation, parameters, options) => + operation === "list_names" + ? Promise.resolve( + targetObservation([ + { address: "", value: "$s4Test7MyClassC" }, + { address: "", value: "-[Fixture buildTapped:]" }, + ]), + ) + : ordinary.execute(operation, parameters, options), + }); + const response = await client.callTool({ + name: "inspect_native_dispatch_metadata", + arguments: { max_records: 100 }, + }); + expect(response.isError).not.toBe(true); + expect(jsonResult(response)).toMatchObject({ + provider: { id: "fixture" }, + target_sha256: "a".repeat(64), + result: { + swift_symbols: [ + { + mangled_name: "$s4Test7MyClassC", + address: null, + evidence: [{ reported_address: "", location: { address: null } }], + }, + ], + objc_dispatch_implementations: [ + { implementation_address: null, evidence: [{ reported_address: "" }] }, + ], + objc_classes: [ + { + name: "Fixture", + methods: [{ selector: "buildTapped:", address: null }], + }, + ], + }, + }); +});