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 <noreply@anthropic.com>
This commit is contained in:
Kaoru0822 2026-10-13 07:46:42 +00:00 committed by morluto
parent f993d33028
commit da49ea2692
5 changed files with 33 additions and 32 deletions

View File

@ -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.

View File

@ -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.

View File

@ -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"),
]),
});
});

View File

@ -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.",
]
: []),
],

View File

@ -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",