[GH-ISSUE #5721] [Bug] Keyed nib decoding reports placeholder classes instead of authored classes #3470

Closed
opened 2026-10-31 00:40:50 +00:00 by bsakaguchi · 1 comment
Owner

Originally created by @coygeek on GitHub (Oct 20, 2026).
Original GitHub issue: https://github.com/morluto/rea/issues/5721

Before submitting

  • I searched the open and closed issues for a related report.
  • I removed credentials, tokens, private paths, and sensitive target data.

Area

Evidence, artifacts, or reconstruction

Observed behavior

decode-interface-builder exits 0 but reports NSCustomObject and NSClassSwapper as the classes of an Apple-compiled keyed nib's controller and custom view. Both graph nodes have kind unknown, although their NSClassName attributes retain NSWindowController and FixturePanelView. A plain button correctly remains control / NSButton. The object and class coverage facets report complete.

Reproduction

Run from a built source checkout with locked dependencies installed, on macOS with Xcode's AppKit ibtool and C compiler, Node.js and Python 3. This creates and statically inspects an inert app; it never launches the app. The independent verifier replayed this shell block at the baseline. Submission rebuilt current main and reran the same compiled fixture and independent oracles once, with the same mismatch.

set -eu
work_dir="$(mktemp -d)"
mkdir -p "$work_dir/home" "$work_dir/cfg" "$work_dir/data" "$work_dir/cache" "$work_dir/state" "$work_dir/tmp" "$work_dir/Fixture.app/Contents/MacOS" "$work_dir/Fixture.app/Contents/Resources"
cat > "$work_dir/Main.xib" <<'XML'
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="23094" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES">
<dependencies><plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="23094"/></dependencies>
<objects>
<customObject id="-2" userLabel="File's Owner" customClass="NSWindowController"/>
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
<view id="view" customClass="FixturePanelView">
<rect key="frame" x="0" y="0" width="100" height="100"/>
<subviews><button id="plain"><rect key="frame" x="0" y="0" width="80" height="32"/><buttonCell key="cell" title="Plain" bezelStyle="rounded" id="plain-cell"/></button></subviews>
</view>
</objects></document>
XML
xcrun ibtool --errors --warnings --compile "$work_dir/Legacy.nib" --minimum-deployment-target 10.6 "$work_dir/Main.xib" > "$work_dir/compile.plist"
cp "$work_dir/Legacy.nib/keyedobjects.nib" "$work_dir/Fixture.app/Contents/Resources/Main.nib"
xcrun ibtool --objects --classes --output-format xml1 "$work_dir/Main.xib" > "$work_dir/oracle.plist"
printf 'int main(void){return 0;}\n' > "$work_dir/main.c"
xcrun clang "$work_dir/main.c" -o "$work_dir/Fixture.app/Contents/MacOS/Fixture"
cat > "$work_dir/Fixture.app/Contents/Info.plist" <<'PLIST'
<?xml version="1.0" encoding="UTF-8"?>
<plist version="1.0"><dict><key>CFBundleExecutable</key><string>Fixture</string><key>CFBundleIdentifier</key><string>dev.rea.fixture</string></dict></plist>
PLIST
python3 - "$work_dir/Fixture.app/Contents/Resources/Main.nib" "$work_dir/oracle.plist" <<'PY'
import plistlib,sys
archive=plistlib.load(open(sys.argv[1],'rb')); table=archive['$objects']
def resolve(v): return table[v.data] if isinstance(v,plistlib.UID) else v
print('archiver:',archive['$archiver'])
for i,obj in enumerate(table):
    if isinstance(obj,dict) and 'NSClassName' in obj:
        print('archive:',i,resolve(obj['$class'])['$classname'],resolve(obj['NSClassName']))
source=plistlib.load(open(sys.argv[2],'rb'))
for k,v in source['com.apple.ibtool.document.objects'].items():
    if k in ['-2','view','plain']: print('ibtool:',k,v.get('custom-class',v.get('class')))
PY
env -i PATH="$PATH" HOME="$work_dir/home" XDG_CONFIG_HOME="$work_dir/cfg" XDG_DATA_HOME="$work_dir/data" XDG_CACHE_HOME="$work_dir/cache" XDG_STATE_HOME="$work_dir/state" TMPDIR="$work_dir/tmp" REA_LOG_LEVEL=silent node scripts/rea.mjs decode-interface-builder "$work_dir/Fixture.app" --json > "$work_dir/compiled-result.json"
python3 - "$work_dir/compiled-result.json" <<'PY'
import json,sys
r=json.load(open(sys.argv[1]))['normalized_result']
print('documents:',r['documents'])
for n in r['graph']['nodes']:
    a=n['attributes']
    if a.get('NSClassName') or a.get('class_name')=='NSButton':
        print('REA:',a['interface_builder_object_id'],n['kind'],a['class_name'],a.get('NSClassName'))
print('coverage:',r['graph']['coverage'])
PY

Expected behavior

Preserve the authored class identities represented by Interface Builder custom-object and class-swapper records: NSWindowController for the controller and FixturePanelView for the view. Account for their source-supported categories, preserving explicit uncertainty when static data cannot determine a category. Keep NSButton / control for the plain button and retain the original descriptor as useful provenance. Do not infer runtime inheritance for arbitrary custom class names.

The baseline's docs/native-investigation.md:383-396 promises objects and controller/class names from compiled nibs; src/contracts/artifactToolContracts.ts:92-96 supports keyed property lists. AGENTS.md, Architecture and Cleanup, requires preserved identity and source evidence. The independent source-side ibtool oracle and compiled archive's UID-linked NSClassName values establish the available authored names.

Impact and scope

P2: this supported keyed-nib path returns incorrect class/category evidence while object and class facets report complete. The independent verifier reproduced an Apple-compiled fixture and a separate Swift-generated keyed archive; submission replayed the Apple-compiled case on main. Wider application prevalence and MCP transport were not tested. No target execution, data corruption or security boundary crossing occurred.

A demonstrated manual workaround is to read the retained NSClassName attributes or follow the compiled archive's UID-linked strings with Python plistlib, as above. This recovers the class facts for this fixture but does not repair REA's graph categories. Hierarchy is explicitly unsupported for this legacy archive and is outside this report's restoration signal.

REA version

rea-agents 6.7.0; independently verified source baseline 9d8e7803326a382cbbb3ffdb900f5e93e5442d68. Reproduced after npm run build:cached on fetched origin/main at 9258df02cc414112d4fa91a425f0d599f43e2357, also 6.7.0. The latest published release checked was 6.7.0; these are source-checkout runtime observations.

Runtime and platform

Node.js v26.12.0; macOS 27.0.1 arm64, freshly checked during submission. Xcode 27.1 build 27A9275; ibtool 27.1 bundle 25098, from the verifier's environment record. No optional deep-analysis provider was used.

Target and artifact context

Self-built Mach-O app containing the actual binary NSKeyedArchiver keyedobjects.nib produced by ibtool for macOS 10.6, copied as its sole Main.nib. The source XIB names a controller, custom view and plain button. HOME, XDG roots and TMPDIR are isolated; no runtime UI instantiation is involved.

Interface

CLI

Earliest failure stage

Native analysis or reconstruction

Relevant output or evidence

Compilation and the isolated CLI exit 0; compiler errors and warnings dictionaries are empty. The source oracle reports owner NSWindowController, view FixturePanelView and plain button NSButton. Python plistlib independently resolves the compiled archive descriptors and class strings. The main replay returns:

archiver: NSKeyedArchiver
archive: 2 NSCustomObject NSWindowController
archive: 12 NSCustomObject NSApplication
archive: 16 NSClassSwapper FixturePanelView
REA: 2 unknown NSCustomObject NSWindowController
REA: 12 unknown NSCustomObject NSApplication
REA: 16 unknown NSClassSwapper FixturePanelView
REA: 20 control NSButton None
objects facet: complete, examined 19, omitted 0
classes facet: complete, examined 10, omitted 0
hierarchy facet: unsupported, archive_hierarchy_not_decoded

The REA rows contain object ID, kind, class_name and NSClassName, respectively. Evidence provenance: observed; local verification: independently reproduced and replayed on main. No public classes map or successful restoration is claimed.

Typed outcome or recovery expected

Proposed restoration check: recompile and decode the same fixture. Preserve the authored controller/view class identities, categorize the known controller correctly, and retain source-supported view identity or explicit uncertainty. Keep the plain button as NSButton / control. Merely changing coverage does not restore class identity. Preserve the archive descriptor as provenance when useful; do not blindly reinterpret every field named NSClassName on arbitrary objects.

Analysis or suspected cause

Source-derived reasoning at the baseline: src/domain/apple/interfaceBuilderKeyedArchive.ts:216-219,246-263,379-415 derives class_name and kind from the archive descriptor while preserving NSClassName only as an attribute. src/domain/apple/interfaceBuilderGraph.ts:142-158 forwards those values. The same logic remains in the rebuilt main. The NIBArchive branch already resolves NSClassSwapper through NSClassName in src/artifacts/apple/InterfaceBuilderAnalysis.ts:495-510; the keyed-property-list branch does not. No bundled dependency was identified as the cause.

Additional context

Prepared with AI assistance and independently verified. #3958 concerns dangling class references and omitted objects; this fixture has valid placeholder descriptors and retained objects with available authored names. Open PR #5114 prevents an unrelated authored className field from overriding descriptor identity; #5218 / PR #5228 concern classification through descriptor superclass chains with class names already correct. Neither PR resolves NSClassName on these Interface Builder placeholder records. Merged #913 adds UIKit class-swapper support to the separate NIBArchive path.

Sibling reports #5692, #5693 and #5695 concern outlet-collection destinations, top-level hierarchy membership and segue connections. #5720 concerns keyed action connectors emitted as controls. Their closure signals do not restore authored object classes; suppressing connector nodes or restoring hierarchy membership can leave this mismatch unchanged.

NOT_ENOUGH_INFO: no earlier known-good revision or regression introduction was established. Broader archive and transport coverage remains untested.

Originally created by @coygeek on GitHub (Oct 20, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/5721 ### Before submitting - [x] I searched the open and closed issues for a related report. - [x] I removed credentials, tokens, private paths, and sensitive target data. ### Area Evidence, artifacts, or reconstruction ### Observed behavior `decode-interface-builder` exits 0 but reports `NSCustomObject` and `NSClassSwapper` as the classes of an Apple-compiled keyed nib's controller and custom view. Both graph nodes have kind `unknown`, although their `NSClassName` attributes retain `NSWindowController` and `FixturePanelView`. A plain button correctly remains `control / NSButton`. The object and class coverage facets report complete. ### Reproduction Run from a built source checkout with locked dependencies installed, on macOS with Xcode's AppKit ibtool and C compiler, Node.js and Python 3. This creates and statically inspects an inert app; it never launches the app. The independent verifier replayed this shell block at the baseline. Submission rebuilt current main and reran the same compiled fixture and independent oracles once, with the same mismatch. ```sh set -eu work_dir="$(mktemp -d)" mkdir -p "$work_dir/home" "$work_dir/cfg" "$work_dir/data" "$work_dir/cache" "$work_dir/state" "$work_dir/tmp" "$work_dir/Fixture.app/Contents/MacOS" "$work_dir/Fixture.app/Contents/Resources" cat > "$work_dir/Main.xib" <<'XML' <?xml version="1.0" encoding="UTF-8"?> <document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="23094" targetRuntime="MacOSX.Cocoa" propertyAccessControl="none" useAutolayout="YES"> <dependencies><plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="23094"/></dependencies> <objects> <customObject id="-2" userLabel="File's Owner" customClass="NSWindowController"/> <customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/> <customObject id="-3" userLabel="Application" customClass="NSObject"/> <view id="view" customClass="FixturePanelView"> <rect key="frame" x="0" y="0" width="100" height="100"/> <subviews><button id="plain"><rect key="frame" x="0" y="0" width="80" height="32"/><buttonCell key="cell" title="Plain" bezelStyle="rounded" id="plain-cell"/></button></subviews> </view> </objects></document> XML xcrun ibtool --errors --warnings --compile "$work_dir/Legacy.nib" --minimum-deployment-target 10.6 "$work_dir/Main.xib" > "$work_dir/compile.plist" cp "$work_dir/Legacy.nib/keyedobjects.nib" "$work_dir/Fixture.app/Contents/Resources/Main.nib" xcrun ibtool --objects --classes --output-format xml1 "$work_dir/Main.xib" > "$work_dir/oracle.plist" printf 'int main(void){return 0;}\n' > "$work_dir/main.c" xcrun clang "$work_dir/main.c" -o "$work_dir/Fixture.app/Contents/MacOS/Fixture" cat > "$work_dir/Fixture.app/Contents/Info.plist" <<'PLIST' <?xml version="1.0" encoding="UTF-8"?> <plist version="1.0"><dict><key>CFBundleExecutable</key><string>Fixture</string><key>CFBundleIdentifier</key><string>dev.rea.fixture</string></dict></plist> PLIST python3 - "$work_dir/Fixture.app/Contents/Resources/Main.nib" "$work_dir/oracle.plist" <<'PY' import plistlib,sys archive=plistlib.load(open(sys.argv[1],'rb')); table=archive['$objects'] def resolve(v): return table[v.data] if isinstance(v,plistlib.UID) else v print('archiver:',archive['$archiver']) for i,obj in enumerate(table): if isinstance(obj,dict) and 'NSClassName' in obj: print('archive:',i,resolve(obj['$class'])['$classname'],resolve(obj['NSClassName'])) source=plistlib.load(open(sys.argv[2],'rb')) for k,v in source['com.apple.ibtool.document.objects'].items(): if k in ['-2','view','plain']: print('ibtool:',k,v.get('custom-class',v.get('class'))) PY env -i PATH="$PATH" HOME="$work_dir/home" XDG_CONFIG_HOME="$work_dir/cfg" XDG_DATA_HOME="$work_dir/data" XDG_CACHE_HOME="$work_dir/cache" XDG_STATE_HOME="$work_dir/state" TMPDIR="$work_dir/tmp" REA_LOG_LEVEL=silent node scripts/rea.mjs decode-interface-builder "$work_dir/Fixture.app" --json > "$work_dir/compiled-result.json" python3 - "$work_dir/compiled-result.json" <<'PY' import json,sys r=json.load(open(sys.argv[1]))['normalized_result'] print('documents:',r['documents']) for n in r['graph']['nodes']: a=n['attributes'] if a.get('NSClassName') or a.get('class_name')=='NSButton': print('REA:',a['interface_builder_object_id'],n['kind'],a['class_name'],a.get('NSClassName')) print('coverage:',r['graph']['coverage']) PY ``` ### Expected behavior Preserve the authored class identities represented by Interface Builder custom-object and class-swapper records: `NSWindowController` for the controller and `FixturePanelView` for the view. Account for their source-supported categories, preserving explicit uncertainty when static data cannot determine a category. Keep `NSButton / control` for the plain button and retain the original descriptor as useful provenance. Do not infer runtime inheritance for arbitrary custom class names. The baseline's `docs/native-investigation.md:383-396` promises objects and controller/class names from compiled nibs; `src/contracts/artifactToolContracts.ts:92-96` supports keyed property lists. `AGENTS.md`, Architecture and Cleanup, requires preserved identity and source evidence. The independent source-side ibtool oracle and compiled archive's UID-linked `NSClassName` values establish the available authored names. ### Impact and scope P2: this supported keyed-nib path returns incorrect class/category evidence while object and class facets report complete. The independent verifier reproduced an Apple-compiled fixture and a separate Swift-generated keyed archive; submission replayed the Apple-compiled case on main. Wider application prevalence and MCP transport were not tested. No target execution, data corruption or security boundary crossing occurred. A demonstrated manual workaround is to read the retained `NSClassName` attributes or follow the compiled archive's UID-linked strings with Python plistlib, as above. This recovers the class facts for this fixture but does not repair REA's graph categories. Hierarchy is explicitly unsupported for this legacy archive and is outside this report's restoration signal. ### REA version `rea-agents` 6.7.0; independently verified source baseline `9d8e7803326a382cbbb3ffdb900f5e93e5442d68`. Reproduced after `npm run build:cached` on fetched `origin/main` at `9258df02cc414112d4fa91a425f0d599f43e2357`, also 6.7.0. The latest published release checked was 6.7.0; these are source-checkout runtime observations. ### Runtime and platform Node.js v26.12.0; macOS 27.0.1 arm64, freshly checked during submission. Xcode 27.1 build 27A9275; ibtool 27.1 bundle 25098, from the verifier's environment record. No optional deep-analysis provider was used. ### Target and artifact context Self-built Mach-O app containing the actual binary NSKeyedArchiver `keyedobjects.nib` produced by ibtool for macOS 10.6, copied as its sole `Main.nib`. The source XIB names a controller, custom view and plain button. HOME, XDG roots and TMPDIR are isolated; no runtime UI instantiation is involved. ### Interface CLI ### Earliest failure stage Native analysis or reconstruction ### Relevant output or evidence Compilation and the isolated CLI exit 0; compiler errors and warnings dictionaries are empty. The source oracle reports owner `NSWindowController`, view `FixturePanelView` and plain button `NSButton`. Python plistlib independently resolves the compiled archive descriptors and class strings. The main replay returns: ```text archiver: NSKeyedArchiver archive: 2 NSCustomObject NSWindowController archive: 12 NSCustomObject NSApplication archive: 16 NSClassSwapper FixturePanelView REA: 2 unknown NSCustomObject NSWindowController REA: 12 unknown NSCustomObject NSApplication REA: 16 unknown NSClassSwapper FixturePanelView REA: 20 control NSButton None objects facet: complete, examined 19, omitted 0 classes facet: complete, examined 10, omitted 0 hierarchy facet: unsupported, archive_hierarchy_not_decoded ``` The REA rows contain object ID, kind, class_name and NSClassName, respectively. Evidence provenance: observed; local verification: independently reproduced and replayed on main. No public classes map or successful restoration is claimed. ### Typed outcome or recovery expected Proposed restoration check: recompile and decode the same fixture. Preserve the authored controller/view class identities, categorize the known controller correctly, and retain source-supported view identity or explicit uncertainty. Keep the plain button as `NSButton / control`. Merely changing coverage does not restore class identity. Preserve the archive descriptor as provenance when useful; do not blindly reinterpret every field named `NSClassName` on arbitrary objects. ### Analysis or suspected cause Source-derived reasoning at the baseline: `src/domain/apple/interfaceBuilderKeyedArchive.ts:216-219,246-263,379-415` derives `class_name` and kind from the archive descriptor while preserving `NSClassName` only as an attribute. `src/domain/apple/interfaceBuilderGraph.ts:142-158` forwards those values. The same logic remains in the rebuilt main. The NIBArchive branch already resolves `NSClassSwapper` through `NSClassName` in `src/artifacts/apple/InterfaceBuilderAnalysis.ts:495-510`; the keyed-property-list branch does not. No bundled dependency was identified as the cause. ### Additional context Prepared with AI assistance and independently verified. #3958 concerns dangling class references and omitted objects; this fixture has valid placeholder descriptors and retained objects with available authored names. Open PR #5114 prevents an unrelated authored `className` field from overriding descriptor identity; #5218 / PR #5228 concern classification through descriptor superclass chains with class names already correct. Neither PR resolves `NSClassName` on these Interface Builder placeholder records. Merged #913 adds UIKit class-swapper support to the separate NIBArchive path. Sibling reports #5692, #5693 and #5695 concern outlet-collection destinations, top-level hierarchy membership and segue connections. #5720 concerns keyed action connectors emitted as controls. Their closure signals do not restore authored object classes; suppressing connector nodes or restoring hierarchy membership can leave this mismatch unchanged. NOT_ENOUGH_INFO: no earlier known-good revision or regression introduction was established. Broader archive and transport coverage remains untested.
Author
Owner

@matbrgz commented on GitHub (Oct 21, 2026):

Cross-referencing with canonical issue #5540 (decode-interface-builder misattributes object-limit omissions to the connection budget) and sibling NIB decoding reports #5541, #5590, #5692, #5693, #5695, #5720.

Consolidated under Cluster AX (Compiled Interface Builder / NIB Decoding & Object Limit Anomalies) in tracker #4330.

<!-- gh-comment-id:6304920517 --> @matbrgz commented on GitHub (Oct 21, 2026): Cross-referencing with canonical issue #5540 (`decode-interface-builder misattributes object-limit omissions to the connection budget`) and sibling NIB decoding reports #5541, #5590, #5692, #5693, #5695, #5720. Consolidated under **Cluster AX (Compiled Interface Builder / NIB Decoding & Object Limit Anomalies)** in tracker #4330.
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#3470
No description provided.