[PR #4118] [MERGED] fix(apple): keep Interface Builder objects whose archived class is unresolved #6068

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

📋 Pull Request Information

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

Base: main ← Head: fix/3958-ib-unresolved-class-objects


📝 Commits (2)

  • 10a2ebd fix(apple): keep Interface Builder objects whose archived class is unresolved
  • e1761b1 fix(apple): keep the dangling class reference of an unknown IB object

📊 Changes

2 files changed (+91 additions, -1 deletions)

View changed files

📝 src/domain/apple/interfaceBuilderGraph.test.ts (+84 -0)
📝 src/domain/apple/interfaceBuilderKeyedArchive.ts (+7 -1)

📄 Description

Summary

Fixes #3958. Interface Builder decoding dropped an archived object whose $class reference could not be resolved. The document's object_count excluded it and object coverage still said complete with zero omissions. When that object was the hierarchy root, its node lost the serialized label. Such objects are now kept as explicit unknowns: class_name: null, the generic kind, and their fields and label preserved.

Problem and expected behavior

parseKeyedArchive (src/domain/apple/interfaceBuilderKeyedArchive.ts) skipped every object-table entry with no resolved class name (if (className === null) continue;). That correctly excludes scalars, $null and class descriptors. It also silently excluded archived objects whose class reference dangles. The node schema already allows class_name: null.

Reproduced with the issue's generated app: object 1 has $class UID 99, which doesn't exist; object 3 is a UIView. Run through decode-interface-builder --json:

variant main this PR
object 1 unresolved, root = object 3 nodes: only 3 (view); object_count: 1; objects coverage complete, examined 1, omitted 0; 4,671 bytes nodes: 1 (unknown, class_name: null, name Unresolved object) and 3; object_count: 2; complete, examined 2; 5,139 bytes
root = unresolved object 1 identity-only node, label lost (as reported in the issue) node 1 keeps its label; object_count: 2; hierarchy and objects complete
class resolved (control) 1 (view, UIView) and 3; object_count: 2 unchanged

Change and scope

  • An object-table entry is kept when it has its own $class key, even if the class does not resolve. Its raw $class reference (for example { UID: 99 }) is retained as an attribute, so the dangling source value stays traceable. Such an entry is an archived object, and its unknown class is reported as null. Entries without $class (scalars, $null, class descriptors) remain excluded.
  • The connection-class match now requires a resolved class name. An object of unknown class cannot be recognized as a connector, and this narrowing also satisfies the types.
  • No schema changes. The graph already maps a null-class object to its unknown kind.

Contract and boundary impact

  • Semantic owner and earliest changed stage: keyed-archive object projection in parseKeyedArchive.

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

  • CLI and MCP/tool-catalog contract: schemas unchanged. Unresolved-class objects now appear as explicit unknowns, as the tool contract promises for unrecognized archive classes.

  • Provider, bridge, target-format, or platform compatibility: NSKeyedArchiver-based Interface Builder archives.

  • Evidence, artifact, provenance, or reconstruction contract: identity, label and fields of unresolved-class objects are preserved, and counts and coverage include them.

  • 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 or updated: src/domain/apple/interfaceBuilderGraph.test.ts, new describe "keyed archive objects with unresolved classes".
    • The issue's object table plus a plain scalar entry: the unresolved object 1 is kept with class_name: null, kind other, its name and label attribute; 3 stays a UIView view; the descriptor and the scalar are not objects; objectCount is 2.
    • It fails on main and passes with the fix.
  • Real applications, decode-interface-builder, median of 3 runs, main vs this PR:
app main this PR
/System/Applications/TextEdit.app 599 ms, 1,460,792 bytes, result 18d1acdefd399d53 571 ms, identical bytes and result
/Applications/XLD.app 2,017 ms, 6,731,008 bytes, result 6676c9d4384bb05c 2,091 ms, identical bytes and result

Output is byte-identical for real apps whose classes resolve. Timing is within run-to-run variance.

  • Remaining proof gaps: no shipped nib with a dangling $class reference was available, so that case is covered by the issue's generated archive and the unit test.

Validation performed

  • npx vitest run src/domain/apple/interfaceBuilderGraph.test.ts — 19 passed; against main, 1 failed as expected.
  • npx vitest run src/domain/apple src/artifacts/apple tests/boundary/filesystem/interfaceBuilderBundle.test.ts tests/boundary/cli/interfaceBuilder tests/acceptance/applications — 68 files, 516 passed, 1 skipped.
  • npm run check:fast — passed. My first test placement exceeded the 150-line describe limit; I moved it into its own describe.
  • The issue's fixture and both variants run through the built CLI, as tabulated.

Compatibility, safety, and release

  • Breaking changes or migration steps: none. Results gain previously missing unknown-class object nodes.
  • Real Hopper/Ghidra, browser, or OS coverage: macOS app bundles.
  • Package or release metadata impact: none.
  • Security, privacy, process, or containment review: static parsing only.

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/4118 **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/3958-ib-unresolved-class-objects` --- ### 📝 Commits (2) - [`10a2ebd`](https://github.com/morluto/rea/commit/10a2ebded5e006fc3d0ecf07749ac423745eac3f) fix(apple): keep Interface Builder objects whose archived class is unresolved - [`e1761b1`](https://github.com/morluto/rea/commit/e1761b1fcd8d36cbae3bc04d4b146804c8580920) fix(apple): keep the dangling class reference of an unknown IB object ### 📊 Changes **2 files changed** (+91 additions, -1 deletions) <details> <summary>View changed files</summary> 📝 `src/domain/apple/interfaceBuilderGraph.test.ts` (+84 -0) 📝 `src/domain/apple/interfaceBuilderKeyedArchive.ts` (+7 -1) </details> ### 📄 Description ## Summary Fixes #3958. Interface Builder decoding dropped an archived object whose `$class` reference could not be resolved. The document's `object_count` excluded it and object coverage still said `complete` with zero omissions. When that object was the hierarchy root, its node lost the serialized label. Such objects are now kept as explicit unknowns: `class_name: null`, the generic kind, and their fields and label preserved. ## Problem and expected behavior `parseKeyedArchive` (`src/domain/apple/interfaceBuilderKeyedArchive.ts`) skipped every object-table entry with no resolved class name (`if (className === null) continue;`). That correctly excludes scalars, `$null` and class descriptors. It also silently excluded archived objects whose class reference dangles. The node schema already allows `class_name: null`. Reproduced with the issue's generated app: object 1 has `$class` UID 99, which doesn't exist; object 3 is a `UIView`. Run through `decode-interface-builder --json`: | variant | main | this PR | |---|---|---| | object 1 unresolved, root = object 3 | nodes: only `3` (view); `object_count: 1`; objects coverage `complete`, examined 1, omitted 0; 4,671 bytes | nodes: `1` (`unknown`, `class_name: null`, name `Unresolved object`) and `3`; `object_count: 2`; `complete`, examined 2; 5,139 bytes | | root = unresolved object 1 | identity-only node, label lost (as reported in the issue) | node `1` keeps its label; `object_count: 2`; hierarchy and objects complete | | class resolved (control) | `1` (view, `UIView`) and `3`; `object_count: 2` | unchanged | ## Change and scope - An object-table entry is kept when it has its own `$class` key, even if the class does not resolve. Its raw `$class` reference (for example `{ UID: 99 }`) is retained as an attribute, so the dangling source value stays traceable. Such an entry is an archived object, and its unknown class is reported as `null`. Entries without `$class` (scalars, `$null`, class descriptors) remain excluded. - The connection-class match now requires a resolved class name. An object of unknown class cannot be recognized as a connector, and this narrowing also satisfies the types. - No schema changes. The graph already maps a null-class object to its `unknown` kind. ## Contract and boundary impact - Semantic owner and earliest changed stage: keyed-archive object projection in `parseKeyedArchive`. - Contribution scope fit and linked maintainer decision: a bug fix within the existing static, read-only boundary; not applicable. - CLI and MCP/tool-catalog contract: schemas unchanged. Unresolved-class objects now appear as explicit unknowns, as the tool contract promises for unrecognized archive classes. - Provider, bridge, target-format, or platform compatibility: NSKeyedArchiver-based Interface Builder archives. - Evidence, artifact, provenance, or reconstruction contract: identity, label and fields of unresolved-class objects are preserved, and counts and coverage include them. - 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 or updated: `src/domain/apple/interfaceBuilderGraph.test.ts`, new `describe` "keyed archive objects with unresolved classes". - The issue's object table plus a plain scalar entry: the unresolved object `1` is kept with `class_name: null`, kind `other`, its name and `label` attribute; `3` stays a `UIView` view; the descriptor and the scalar are not objects; `objectCount` is 2. - It fails on main and passes with the fix. - Real applications, `decode-interface-builder`, median of 3 runs, main vs this PR: | app | main | this PR | |---|---|---| | `/System/Applications/TextEdit.app` | 599 ms, 1,460,792 bytes, result `18d1acdefd399d53` | 571 ms, identical bytes and result | | `/Applications/XLD.app` | 2,017 ms, 6,731,008 bytes, result `6676c9d4384bb05c` | 2,091 ms, identical bytes and result | Output is byte-identical for real apps whose classes resolve. Timing is within run-to-run variance. - Remaining proof gaps: no shipped nib with a dangling `$class` reference was available, so that case is covered by the issue's generated archive and the unit test. ## Validation performed - `npx vitest run src/domain/apple/interfaceBuilderGraph.test.ts` — 19 passed; against main, 1 failed as expected. - `npx vitest run src/domain/apple src/artifacts/apple tests/boundary/filesystem/interfaceBuilderBundle.test.ts tests/boundary/cli/interfaceBuilder tests/acceptance/applications` — 68 files, 516 passed, 1 skipped. - `npm run check:fast` — passed. My first test placement exceeded the 150-line `describe` limit; I moved it into its own `describe`. - The issue's fixture and both variants run through the built CLI, as tabulated. ## Compatibility, safety, and release - Breaking changes or migration steps: none. Results gain previously missing unknown-class object nodes. - Real Hopper/Ghidra, browser, or OS coverage: macOS app bundles. - Package or release metadata impact: none. - Security, privacy, process, or containment review: static parsing only. ## 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:36 +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#6068
No description provided.