From da49ea26928e49ee22dc45ceb690720a522661cb Mon Sep 17 00:00:00 2001 From: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com> Date: Tue, 13 Oct 2026 07:46:42 +0000 Subject: [PATCH] fix(artifacts): describe incomplete Interface Builder graphs accurately graph.truncated is also set for invalid archives, omitted dictionary keys and fixed decode budgets, not only caller-selected limits, so the view's unknown now points at coverage and limitations for the cause. The MCP test requests the summary first and reads its retained pages before any complete call, covering the flow where complete delivery cannot fit. Co-Authored-By: Claude Opus 5.5 --- docs/mcp-contracts.md | 2 +- docs/native-investigation.md | 2 +- .../analysisView/interfaceBuilderView.test.ts | 2 +- .../analysisView/interfaceBuilderView.ts | 2 +- .../mcp/interfaceBuilderSummaryMcp.test.ts | 57 ++++++++++--------- 5 files changed, 33 insertions(+), 32 deletions(-) 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",