diff --git a/docs/mcp-contracts.md b/docs/mcp-contracts.md index b8452922e..7052a0372 100644 --- a/docs/mcp-contracts.md +++ b/docs/mcp-contracts.md @@ -252,7 +252,7 @@ Collections are `documents`, `nodes`, `edges`, and `coverage`; for nodes and edges an optional `document` keeps rows whose evidence names that exact decoded `relative_path`, and an undecoded path is refused. The summary keeps every decoded document, the provider, node-kind, edge-relation and coverage-status -counts, and decoder truncation. Each page repeats `source_truncated`. A complete +counts, and whether the decoded graph is incomplete. Each page repeats `source_truncated`. A complete app graph often exceeds the default MCP response budget (XLD.app: 6.7 MB), so select `detail: "summary"` on the first call. Delivery detail never reaches the decoder or changes the source Evidence ID. diff --git a/docs/native-investigation.md b/docs/native-investigation.md index f0f4d6483..534a0d57e 100644 --- a/docs/native-investigation.md +++ b/docs/native-investigation.md @@ -328,7 +328,7 @@ complete observation. "Contents/Resources/Base.lproj/MainMenu.nib", offset: 0, limit: 64}`. `document` selects rows whose evidence names that exact `documents[].relative_path` (nodes and edges only); `source_truncated` keeps - decoder truncation visible on every page. `verify:interface-builder` compiles + graph incompleteness visible on every page. `verify:interface-builder` compiles a source-owned AppKit XIB with `ibtool`. Storyboards require an installed iOS platform; unsupported archive forms remain explicit. XML plist archives accept UTF-8 and BOM-marked UTF-16 in either byte order. diff --git a/src/domain/analysisView/interfaceBuilderView.test.ts b/src/domain/analysisView/interfaceBuilderView.test.ts index 97d2f375b..a591d3194 100644 --- a/src/domain/analysisView/interfaceBuilderView.test.ts +++ b/src/domain/analysisView/interfaceBuilderView.test.ts @@ -132,7 +132,7 @@ describe("Interface Builder analysis views", () => { items: report().graph.coverage, source_truncated: true, unknowns: expect.arrayContaining([ - expect.stringContaining("stopped at a caller-selected bound"), + expect.stringContaining("The decoded graph is incomplete"), ]), }); }); diff --git a/src/domain/analysisView/interfaceBuilderView.ts b/src/domain/analysisView/interfaceBuilderView.ts index 80f2c6179..8710795a1 100644 --- a/src/domain/analysisView/interfaceBuilderView.ts +++ b/src/domain/analysisView/interfaceBuilderView.ts @@ -188,7 +188,7 @@ export const projectInterfaceBuilderView = ( "Rows describe recognized archive objects and connections only; unrecognized classes, fields and runtime-created UI remain unknown.", ...(graph.truncated ? [ - "The decoder stopped at a caller-selected bound; page exhaustion does not establish absence of undecoded objects or connections.", + "The decoded graph is incomplete (see coverage and limitations for the cause); page exhaustion does not establish absence of undecoded objects or connections.", ] : []), ], diff --git a/tests/boundary/mcp/interfaceBuilderSummaryMcp.test.ts b/tests/boundary/mcp/interfaceBuilderSummaryMcp.test.ts index 2e59336af..b62fcdf53 100644 --- a/tests/boundary/mcp/interfaceBuilderSummaryMcp.test.ts +++ b/tests/boundary/mcp/interfaceBuilderSummaryMcp.test.ts @@ -67,18 +67,7 @@ mcpTest( const app = await fixtureApp(); const client = await openProductionTarget(mcp, onTestFinished, app); - const complete = await client.callTool({ - name: "decode_interface_builder", - arguments: {}, - }); - expect(complete.isError, JSON.stringify(complete.content)).not.toBe(true); - const full = evidenceOf(interfaceBuilderAnalysisSchema).parse( - complete.structuredContent, - ); - expect(full.parameters).not.toHaveProperty("detail"); - const report = full.normalized_result; - expect(report.documents).toHaveLength(2); - + // Summary first: its own retained parent must serve every later page. const summarized = await client.callTool({ name: "decode_interface_builder", arguments: { detail: "summary" }, @@ -89,15 +78,6 @@ mcpTest( const summary = evidenceOf(summarySchema).parse( summarized.structuredContent, ).normalized_result; - // Delivery detail never reaches the decoder or the source Evidence ID. - expect(summary.parent_evidence_id).toBe(full.evidence_id); - expect(summary.summary.documents).toEqual(report.documents); - expect(summary.summary.available_counts).toEqual({ - nodes: report.graph.nodes.length, - edges: report.graph.edges.length, - coverage: report.graph.coverage.length, - }); - const source = { kind: "retained-evidence", evidence_id: summary.parent_evidence_id, @@ -122,16 +102,37 @@ mcpTest( } return rows; }; - expect(await readAll({ collection: "edges" })).toEqual(report.graph.edges); const main = "Contents/Resources/Main.nib"; - const mainNodes = report.graph.nodes.filter(({ evidence }) => + const edges = await readAll({ collection: "edges" }); + const mainNodes = await readAll({ collection: "nodes", document: main }); + + // Only then compare with a complete call made for reference. + const complete = await client.callTool({ + name: "decode_interface_builder", + arguments: {}, + }); + expect(complete.isError, JSON.stringify(complete.content)).not.toBe(true); + const full = evidenceOf(interfaceBuilderAnalysisSchema).parse( + complete.structuredContent, + ); + expect(full.parameters).not.toHaveProperty("detail"); + const report = full.normalized_result; + expect(report.documents).toHaveLength(2); + // Delivery detail never reaches the decoder or the source Evidence ID. + expect(summary.parent_evidence_id).toBe(full.evidence_id); + expect(summary.summary.documents).toEqual(report.documents); + expect(summary.summary.available_counts).toEqual({ + nodes: report.graph.nodes.length, + edges: report.graph.edges.length, + coverage: report.graph.coverage.length, + }); + expect(edges).toEqual(report.graph.edges); + const expectedMainNodes = report.graph.nodes.filter(({ evidence }) => evidence.some(({ artifact_path }) => artifact_path === main), ); - expect(mainNodes.length).toBeGreaterThan(0); - expect(mainNodes.length).toBeLessThan(report.graph.nodes.length); - expect(await readAll({ collection: "nodes", document: main })).toEqual( - mainNodes, - ); + expect(expectedMainNodes.length).toBeGreaterThan(0); + expect(expectedMainNodes.length).toBeLessThan(report.graph.nodes.length); + expect(mainNodes).toEqual(expectedMainNodes); const unknownDocument = await client.callTool({ name: "inspect_analysis_view",