[PR #2484] [MERGED] fix(apple): report a recognized connector that cannot name its owner #5262

Closed
opened 2026-10-31 01:05:15 +00:00 by bsakaguchi · 0 comments
Owner

📋 Pull Request Information

Original PR: https://github.com/morluto/rea/pull/2484
Author: @XiaoBinGan
Created: 10/13/2026
Status: ✅ Merged
Merged: 10/13/2026
Merged by: @morluto

Base: main ← Head: fix/apple-unresolved-connector-coverage


📝 Commits (2)

  • 146e7bc fix(apple): report a connector that cannot name its owner as an omission
  • 06051a1 fix(apple): count ownerless NIBArchive connectors and keep the limit reason

📊 Changes

5 files changed (+315 additions, -12 deletions)

View changed files

📝 src/artifacts/apple/InterfaceBuilderAnalysis.ts (+50 -6)
📝 src/domain/apple/interfaceBuilderGraph.test.ts (+100 -0)
📝 src/domain/apple/interfaceBuilderGraph.ts (+18 -5)
📝 src/domain/apple/interfaceBuilderKeyedArchive.ts (+11 -1)
📝 tests/boundary/filesystem/interfaceBuilderBundle.test.ts (+136 -0)

📄 Description

Follow-up to #2177, which fixed the UIKit keyed-archive field decode but left the omission accounting for recognized connectors untouched.

Summary

A connector is recognized by class name, and then dropped by an early continue when its owner cannot be resolved. Nothing recorded that drop, so the document summary reported connection_count: 0 and the connections:<path> coverage facet still derived complete with examined: 0 and omitted: 0. A connector that was present, recognized and discarded was reported as absent, with no limitation and no partial coverage.

An unresolved source cannot be represented as a connection at all: connection.source_id is a non-empty string and the outlet's contains edge requires a source node. That is unlike an unresolved destination, which the graph already retains as an unresolved edge with its own reason. So the only truthful representation is an omission count, which did not exist.

Problem and expected behavior

For a rooted archive holding one UIRuntimeOutletConnection that carries UIDestination and UILabel but no UISource, the public CLI on current main reports:

{
  "connection_count": 0,
  "outlets": [],
  "coverage": [
    { "facet": "connections:Main.nib", "status": "complete", "reason": null, "examined": 0, "omitted": 0 }
  ],
  "graph_truncated": false
}

parseKeyedArchive resolves the source, finds null and continues without recording anything; omittedConnections is only incremented at the 40 000-connection cap, so buildInterfaceBuilderAnalysis sees no connections and no omissions and derives completeness from that.

Expected: the connector is reported as an omission, and connection coverage is partial with a reason that names the actual cause. connection_limit_reached must stay reserved for connections dropped by the hard cap.

Change and scope

Both producers of that coverage contract are affected and are changed together, because they deliver the same facet and must agree on the reason value.

Keyed-archive parser. parseKeyedArchive counts a recognized connector whose owner cannot be resolved in a new unresolvedConnections field on the parser result, alongside the existing omittedConnections. The flat ibtool dictionary branch, which has no connector resolution step, reports unresolvedConnections: 0. buildInterfaceBuilderAnalysis adds it to the facet's omitted total and to the graph's omission total, and drives status: "partial" with reason: "connection_unresolved".

NIBArchive projection. projectNibArchive filters connector classes out of the projected objects map and re-emits them under source keys, so a connector with an unresolvable owner disappeared from the projected dictionary entirely — the count has to be taken where the connector is discarded, because no later consumer can find a trace of it. It is plumbed through the same per-document omission channel that already carries incomplete hierarchies in finalizeHierarchyCoverage, which maps it onto the same connections:<path> facet with the same reason and adds it to the graph's truncated flag.

The value is also added to the document summary connection_count. That field already counts connectors the parser recognized but could not emit, because connections dropped by the hard cap are counted there too; unresolved connectors follow the existing rule rather than a new one.

No schema change: reason already accepts a nullable string. No decoder change: the UIKit field decode from #2177 is untouched, and serialized_view_hierarchy_incomplete keeps its existing meaning.

Contract and boundary impact

  • Semantic owner and earliest changed stage: connector omission accounting in the keyed-archive domain parser and in the NIBArchive artifact projection, and coverage derivation in buildInterfaceBuilderAnalysis. The NIBArchive decoder, the view-hierarchy projection and the CLI/MCP surface are untouched.
  • CLI and MCP/tool-catalog contract: tool and command names, inputs, output schemas and error codes are unchanged. Only the values of connection_count, the connections:<path> coverage facet, graph.truncated and the returned limitations change, and only for archives that hold a recognized ownerless connector.
  • Provider, target-format or platform compatibility: keyed archives and NIBArchive documents. Archives whose connectors all resolve keep complete coverage with unchanged counts and unchanged limitations; connections dropped by the hard cap keep connection_limit_reached; hierarchy omissions keep serialized_view_hierarchy_incomplete.
  • Evidence, artifact, provenance or reconstruction contract: the change adds no Evidence and no node or edge. It records an omission that was previously unrecorded, so a consumer can no longer read a complete connections facet for a document whose connector was discarded. Artifact bytes are unmodified.
  • Process execution, authorization, cleanup or containment impact: none. Static decoding of caller-supplied bytes.
  • Generated metadata, package or installation impact: none.

Evidence and regression coverage

A new keyed archive unresolved connectors block builds a rooted UIKit archive with UINibConnectionsKey / UINibObjectsKey / UINibTopLevelObjectsKey roots and CF$UID references. A new case in the existing NIBArchive UIKit event connections block drives the same input shape through encodeNibArchiveFixture and the bundle reader.

Each producer is covered by a positive and a negative case, so the new reason cannot swallow ordinary archives:

  • the resolvable keyed-archive control asserts unresolvedConnections is 0 and coverage stays complete with examined: 1 and omitted: 0;
  • the ownerless keyed-archive case asserts unresolvedConnections is 1, that no outlet node is fabricated, and coverage is partial / connection_unresolved with examined: 0, omitted: 1 and truncated: true;
  • the ownerless NIBArchive case asserts the same facet and reason through the public bundle reader, plus the added limitation and truncated: true.

I confirmed each half is independently required by reverting them in turn: removing the keyed-archive parser counter fails with expected +0 to be 1, pinning the domain coverage flag off fails with expected [ … ] to deep equally contain ObjectContaining{…}, and removing the projection counter fails the NIBArchive case with the same shape of assertion error.

Verification performed on macOS 26.5.1 arm64 with Node.js 24.18.0 and npm 11.16.0, on a tree rebased onto a6efa4f:

  • npm run test:focused over the Interface Builder graph, the NIBArchive decoder, the view-hierarchy projection, the UIKit keyed-archive acceptance lane, the Interface Builder bundle, keyed-archive selection and the asset-catalog suites: 98 passed, 2 skipped, 0 failed.
  • npm run check:fast: passed.
  • npm run verify:test-discovery: all 1013 test files discovered exactly once.
  • The public CLI against the archive described above: connection_count: 0, complete, examined: 0, omitted: 0, truncated: false before the change; connection_count: 1, partial, connection_unresolved, examined: 0, omitted: 1, truncated: true after it.

Unavailable coverage: no Windows or Linux run, no Xcode ibtool lane and no verify:interface-builder run (this host selects only CommandLineTools), and no native encoder output — both regression fixtures are constructed archives. The change is platform-independent parser and projection logic; the recorded ibtool-dependent cases are unaffected by it.


🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.

## 📋 Pull Request Information **Original PR:** https://github.com/morluto/rea/pull/2484 **Author:** [@XiaoBinGan](https://github.com/XiaoBinGan) **Created:** 10/13/2026 **Status:** ✅ Merged **Merged:** 10/13/2026 **Merged by:** [@morluto](https://github.com/morluto) **Base:** `main` ← **Head:** `fix/apple-unresolved-connector-coverage` --- ### 📝 Commits (2) - [`146e7bc`](https://github.com/morluto/rea/commit/146e7bc524f87db745b44487ef477320c7cc47af) fix(apple): report a connector that cannot name its owner as an omission - [`06051a1`](https://github.com/morluto/rea/commit/06051a11e054a5e53c55d91b92b73d73cc4aa9f2) fix(apple): count ownerless NIBArchive connectors and keep the limit reason ### 📊 Changes **5 files changed** (+315 additions, -12 deletions) <details> <summary>View changed files</summary> 📝 `src/artifacts/apple/InterfaceBuilderAnalysis.ts` (+50 -6) 📝 `src/domain/apple/interfaceBuilderGraph.test.ts` (+100 -0) 📝 `src/domain/apple/interfaceBuilderGraph.ts` (+18 -5) 📝 `src/domain/apple/interfaceBuilderKeyedArchive.ts` (+11 -1) 📝 `tests/boundary/filesystem/interfaceBuilderBundle.test.ts` (+136 -0) </details> ### 📄 Description Follow-up to #2177, which fixed the UIKit keyed-archive field decode but left the omission accounting for recognized connectors untouched. ## Summary A connector is recognized by class name, and then dropped by an early `continue` when its owner cannot be resolved. Nothing recorded that drop, so the document summary reported `connection_count: 0` and the `connections:<path>` coverage facet still derived `complete` with `examined: 0` and `omitted: 0`. A connector that was present, recognized and discarded was reported as absent, with no limitation and no partial coverage. An unresolved source cannot be represented as a connection at all: `connection.source_id` is a non-empty string and the outlet's `contains` edge requires a source node. That is unlike an unresolved destination, which the graph already retains as an `unresolved` edge with its own reason. So the only truthful representation is an omission count, which did not exist. ## Problem and expected behavior For a rooted archive holding one `UIRuntimeOutletConnection` that carries `UIDestination` and `UILabel` but no `UISource`, the public CLI on current `main` reports: ```json { "connection_count": 0, "outlets": [], "coverage": [ { "facet": "connections:Main.nib", "status": "complete", "reason": null, "examined": 0, "omitted": 0 } ], "graph_truncated": false } ``` [`parseKeyedArchive`](https://github.com/morluto/rea/blob/8d4d631/src/domain/apple/interfaceBuilderKeyedArchive.ts) resolves the source, finds `null` and continues without recording anything; [`omittedConnections`](https://github.com/morluto/rea/blob/8d4d631/src/domain/apple/interfaceBuilderKeyedArchive.ts) is only incremented at the 40 000-connection cap, so [`buildInterfaceBuilderAnalysis`](https://github.com/morluto/rea/blob/8d4d631/src/domain/apple/interfaceBuilderGraph.ts) sees no connections and no omissions and derives completeness from that. Expected: the connector is reported as an omission, and connection coverage is partial with a reason that names the actual cause. `connection_limit_reached` must stay reserved for connections dropped by the hard cap. ## Change and scope Both producers of that coverage contract are affected and are changed together, because they deliver the same facet and must agree on the reason value. **Keyed-archive parser.** [`parseKeyedArchive`](https://github.com/XiaoBinGan/rea/blob/8db930f/src/domain/apple/interfaceBuilderKeyedArchive.ts) counts a recognized connector whose owner cannot be resolved in a new `unresolvedConnections` field on the parser result, alongside the existing `omittedConnections`. The flat `ibtool` dictionary branch, which has no connector resolution step, reports `unresolvedConnections: 0`. [`buildInterfaceBuilderAnalysis`](https://github.com/XiaoBinGan/rea/blob/8db930f/src/domain/apple/interfaceBuilderGraph.ts) adds it to the facet's `omitted` total and to the graph's omission total, and drives `status: "partial"` with `reason: "connection_unresolved"`. **NIBArchive projection.** [`projectNibArchive`](https://github.com/XiaoBinGan/rea/blob/8db930f/src/artifacts/apple/InterfaceBuilderAnalysis.ts) filters connector classes out of the projected `objects` map and re-emits them under source keys, so a connector with an unresolvable owner disappeared from the projected dictionary entirely — the count has to be taken where the connector is discarded, because no later consumer can find a trace of it. It is plumbed through the same per-document omission channel that already carries incomplete hierarchies in `finalizeHierarchyCoverage`, which maps it onto the same `connections:<path>` facet with the same reason and adds it to the graph's `truncated` flag. The value is also added to the document summary `connection_count`. That field already counts connectors the parser recognized but could not emit, because connections dropped by the hard cap are counted there too; unresolved connectors follow the existing rule rather than a new one. No schema change: `reason` already accepts a nullable string. No decoder change: the UIKit field decode from #2177 is untouched, and `serialized_view_hierarchy_incomplete` keeps its existing meaning. ## Contract and boundary impact - **Semantic owner and earliest changed stage:** connector omission accounting in the keyed-archive domain parser and in the NIBArchive artifact projection, and coverage derivation in `buildInterfaceBuilderAnalysis`. The NIBArchive decoder, the view-hierarchy projection and the CLI/MCP surface are untouched. - **CLI and MCP/tool-catalog contract:** tool and command names, inputs, output schemas and error codes are unchanged. Only the values of `connection_count`, the `connections:<path>` coverage facet, `graph.truncated` and the returned limitations change, and only for archives that hold a recognized ownerless connector. - **Provider, target-format or platform compatibility:** keyed archives and NIBArchive documents. Archives whose connectors all resolve keep `complete` coverage with unchanged counts and unchanged limitations; connections dropped by the hard cap keep `connection_limit_reached`; hierarchy omissions keep `serialized_view_hierarchy_incomplete`. - **Evidence, artifact, provenance or reconstruction contract:** the change adds no Evidence and no node or edge. It records an omission that was previously unrecorded, so a consumer can no longer read a complete connections facet for a document whose connector was discarded. Artifact bytes are unmodified. - **Process execution, authorization, cleanup or containment impact:** none. Static decoding of caller-supplied bytes. - **Generated metadata, package or installation impact:** none. ## Evidence and regression coverage A new [`keyed archive unresolved connectors`](https://github.com/XiaoBinGan/rea/blob/8db930f/src/domain/apple/interfaceBuilderGraph.test.ts) block builds a rooted UIKit archive with `UINibConnectionsKey` / `UINibObjectsKey` / `UINibTopLevelObjectsKey` roots and `CF$UID` references. A new case in the existing [`NIBArchive UIKit event connections`](https://github.com/XiaoBinGan/rea/blob/8db930f/tests/boundary/filesystem/interfaceBuilderBundle.test.ts) block drives the same input shape through `encodeNibArchiveFixture` and the bundle reader. Each producer is covered by a positive and a negative case, so the new reason cannot swallow ordinary archives: - the resolvable keyed-archive control asserts `unresolvedConnections` is 0 and coverage stays `complete` with `examined: 1` and `omitted: 0`; - the ownerless keyed-archive case asserts `unresolvedConnections` is 1, that no outlet node is fabricated, and coverage is `partial` / `connection_unresolved` with `examined: 0`, `omitted: 1` and `truncated: true`; - the ownerless NIBArchive case asserts the same facet and reason through the public bundle reader, plus the added limitation and `truncated: true`. I confirmed each half is independently required by reverting them in turn: removing the keyed-archive parser counter fails with `expected +0 to be 1`, pinning the domain coverage flag off fails with `expected [ … ] to deep equally contain ObjectContaining{…}`, and removing the projection counter fails the NIBArchive case with the same shape of assertion error. Verification performed on macOS 26.5.1 arm64 with Node.js 24.18.0 and npm 11.16.0, on a tree rebased onto `a6efa4f`: - `npm run test:focused` over the Interface Builder graph, the NIBArchive decoder, the view-hierarchy projection, the UIKit keyed-archive acceptance lane, the Interface Builder bundle, keyed-archive selection and the asset-catalog suites: 98 passed, 2 skipped, 0 failed. - `npm run check:fast`: passed. - `npm run verify:test-discovery`: all 1013 test files discovered exactly once. - The public CLI against the archive described above: `connection_count: 0`, `complete`, `examined: 0`, `omitted: 0`, `truncated: false` before the change; `connection_count: 1`, `partial`, `connection_unresolved`, `examined: 0`, `omitted: 1`, `truncated: true` after it. Unavailable coverage: no Windows or Linux run, no Xcode `ibtool` lane and no `verify:interface-builder` run (this host selects only CommandLineTools), and no native encoder output — both regression fixtures are constructed archives. The change is platform-independent parser and projection logic; the recorded `ibtool`-dependent cases are unaffected by it. --- <sub>🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.</sub>
bsakaguchi 2026-10-31 01:05:15 +00:00
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
morluto/rea#5262
No description provided.