From 23e68cbdfb4709b3bc83fafc9f1aaed754e61729 Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Fri, 23 Oct 2026 19:29:41 +0800 Subject: [PATCH] fix(macho): preserve import identity in retained views Use distinct row contracts for imports and exports, retaining ambiguous names and historical import defaults without adding import-only fields to export pages. Verify the SDK-advertised schemas and both facets through MCP. --- src/domain/analysisView/machoView.test.ts | 23 +++++ src/domain/analysisView/machoView.ts | 3 +- .../mcp/nativeMachoSummaryMcp.test.ts | 99 +++++++++++++++++++ 3 files changed, 124 insertions(+), 1 deletion(-) diff --git a/src/domain/analysisView/machoView.test.ts b/src/domain/analysisView/machoView.test.ts index 2018438be7..b106c9e93b 100644 --- a/src/domain/analysisView/machoView.test.ts +++ b/src/domain/analysisView/machoView.test.ts @@ -107,6 +107,29 @@ const parent = ( limitations: ["Parent limitation."], }); +it("preserves ambiguous import facts without adding import fields to exports", () => { + const imported = { + ...symbol("unused"), + name: null, + raw_name: "_value+0x4", + name_status: "ambiguous", + addend: null, + }; + const observed = { ...report, imports: coverage([imported], false) }; + for (const facet of ["imports", "exports"] as const) { + const projected = projectAnalysisView(parent(observed), { + kind: "macho", + facet, + offset: 0, + limit: 64, + }); + if (!projected.ok) throw projected.error; + expect(projected.value.kind === "macho" && projected.value.item).toEqual( + observed[facet].items, + ); + } +}); + describe("Mach-O analysis views", () => { it("summarizes slice identity and collection counts without rows", () => { const projected = projectAnalysisView(parent(), { kind: "summary" }); diff --git a/src/domain/analysisView/machoView.ts b/src/domain/analysisView/machoView.ts index 29e56ebbd9..a5d22c2cc2 100644 --- a/src/domain/analysisView/machoView.ts +++ b/src/domain/analysisView/machoView.ts @@ -51,7 +51,8 @@ export const machoViewItemSchema = z.union([ exactRows(shape.build_metadata.shape.items), exactRows(shape.load_commands.shape.items), exactRows(shape.dependencies.shape.items), - // Imports and exports share one row shape. + // Exports must precede imports, whose historical defaults also accept export keys. + exactRows(shape.exports.shape.items), exactRows(shape.imports.shape.items), exactRows(shape.segments.shape.items), exactRows(shape.provenance), diff --git a/tests/boundary/mcp/nativeMachoSummaryMcp.test.ts b/tests/boundary/mcp/nativeMachoSummaryMcp.test.ts index 1a2f29902b..8ae846b006 100644 --- a/tests/boundary/mcp/nativeMachoSummaryMcp.test.ts +++ b/tests/boundary/mcp/nativeMachoSummaryMcp.test.ts @@ -14,6 +14,8 @@ import { parseMcpToolError } from "../../fixtures/mcpToolError.js"; import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js"; import { mcpTest } from "../../support/mcp/mcpFixture.js"; import { openProductionTarget } from "../../support/mcp/productionSession.js"; +import { createEvidence, parseEvidence } from "../../../src/domain/evidence.js"; +import { advertisedMcpToolSchemas } from "../../fixtures/advertisedMcpTool.js"; const evidenceOf = (schema: Schema) => z.object({ @@ -41,6 +43,103 @@ const pageSchema = z.object({ coverage: z.object({ next_offset: z.number().nullable() }), }); +mcpTest( + "pages ambiguous imports and exact exports under the advertised MCP schema", + async ({ mcp, onTestFinished }) => { + const empty = { items: [], total: 0, exhaustive: true, limitations: [] }; + const imported = { + name: null, + raw_name: "_value+0x4", + name_status: "ambiguous", + addend: null, + address: null, + weak: null, + reexport: null, + source: "libFixture", + }; + const exported = { + name: "_value", + address: "0x1000", + weak: null, + reexport: null, + source: null, + }; + const report = inspectMachoSchema.parse({ + format: "mach-o", + selected_architecture: { name: "arm64", container_file_offset: 0 }, + endian: "little", + word_size: 64, + file_type: "MH_DYLIB", + flags: [], + uuid: null, + entrypoints: empty, + architectures: empty, + build_metadata: empty, + load_commands: empty, + dependencies: empty, + imports: { ...empty, items: [imported], total: 1 }, + exports: { ...empty, items: [exported], total: 1 }, + segments: empty, + provenance: [], + limitations: [], + }); + const source = createEvidence( + undefined, + { id: "fixture", name: "Fixture", version: "1" }, + { + operation: "inspect_macho", + parameters: {}, + result: report, + }, + ); + const session = createTestBinarySession(() => { + throw new Error("Imported views must not start a provider"); + }); + onTestFinished(async () => { + await session.close(); + }); + const client = await mcp.connect( + createServer( + { kind: "session", session }, + { + environment: { ...process.env, REA_MCP_INPUT_SCHEMA_PROFILE: "full" }, + }, + ), + ); + const schemas = await advertisedMcpToolSchemas( + client, + "inspect_analysis_view", + ); + for (const [facet, expected] of [ + ["imports", imported], + ["exports", exported], + ] as const) { + const request = { + source: { kind: "inline", evidence: source }, + view: { kind: "macho", facet }, + }; + expect( + schemas.ajv.validate(schemas.inputSchema, request), + JSON.stringify(schemas.ajv.errors), + ).toBe(true); + const reply = await client.callTool({ + name: "inspect_analysis_view", + arguments: request, + }); + expect(reply.isError, JSON.stringify(reply.content)).not.toBe(true); + expect( + schemas.ajv.validate(schemas.outputSchema, reply.structuredContent), + JSON.stringify(schemas.ajv.errors), + ).toBe(true); + expect( + pageSchema.parse( + parseEvidence(reply.structuredContent).normalized_result, + ).item, + ).toEqual([expected]); + } + }, +); + describe.skipIf(process.platform !== "darwin")( "Mach-O summary delivery through MCP", () => {