mirror of
https://github.com/morluto/rea.git
synced 2026-11-08 15:56:35 +00:00
fix(apple): derive hierarchy completeness once per document
Include omitted roots, unresolved retained endpoints and serialized NIB omissions in the owning hierarchy status. Reuse that status for summaries and coverage; remove the adapter recomputation and preserve per-document isolation. Validation: 18 graph and 12 real NIB filesystem/built CLI tests pass. Based on #3156. Co-authored-by: Aliciawque <166712943+Aliciawque@users.noreply.github.com>
This commit is contained in:
1 parent
583166785f
commit
7cfcd166e3
3 files changed
+117
-44
No files matched your search
@@ -149,6 +149,9 @@ export const analyzeInterfaceBuilderBundle = async (input: {
|
||||
? "storyboard_scene"
|
||||
: "nib",
|
||||
raw,
|
||||
...(nib === null || nib.omitted === 0
|
||||
? {}
|
||||
: { omittedSerializedHierarchyReferences: nib.omitted }),
|
||||
});
|
||||
} catch (cause: unknown) {
|
||||
if (input.signal?.aborted === true) throw cause;
|
||||
@@ -266,34 +269,14 @@ const finalizeHierarchyCoverage = (
|
||||
...result,
|
||||
documents: result.documents.map((document) => ({
|
||||
...document,
|
||||
// The dictionary parser only sees connectors that were projected, so the
|
||||
// omission recorded where an ownerless connector was dropped has to be
|
||||
// added here for this producer to report the same document count as the
|
||||
// keyed-archive producer for the same recognized connector.
|
||||
connection_count:
|
||||
document.connection_count +
|
||||
(unresolvedConnections.get(document.relative_path) ?? 0),
|
||||
hierarchy_complete:
|
||||
document.hierarchy_complete &&
|
||||
!incompleteHierarchies.has(document.relative_path),
|
||||
})),
|
||||
graph: {
|
||||
...result.graph,
|
||||
coverage: [
|
||||
...result.graph.coverage.map((facet) => {
|
||||
const omittedHierarchy = incompleteHierarchies.get(
|
||||
facet.facet.replace(/^hierarchy:/u, ""),
|
||||
);
|
||||
if (
|
||||
facet.facet.startsWith("hierarchy:") &&
|
||||
omittedHierarchy !== undefined
|
||||
)
|
||||
return {
|
||||
...facet,
|
||||
status: "partial" as const,
|
||||
reason: "serialized_view_hierarchy_incomplete",
|
||||
omitted: facet.omitted + omittedHierarchy,
|
||||
};
|
||||
const omittedConnection = unresolvedConnections.get(
|
||||
facet.facet.replace(/^connections:/u, ""),
|
||||
);
|
||||
@@ -336,7 +319,6 @@ const finalizeHierarchyCoverage = (
|
||||
result.graph.truncated ||
|
||||
counts.omitted > 0 ||
|
||||
archiveDecodePartial ||
|
||||
incompleteHierarchies.size > 0 ||
|
||||
unresolvedConnections.size > 0,
|
||||
},
|
||||
limitations: [
|
||||
|
||||
@@ -360,6 +360,83 @@ describe("Interface Builder graph limits", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("Interface Builder hierarchy completeness", () => {
|
||||
it("marks only the later document incomplete when its hierarchy root is omitted", () => {
|
||||
const document = (relativePath: string, objectId: string) => ({
|
||||
relativePath,
|
||||
archiveSha256: hash,
|
||||
documentKind: "nib" as const,
|
||||
raw: {
|
||||
"com.apple.ibtool.document.objects": {
|
||||
[objectId]: { class: "UIView" },
|
||||
},
|
||||
"com.apple.ibtool.document.hierarchy": [{ objectID: objectId }],
|
||||
},
|
||||
});
|
||||
const result = buildInterfaceBuilderAnalysis({
|
||||
targetSha256: hash,
|
||||
toolVersion: "test",
|
||||
documents: [
|
||||
document("First.nib", "first"),
|
||||
document("Second.nib", "second"),
|
||||
],
|
||||
limits: interfaceBuilderLimitsSchema.parse({ max_objects: 2 }),
|
||||
});
|
||||
|
||||
expect(
|
||||
result.documents.map(({ hierarchy_complete }) => hierarchy_complete),
|
||||
).toEqual([true, false]);
|
||||
expect(result.graph.coverage).toContainEqual(
|
||||
expect.objectContaining({
|
||||
facet: "hierarchy:Second.nib",
|
||||
status: "partial",
|
||||
reason: "graph_limit_reached",
|
||||
omitted: 1,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("marks retained hierarchy edges with unresolved endpoints partial", () => {
|
||||
const result = buildInterfaceBuilderAnalysis({
|
||||
targetSha256: hash,
|
||||
toolVersion: "test",
|
||||
documents: [
|
||||
{
|
||||
relativePath: "Endpoint.nib",
|
||||
archiveSha256: hash,
|
||||
documentKind: "nib",
|
||||
raw: {
|
||||
"com.apple.ibtool.document.objects": {
|
||||
kept: { class: "UIView" },
|
||||
omitted: { class: "UIView" },
|
||||
},
|
||||
"com.apple.ibtool.document.hierarchy": [{ objectID: "omitted" }],
|
||||
},
|
||||
},
|
||||
],
|
||||
limits: interfaceBuilderLimitsSchema.parse({ max_objects: 2 }),
|
||||
});
|
||||
|
||||
expect(result.documents[0]?.hierarchy_complete).toBe(false);
|
||||
expect(result.graph.coverage).toContainEqual(
|
||||
expect.objectContaining({
|
||||
facet: "hierarchy:Endpoint.nib",
|
||||
status: "partial",
|
||||
reason: "graph_limit_reached",
|
||||
omitted: 0,
|
||||
}),
|
||||
);
|
||||
expect(result.graph.edges).toContainEqual(
|
||||
expect.objectContaining({
|
||||
relation: "contains",
|
||||
resolution: "unresolved",
|
||||
to: null,
|
||||
reason: "endpoint_omitted_by_object_limit",
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("keyed archive object references", () => {
|
||||
it("keeps referenced objects as stubs so shared and cyclic references stay finite", () => {
|
||||
// Each view references its window, superview, and next key view, so
|
||||
|
||||
@@ -35,6 +35,8 @@ export interface InterfaceBuilderDocumentInput {
|
||||
readonly archiveSha256: string;
|
||||
readonly documentKind: "nib" | "storyboard_scene";
|
||||
readonly raw: unknown;
|
||||
/** Serialized hierarchy links omitted before normalized graph projection. */
|
||||
readonly omittedSerializedHierarchyReferences?: number;
|
||||
}
|
||||
|
||||
type InvestigationNode = z.infer<typeof nativeInvestigationNodeSchema>;
|
||||
@@ -128,6 +130,7 @@ export const buildInterfaceBuilderAnalysis = (input: {
|
||||
let hierarchyEdgeCount = 0;
|
||||
let documentOmittedConnections = 0;
|
||||
let documentOmittedHierarchyEdges = 0;
|
||||
let hierarchyTruncated = false;
|
||||
const objectNodeId = (objectId: string): string =>
|
||||
graphId("object", [objectId]);
|
||||
const addEdge = (
|
||||
@@ -141,6 +144,7 @@ export const buildInterfaceBuilderAnalysis = (input: {
|
||||
) {
|
||||
truncated = true;
|
||||
if (isHierarchy) {
|
||||
hierarchyTruncated = true;
|
||||
documentOmittedHierarchyEdges += 1;
|
||||
} else {
|
||||
omittedConnections += 1;
|
||||
@@ -160,9 +164,11 @@ export const buildInterfaceBuilderAnalysis = (input: {
|
||||
description: string,
|
||||
reason?: string,
|
||||
) => {
|
||||
const isHierarchy = relation === "contains" && id.includes(":hierarchy:");
|
||||
if (!nodes.has(from)) {
|
||||
truncated = true;
|
||||
if (relation === "contains" && id.includes(":hierarchy:")) {
|
||||
if (isHierarchy) {
|
||||
hierarchyTruncated = true;
|
||||
documentOmittedHierarchyEdges += 1;
|
||||
} else {
|
||||
omittedConnections += 1;
|
||||
@@ -172,7 +178,10 @@ export const buildInterfaceBuilderAnalysis = (input: {
|
||||
}
|
||||
const evidence = evidenceFor(description);
|
||||
const unresolvedEndpoint = to !== null && !nodes.has(to);
|
||||
if (unresolvedEndpoint) truncated = true;
|
||||
if (unresolvedEndpoint) {
|
||||
truncated = true;
|
||||
if (isHierarchy) hierarchyTruncated = true;
|
||||
}
|
||||
const edgeTo = unresolvedEndpoint ? null : to;
|
||||
const edgeReason = unresolvedEndpoint
|
||||
? "endpoint_omitted_by_object_limit"
|
||||
@@ -198,11 +207,10 @@ export const buildInterfaceBuilderAnalysis = (input: {
|
||||
evidence,
|
||||
limitations: [],
|
||||
},
|
||||
relation === "contains" && id.includes(":hierarchy:"),
|
||||
isHierarchy,
|
||||
);
|
||||
};
|
||||
let hierarchyCount = 0;
|
||||
let hierarchyTruncated = false;
|
||||
const visitHierarchy = (
|
||||
initial: unknown,
|
||||
initialParentId: string,
|
||||
@@ -405,7 +413,10 @@ export const buildInterfaceBuilderAnalysis = (input: {
|
||||
const objectsTruncated =
|
||||
parsed.omittedObjects > 0 || parsed.objects.length > documentObjectCount;
|
||||
const parserOmittedHierarchyReferences = parsed.omittedHierarchyReferences;
|
||||
omittedHierarchyReferences += parserOmittedHierarchyReferences;
|
||||
const omittedSerializedHierarchyReferences =
|
||||
document.omittedSerializedHierarchyReferences ?? 0;
|
||||
omittedHierarchyReferences +=
|
||||
parserOmittedHierarchyReferences + omittedSerializedHierarchyReferences;
|
||||
const parserOmittedConnections = parsed.omittedConnections;
|
||||
const connectionsTruncated =
|
||||
parserOmittedConnections > 0 ||
|
||||
@@ -421,6 +432,15 @@ export const buildInterfaceBuilderAnalysis = (input: {
|
||||
Math.max(0, parsed.connections.length - input.limits.max_connections) +
|
||||
parserOmittedConnections +
|
||||
parsed.unresolvedConnections;
|
||||
const hierarchyStatus =
|
||||
parserOmittedHierarchyReferences > 0 ||
|
||||
omittedSerializedHierarchyReferences > 0
|
||||
? "partial"
|
||||
: parsed.hierarchy.length === 0
|
||||
? "unsupported"
|
||||
: hierarchyTruncated
|
||||
? "partial"
|
||||
: "complete";
|
||||
summaries.push({
|
||||
relative_path: document.relativePath,
|
||||
archive_sha256: document.archiveSha256,
|
||||
@@ -430,10 +450,7 @@ export const buildInterfaceBuilderAnalysis = (input: {
|
||||
parsed.connections.length +
|
||||
parserOmittedConnections +
|
||||
parsed.unresolvedConnections,
|
||||
hierarchy_complete:
|
||||
parsed.hierarchy.length > 0 &&
|
||||
!hierarchyTruncated &&
|
||||
parserOmittedHierarchyReferences === 0,
|
||||
hierarchy_complete: hierarchyStatus === "complete",
|
||||
});
|
||||
coverage.push(
|
||||
{
|
||||
@@ -474,25 +491,22 @@ export const buildInterfaceBuilderAnalysis = (input: {
|
||||
},
|
||||
{
|
||||
facet: `hierarchy:${document.relativePath}`,
|
||||
status:
|
||||
parserOmittedHierarchyReferences > 0
|
||||
? "partial"
|
||||
: parsed.hierarchy.length === 0
|
||||
? "unsupported"
|
||||
: hierarchyTruncated || documentOmittedHierarchyEdges > 0
|
||||
? "partial"
|
||||
: "complete",
|
||||
status: hierarchyStatus,
|
||||
reason:
|
||||
parserOmittedHierarchyReferences > 0
|
||||
? "archive_hierarchy_references_missing"
|
||||
: parsed.hierarchy.length === 0
|
||||
? "archive_hierarchy_not_decoded"
|
||||
: hierarchyTruncated || documentOmittedHierarchyEdges > 0
|
||||
? "graph_limit_reached"
|
||||
: null,
|
||||
: omittedSerializedHierarchyReferences > 0
|
||||
? "serialized_view_hierarchy_incomplete"
|
||||
: parsed.hierarchy.length === 0
|
||||
? "archive_hierarchy_not_decoded"
|
||||
: hierarchyTruncated
|
||||
? "graph_limit_reached"
|
||||
: null,
|
||||
examined: hierarchyCount,
|
||||
omitted:
|
||||
documentOmittedHierarchyEdges + parserOmittedHierarchyReferences,
|
||||
documentOmittedHierarchyEdges +
|
||||
parserOmittedHierarchyReferences +
|
||||
omittedSerializedHierarchyReferences,
|
||||
},
|
||||
{
|
||||
facet: `classes:${document.relativePath}`,
|
||||
|
||||
Reference in new issue
Block a user