[PR #4162] [MERGED] fix(apple): count Interface Builder hierarchy records beyond the object limit #6101

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

📋 Pull Request Information

Original PR: https://github.com/morluto/rea/pull/4162
Author: @kaoru0822-kitauji
Created: 10/15/2026
Status: ✅ Merged
Merged: 10/15/2026
Merged by: @kaoru0822-kitauji

Base: main ← Head: fix/3950-ib-hierarchy-limit


📝 Commits (1)

  • 6ec86b0 fix(apple): count Interface Builder hierarchy records beyond the object limit

📊 Changes

3 files changed (+97 additions, -7 deletions)

View changed files

📝 src/domain/apple/interfaceBuilderGraph.test.ts (+87 -0)
📝 src/domain/apple/interfaceBuilderGraph.ts (+9 -6)
📝 src/domain/apple/interfaceBuilderKeyedArchive.ts (+1 -1)

📄 Description

Summary

Fixes #3950. In an ibtool dictionary nib, an identified hierarchy record that came after the first 20,000 hierarchy entries was dropped. The document still reported complete hierarchy coverage and an untruncated graph. The record and its containment edge are now kept. When max_objects does omit identified hierarchy records, the hierarchy facet reports them as partial, with an exact omitted count.

Problem and expected behavior

parseInterfaceBuilderRecords (src/domain/apple/interfaceBuilderKeyedArchive.ts) sliced the ibtool hierarchy array to a hard-coded 20,000 entries and reported zero hierarchy omissions. That limit also counted structural records with no object ID. The graph walker in buildInterfaceBuilderAnalysis (src/domain/apple/interfaceBuilderGraph.ts) already applies max_objects to identified hierarchy records and owns hierarchy coverage, so the parser's cut duplicated that limit and reported nothing about what it dropped.

The walker also checked its limit before popping each pending entry. As a result:

  • Reaching the limit stopped the walk, so the omitted records were never counted (omitted stayed 0).
  • A hierarchy whose identified records exactly reached the limit was marked partial as soon as any structural entry was still pending.

Reproduced with the issue's CLI fixtures (decode-interface-builder --json; one identified tail record among empty structural records):

hierarchy main this PR
20,000 entries, tail last tail kept, 1 edge, complete 1/0, 4,697 bytes, 1,192 ms unchanged (4,697 bytes, 1,253 ms)
20,001 entries, tail last tail dropped, 0 edges, complete, examined 0, 3,668 bytes, 1,332 ms tail kept, 1 edge, complete 1/0, 4,697 bytes, 1,236 ms
20,001 entries, tail first tail kept, 1 edge, complete 1/0, 4,698 bytes, 1,308 ms unchanged (4,698 bytes, 1,139 ms)
100,000 entries, tail last tail dropped, 0 edges, complete, examined 0, 3,669 bytes, 4,692 ms tail kept, 1 edge, complete 1/0, 4,698 bytes, 4,209–4,713 ms

Facet values are shown as status examined/omitted. Timings are single cold CLI runs and are dominated by plist decoding. Runs on the same build vary by about ±500 ms.

Change and scope

  • The ibtool dictionary parser passes the decoded hierarchy array through. The fixed 20,000-entry cut is removed. The array is already bounded by the Interface Builder decode budget, and the keyed-archive path never had this cut.
  • The graph walker checks max_objects when it reaches an identified record rather than before each pending entry. Past the limit, it keeps walking the decoded records and counts each identified record it omits. The document is then reported partial with graph_limit_reached and that count. Node-limit and edge-limit accounting is unchanged.

Contract and boundary impact

  • Semantic owner and earliest changed stage: the graph walker owns the hierarchy limit and coverage; the parser no longer applies a second, unaccounted limit.

  • Contribution scope fit and linked maintainer decision: a bug fix within the existing static, read-only decoder; not applicable.

  • CLI and MCP/tool-catalog contract: schemas unchanged. CLI and MCP share the decoder.

  • Provider, bridge, target-format, or platform compatibility: provider-independent static decoding.

  • Evidence, artifact, provenance, or reconstruction contract: a retained hierarchy no longer loses records silently. Omissions caused by the limit are counted in the hierarchy facet's omitted field.

  • Target selection, authority, upstream behavior, and cleanup details: not applicable.

  • Generated metadata, package, or installation impact: none.

  • Read existing files or captured data.

Evidence and regression coverage

  • Tests added in src/domain/apple/interfaceBuilderGraph.test.ts (describe "Interface Builder hierarchy record limits"):

    • an identified record after 20,000 structural records is kept, and coverage stays complete;
    • identified records beyond max_objects are reported partial with graph_limit_reached, examined 2 and omitted 2;
    • a hierarchy that exactly reaches max_objects, with a structural entry still pending, stays complete.

    All three fail on main and pass with this change.

  • Real apps (decode-interface-builder --json, median of 3 runs; the result digest is a SHA-256 over normalized_result):

app documents nodes output bytes result digest (main = PR) main this PR
TextEdit 8 1,428 1,460,792 18d1acdefd399d53 571 ms 560 ms
Preview 64 5,061 5,633,072 a48bc2bef16e4687 758 ms 760 ms
Chess 3 910 885,074 aee8d6e686cdb7e2 521 ms 523 ms

These system nibs are keyed archives, so their results are byte-identical. The changed path is ibtool dictionary hierarchies, which the fixtures above exercise.

  • Remaining proof gaps: no real ibtool-dictionary nib with more than 20,000 hierarchy records was available. MCP parity is covered by the shared decoder and by the existing interfaceBuilderHierarchyLimits acceptance test. A separate MCP run on the new fixtures was not done.

Validation performed

  • npx vitest run src/domain/apple/interfaceBuilderGraph.test.ts — 23 passed; with the fix removed, the 3 new tests failed.
  • npx vitest run src/domain/apple src/artifacts/apple tests/boundary/filesystem/interfaceBuilderBundle.test.ts tests/boundary/filesystem/nibNativeHierarchy.test.ts tests/acceptance/applications/interfaceBuilderHierarchyLimits.test.ts — 15 files, 272 passed, 7 skipped.
  • npx vitest run on the other Interface Builder workflow tests (direct analysis, keyed archive selection, IB summary MCP, dylib resolution MCP, nib field identity, asset catalog resource keys, UIKit keyed archive, enhanced native investigation, native investigation graph, IB analysis view) — 10 files, 87 passed, 1 skipped.
  • npm run check:fast — passed.
  • CLI runs on the issue fixtures and the real apps listed above, on both builds.

Compatibility, safety, and release

  • Breaking changes or migration steps: none.
  • Real Hopper/Ghidra, browser, or OS coverage: not applicable. Real macOS system apps were decoded on macOS arm64 under Node 26.5.
  • Package or release metadata impact: none.
  • Security, privacy, process, or containment review: static decoding only. Walking past the limit reads only already-decoded records, which the existing decode budget bounds.

Author readiness

Human owner:

  • I reviewed the final diff and relevant existing code, understand the change and its edge cases, and take responsibility for the complete PR, including AI-assisted work.
  • I checked the evidence above, confirmed it supports the current change, and recorded meaningful validation gaps.
  • I declare this PR ready for initial maintainer review.

Before merge

  • This PR addresses a concrete problem or an agreed enhancement.
  • Material scope and authority questions are resolved, and any required maintainer scope decision covers the final diff.
  • The PR has one focused outcome and the title follows type(scope): outcome.
  • Related issue is linked, or the reason for not linking one is stated above.
  • Tests cover changed observable behavior and meaningful failure paths.
  • Owning docs, contracts, and generated metadata are updated where needed.
  • User-visible CLI/MCP changes include representative output.
  • I checked the final diff for secrets, unrelated cleanup, and unsupported claims.

🤖 Generated with Claude Code


🔄 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/4162 **Author:** [@kaoru0822-kitauji](https://github.com/kaoru0822-kitauji) **Created:** 10/15/2026 **Status:** ✅ Merged **Merged:** 10/15/2026 **Merged by:** [@kaoru0822-kitauji](https://github.com/kaoru0822-kitauji) **Base:** `main` ← **Head:** `fix/3950-ib-hierarchy-limit` --- ### 📝 Commits (1) - [`6ec86b0`](https://github.com/morluto/rea/commit/6ec86b0562ad9de0813b0c881a10ef67ea262f4e) fix(apple): count Interface Builder hierarchy records beyond the object limit ### 📊 Changes **3 files changed** (+97 additions, -7 deletions) <details> <summary>View changed files</summary> 📝 `src/domain/apple/interfaceBuilderGraph.test.ts` (+87 -0) 📝 `src/domain/apple/interfaceBuilderGraph.ts` (+9 -6) 📝 `src/domain/apple/interfaceBuilderKeyedArchive.ts` (+1 -1) </details> ### 📄 Description ## Summary Fixes #3950. In an ibtool dictionary nib, an identified hierarchy record that came after the first 20,000 hierarchy entries was dropped. The document still reported complete hierarchy coverage and an untruncated graph. The record and its containment edge are now kept. When `max_objects` does omit identified hierarchy records, the hierarchy facet reports them as partial, with an exact omitted count. ## Problem and expected behavior `parseInterfaceBuilderRecords` (`src/domain/apple/interfaceBuilderKeyedArchive.ts`) sliced the ibtool hierarchy array to a hard-coded 20,000 entries and reported zero hierarchy omissions. That limit also counted structural records with no object ID. The graph walker in `buildInterfaceBuilderAnalysis` (`src/domain/apple/interfaceBuilderGraph.ts`) already applies `max_objects` to identified hierarchy records and owns hierarchy coverage, so the parser's cut duplicated that limit and reported nothing about what it dropped. The walker also checked its limit before popping each pending entry. As a result: - Reaching the limit stopped the walk, so the omitted records were never counted (`omitted` stayed 0). - A hierarchy whose identified records exactly reached the limit was marked partial as soon as any structural entry was still pending. Reproduced with the issue's CLI fixtures (`decode-interface-builder --json`; one identified `tail` record among empty structural records): | hierarchy | main | this PR | |---|---|---| | 20,000 entries, `tail` last | `tail` kept, 1 edge, complete 1/0, 4,697 bytes, 1,192 ms | unchanged (4,697 bytes, 1,253 ms) | | 20,001 entries, `tail` last | **`tail` dropped, 0 edges, complete, examined 0**, 3,668 bytes, 1,332 ms | `tail` kept, 1 edge, complete 1/0, 4,697 bytes, 1,236 ms | | 20,001 entries, `tail` first | `tail` kept, 1 edge, complete 1/0, 4,698 bytes, 1,308 ms | unchanged (4,698 bytes, 1,139 ms) | | 100,000 entries, `tail` last | **`tail` dropped, 0 edges, complete, examined 0**, 3,669 bytes, 4,692 ms | `tail` kept, 1 edge, complete 1/0, 4,698 bytes, 4,209–4,713 ms | Facet values are shown as status examined/omitted. Timings are single cold CLI runs and are dominated by plist decoding. Runs on the same build vary by about ±500 ms. ## Change and scope - The ibtool dictionary parser passes the decoded hierarchy array through. The fixed 20,000-entry cut is removed. The array is already bounded by the Interface Builder decode budget, and the keyed-archive path never had this cut. - The graph walker checks `max_objects` when it reaches an identified record rather than before each pending entry. Past the limit, it keeps walking the decoded records and counts each identified record it omits. The document is then reported partial with `graph_limit_reached` and that count. Node-limit and edge-limit accounting is unchanged. ## Contract and boundary impact - Semantic owner and earliest changed stage: the graph walker owns the hierarchy limit and coverage; the parser no longer applies a second, unaccounted limit. - Contribution scope fit and linked maintainer decision: a bug fix within the existing static, read-only decoder; not applicable. - CLI and MCP/tool-catalog contract: schemas unchanged. CLI and MCP share the decoder. - Provider, bridge, target-format, or platform compatibility: provider-independent static decoding. - Evidence, artifact, provenance, or reconstruction contract: a retained hierarchy no longer loses records silently. Omissions caused by the limit are counted in the hierarchy facet's `omitted` field. - Target selection, authority, upstream behavior, and cleanup details: not applicable. - Generated metadata, package, or installation impact: none. - [x] Read existing files or captured data. ## Evidence and regression coverage - Tests added in `src/domain/apple/interfaceBuilderGraph.test.ts` (describe "Interface Builder hierarchy record limits"): - an identified record after 20,000 structural records is kept, and coverage stays complete; - identified records beyond `max_objects` are reported partial with `graph_limit_reached`, examined 2 and omitted 2; - a hierarchy that exactly reaches `max_objects`, with a structural entry still pending, stays complete. All three fail on main and pass with this change. - Real apps (`decode-interface-builder --json`, median of 3 runs; the result digest is a SHA-256 over `normalized_result`): | app | documents | nodes | output bytes | result digest (main = PR) | main | this PR | |---|---|---|---|---|---|---| | TextEdit | 8 | 1,428 | 1,460,792 | `18d1acdefd399d53` | 571 ms | 560 ms | | Preview | 64 | 5,061 | 5,633,072 | `a48bc2bef16e4687` | 758 ms | 760 ms | | Chess | 3 | 910 | 885,074 | `aee8d6e686cdb7e2` | 521 ms | 523 ms | These system nibs are keyed archives, so their results are byte-identical. The changed path is ibtool dictionary hierarchies, which the fixtures above exercise. - Remaining proof gaps: no real ibtool-dictionary nib with more than 20,000 hierarchy records was available. MCP parity is covered by the shared decoder and by the existing `interfaceBuilderHierarchyLimits` acceptance test. A separate MCP run on the new fixtures was not done. ## Validation performed - `npx vitest run src/domain/apple/interfaceBuilderGraph.test.ts` — 23 passed; with the fix removed, the 3 new tests failed. - `npx vitest run src/domain/apple src/artifacts/apple tests/boundary/filesystem/interfaceBuilderBundle.test.ts tests/boundary/filesystem/nibNativeHierarchy.test.ts tests/acceptance/applications/interfaceBuilderHierarchyLimits.test.ts` — 15 files, 272 passed, 7 skipped. - `npx vitest run` on the other Interface Builder workflow tests (direct analysis, keyed archive selection, IB summary MCP, dylib resolution MCP, nib field identity, asset catalog resource keys, UIKit keyed archive, enhanced native investigation, native investigation graph, IB analysis view) — 10 files, 87 passed, 1 skipped. - `npm run check:fast` — passed. - CLI runs on the issue fixtures and the real apps listed above, on both builds. ## Compatibility, safety, and release - Breaking changes or migration steps: none. - Real Hopper/Ghidra, browser, or OS coverage: not applicable. Real macOS system apps were decoded on macOS arm64 under Node 26.5. - Package or release metadata impact: none. - Security, privacy, process, or containment review: static decoding only. Walking past the limit reads only already-decoded records, which the existing decode budget bounds. ## Author readiness Human owner: <!-- @GitHub-handle --> - [ ] I reviewed the final diff and relevant existing code, understand the change and its edge cases, and take responsibility for the complete PR, including AI-assisted work. - [ ] I checked the evidence above, confirmed it supports the current change, and recorded meaningful validation gaps. - [ ] I declare this PR ready for initial maintainer review. ## Before merge - [ ] This PR addresses a concrete problem or an agreed enhancement. - [ ] Material scope and authority questions are resolved, and any required maintainer scope decision covers the final diff. - [ ] The PR has one focused outcome and the title follows `type(scope): outcome`. - [ ] Related issue is linked, or the reason for not linking one is stated above. - [ ] Tests cover changed observable behavior and meaningful failure paths. - [ ] Owning docs, contracts, and generated metadata are updated where needed. - [ ] User-visible CLI/MCP changes include representative output. - [ ] I checked the final diff for secrets, unrelated cleanup, and unsupported claims. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- <sub>🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.</sub>
bsakaguchi 2026-10-31 01:11:52 +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#6101
No description provided.