[GH-ISSUE #6850] [Bug] Keyed UIKit nib hierarchy ignores UINibTopLevelObjectsKey #4190

Closed
opened 2026-10-31 00:47:34 +00:00 by bsakaguchi · 0 comments
Owner

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

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

The production bundle analyzer decodes a small UIKit keyed nib's objects and outlet but emits no hierarchy when $top contains UINibTopLevelObjectsKey without a redundant root alias. It reports hierarchy status unsupported with reason archive_hierarchy_not_decoded and hierarchy_complete: false.

Adding root pointing at the very same top-level array restores the two top-level edges. Renaming only UINibTopLevelObjectsKey to the parser's misspelled UITopLevelObjectsKey also restores those edges. The misspelled form is an artificial diagnostic control, not a native archive recommendation. XML and actual binary UID encodings produced the same result.

Reproduction

Actually executed from the source checkout, with this script saved outside it as uikit-top-level.mts and an existing scratch directory passed as probe_dir:

node --import ./node_modules/tsx/dist/loader.mjs "$probe_dir/uikit-top-level.mts" "$PWD" "$probe_dir"
import assert from 'node:assert/strict';
import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises';
import { join } from 'node:path';
import { tmpdir } from 'node:os';
import { pathToFileURL } from 'node:url';
const checkout=process.argv[2]!,out=process.argv[3]!;
const {analyzeInterfaceBuilderBundle}=await import(pathToFileURL(join(checkout,'src/artifacts/apple/InterfaceBuilderAnalysis.ts')).href);
const {parseInterfaceBuilderRecords}=await import(pathToFileURL(join(checkout,'src/domain/apple/interfaceBuilderKeyedArchive.ts')).href);
const {decodeKeyedArchiveBytes}=await import(pathToFileURL(join(checkout,'src/artifacts/apple/KeyedArchiveReader.ts')).href);
const uid=(n:number)=>({'CF$UID':n});
function xml(v:unknown):string {
 if(typeof v==='number')return `<integer>${v}</integer>`;
 if(typeof v==='string')return `<string>${v}</string>`;
 if(Array.isArray(v))return `<array>${v.map(xml).join('')}</array>`;
 return `<dict>${Object.entries(v as object).map(([k,c])=>`<key>${k}</key>${xml(c)}`).join('')}</dict>`;
}
const work=await mkdtemp(join(tmpdir(),'rea-apple-top-'));
const rows=[];
try {
 for(const variant of ['native-key','root-alias','misspelled-key']) {
  const top:Record<string,unknown>={UINibConnectionsKey:uid(9),UINibObjectsKey:uid(10),[variant==='misspelled-key'?'UITopLevelObjectsKey':'UINibTopLevelObjectsKey']:uid(10)};
  if(variant==='root-alias')top.root=uid(10);
  const archive={$archiver:'NSKeyedArchiver',$version:100000,$objects:['$null',
    {$class:uid(4)},{$class:uid(5)},{$class:uid(6),UISource:uid(1),UIDestination:uid(2),UILabel:uid(7)},
    {$classname:'UIViewController',$classes:['UIViewController','UIResponder','NSObject']},
    {$classname:'UIView',$classes:['UIView','UIResponder','NSObject']},
    {$classname:'UIRuntimeOutletConnection',$classes:['UIRuntimeOutletConnection','UIRuntimeConnection','NSObject']},'view',
    {$class:uid(11),'NS.objects':[uid(1),uid(2),uid(3)]},
    {$class:uid(11),'NS.objects':[uid(3)]},
    {$class:uid(11),'NS.objects':[uid(1),uid(2)]},
    {$classname:'NSArray',$classes:['NSArray','NSObject']}],$top:top};
  const bytes=Buffer.from('<?xml version="1.0"?><plist version="1.0">'+xml(archive)+'</plist>');
  await writeFile(join(out,'top-'+variant+'.nib'),bytes);
  const bundlePath=join(work,variant+'.app'); await mkdir(bundlePath); await writeFile(join(bundlePath,'Main.nib'),bytes);
  const start=performance.now();const result=await analyzeInterfaceBuilderBundle({bundlePath,targetSha256:'a'.repeat(64)});const elapsedMs=performance.now()-start;
  const parsed=parseInterfaceBuilderRecords(archive);
  const generic=decodeKeyedArchiveBytes(bytes,{offset:0,limit:20});
  const hierarchy=result.graph.edges.filter((e:any)=>e.id.includes(':hierarchy:')).map((e:any)=>({from:e.from,to:e.to,resolution:e.resolution}));
  const row={variant,elapsedMs,sha256:result.documents[0].archive_sha256,document:result.documents[0],hierarchy,parsedHierarchy:parsed.hierarchy,objectIDs:parsed.objects.map((o:any)=>o.id),connections:parsed.connections,coverage:result.graph.coverage,truncated:result.graph.truncated,keyedRoots:generic.roots};
  rows.push(row);console.log(JSON.stringify(row));
  assert.equal(result.documents[0].connection_count,1);assert.equal(result.graph.truncated,false);
  assert.equal(result.documents[0].hierarchy_complete,variant!=='native-key');
  assert.equal(hierarchy.length,variant==='native-key'?0:2);
 }
 await writeFile(join(out,'uikit-top-level-results.json'),JSON.stringify({runtime:process.version,platform:process.platform,arch:process.arch,rows},null,2)+'\n');
} finally {await rm(work,{recursive:true,force:true});}

All assertions passed. The XML native-key analyzer call took 24.061 ms and its binary counterpart took 5.466 ms. These are single-call wall-clock observations. Each final XML case and binary counterpart ran once in the saved executions. Temporary bundles were removed.

Expected behavior

Recognize the serialized UINibTopLevelObjectsKey root and project its two top-level objects, using the array decoding already demonstrated by the controls. A separate generic root key is not needed to identify this explicitly named root.

The repository's UIKit archive fixture uses UINibTopLevelObjectsKey; its extra root masks the misspelled fallback. As an independent source reference, Microsoft's UINib implementation reads that correctly spelled key directly for NSKeyedUnarchiver input. This is source evidence, not a claimed native-consumer run. The expected contract does not require loading classes or executing a target.

Impact and scope

The demonstrated named-root archives lose recoverable top-level structure while objects and the outlet remain available. The result truthfully marks hierarchy unsupported, so this report does not claim false complete hierarchy coverage. The executed generic keyed-archive decoder retains UINibTopLevelObjectsKey → UID 10 and array 10's references to objects 1 and 2, permitting manual inspection. The root-alias and misspelled-key cases are diagnostic controls, not suggested changes to inspected artifacts. Actual app prevalence and native Apple acceptance of this rootless synthetic variant were not measured.

REA version

rea-agents 6.7.0, source revision 9066444f6b8e9ffe1607a9b02cde3895a37db431. This is a source-checkout reproduction with locked dependencies; it does not claim execution of the published tarball. The research assignment confirmed 6.7.0 as npm latest.

Runtime and platform

Node.js v24.22.1; Linux x64; source tsx loader. No build, install, full suite, Apple compiler, native provider, target process, or CLI/MCP adapter was run. Binary controls used Python plistlib to encode the same synthetic archive dictionaries with actual plist UID records.

Target and artifact context

Synthetic XML and binary Foundation keyed archives modeled on REA's existing UIKit outlet fixture: a UIViewController, a UIView, one UIRuntimeOutletConnection, NSArray wrappers and four class descriptors. $top names connections, objects and top-level objects. All UIDs are in bounds. No embedded prototype nib, segue, symlink, malformed plist or class-swapper behavior is involved. The analyzer receives a placeholder target digest; public target admission was not tested.

Interface

Other

Earliest failure stage

Native analysis or reconstruction

Relevant output or evidence

Reduced observed output:

{
  "variant": "native-key",
  "document": {"object_count":6,"connection_count":1,"hierarchy_complete":false},
  "hierarchy": [],
  "hierarchyCoverage": {
    "status":"unsupported", "reason":"archive_hierarchy_not_decoded",
    "examined":0, "omitted":0
  },
  "truncated": false,
  "keyedRoots": {
    "UINibConnectionsKey":{"CF$UID":9},
    "UINibObjectsKey":{"CF$UID":10},
    "UINibTopLevelObjectsKey":{"CF$UID":10}
  }
}

The root-alias and misspelled-key controls each emit document → 1 and document → 2 with complete hierarchy coverage (examined: 2, omitted: 0); all cases preserve the one view outlet. Binary controls give the same hierarchy outcomes with binary UID reference markers.

Failing archive SHA-256: XML 80f1df200aedac1c2c14c3ad0796da0698c29346b3b0cb548387a38560733f83; binary 456b930592513bea48aa4a351c3f52ef7b5d67c3f088afd22e4d25fdfe29e680. REA source revision is the one above. This establishes source-boundary behavior, not a public CLI/MCP or native UIKit execution.

Typed outcome or recovery expected

Two observed hierarchy containment links from the document to the explicitly named top-level objects, with retained archive provenance.

Analysis or suspected cause

Source-derived: the root selection reads top.root ?? top.UITopLevelObjectsKey, omitting Nib from the real key. The existing array and object decoders already project the same members when either artificial control makes that array reachable. This isolates root-key selection from member decoding or outlet interpretation.

Additional context

Preliminary read-only open/closed searches for both key spellings found only related closed #2177, whose reported problem was missing UIKit outlets and whose retained fixture includes a masking root alias. The current probe's outlet already decodes correctly. Open #5693 is an AppKit object-parent registry omission; #5739 concerns embedded prototype archives. Neither describes this keyed UIKit named-root typo.

Evidence provenance: executed production parser, bundle analyzer and generic keyed-archive decoder; local verification: reproduced; reproduction completeness: complete for the supplied synthetic XML and binary inputs. Native Apple consumer qualification is source-supported but not executed here. No implementation changes were made.

Triage assessment

  • Impact level: P2
  • Assessment status: supported
  • Impact basis: The demonstrated correctly named UIKit root is ignored, removing all recoverable top-level hierarchy links from otherwise decoded synthetic archives. Paired alias and spelling controls isolate the lookup defect; hierarchy incompleteness is disclosed and raw evidence remains available.
  • Workaround status: available
  • Workaround basis: The executed generic keyed-archive decoder preserves the named root, array UID 10 and its member UIDs for manual reconstruction. Altering the inspected archive or relying on the artificial misspelling is not proposed as a workaround.
Originally created by @coygeek on GitHub (Oct 23, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/6850 ### 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 The production bundle analyzer decodes a small UIKit keyed nib's objects and outlet but emits no hierarchy when `$top` contains `UINibTopLevelObjectsKey` without a redundant `root` alias. It reports hierarchy status `unsupported` with reason `archive_hierarchy_not_decoded` and `hierarchy_complete: false`. Adding `root` pointing at the very same top-level array restores the two top-level edges. Renaming only `UINibTopLevelObjectsKey` to the parser's misspelled `UITopLevelObjectsKey` also restores those edges. The misspelled form is an artificial diagnostic control, not a native archive recommendation. XML and actual binary UID encodings produced the same result. ### Reproduction Actually executed from the source checkout, with this script saved outside it as `uikit-top-level.mts` and an existing scratch directory passed as `probe_dir`: ```sh node --import ./node_modules/tsx/dist/loader.mjs "$probe_dir/uikit-top-level.mts" "$PWD" "$probe_dir" ``` ```ts import assert from 'node:assert/strict'; import { mkdir, mkdtemp, rm, writeFile } from 'node:fs/promises'; import { join } from 'node:path'; import { tmpdir } from 'node:os'; import { pathToFileURL } from 'node:url'; const checkout=process.argv[2]!,out=process.argv[3]!; const {analyzeInterfaceBuilderBundle}=await import(pathToFileURL(join(checkout,'src/artifacts/apple/InterfaceBuilderAnalysis.ts')).href); const {parseInterfaceBuilderRecords}=await import(pathToFileURL(join(checkout,'src/domain/apple/interfaceBuilderKeyedArchive.ts')).href); const {decodeKeyedArchiveBytes}=await import(pathToFileURL(join(checkout,'src/artifacts/apple/KeyedArchiveReader.ts')).href); const uid=(n:number)=>({'CF$UID':n}); function xml(v:unknown):string { if(typeof v==='number')return `<integer>${v}</integer>`; if(typeof v==='string')return `<string>${v}</string>`; if(Array.isArray(v))return `<array>${v.map(xml).join('')}</array>`; return `<dict>${Object.entries(v as object).map(([k,c])=>`<key>${k}</key>${xml(c)}`).join('')}</dict>`; } const work=await mkdtemp(join(tmpdir(),'rea-apple-top-')); const rows=[]; try { for(const variant of ['native-key','root-alias','misspelled-key']) { const top:Record<string,unknown>={UINibConnectionsKey:uid(9),UINibObjectsKey:uid(10),[variant==='misspelled-key'?'UITopLevelObjectsKey':'UINibTopLevelObjectsKey']:uid(10)}; if(variant==='root-alias')top.root=uid(10); const archive={$archiver:'NSKeyedArchiver',$version:100000,$objects:['$null', {$class:uid(4)},{$class:uid(5)},{$class:uid(6),UISource:uid(1),UIDestination:uid(2),UILabel:uid(7)}, {$classname:'UIViewController',$classes:['UIViewController','UIResponder','NSObject']}, {$classname:'UIView',$classes:['UIView','UIResponder','NSObject']}, {$classname:'UIRuntimeOutletConnection',$classes:['UIRuntimeOutletConnection','UIRuntimeConnection','NSObject']},'view', {$class:uid(11),'NS.objects':[uid(1),uid(2),uid(3)]}, {$class:uid(11),'NS.objects':[uid(3)]}, {$class:uid(11),'NS.objects':[uid(1),uid(2)]}, {$classname:'NSArray',$classes:['NSArray','NSObject']}],$top:top}; const bytes=Buffer.from('<?xml version="1.0"?><plist version="1.0">'+xml(archive)+'</plist>'); await writeFile(join(out,'top-'+variant+'.nib'),bytes); const bundlePath=join(work,variant+'.app'); await mkdir(bundlePath); await writeFile(join(bundlePath,'Main.nib'),bytes); const start=performance.now();const result=await analyzeInterfaceBuilderBundle({bundlePath,targetSha256:'a'.repeat(64)});const elapsedMs=performance.now()-start; const parsed=parseInterfaceBuilderRecords(archive); const generic=decodeKeyedArchiveBytes(bytes,{offset:0,limit:20}); const hierarchy=result.graph.edges.filter((e:any)=>e.id.includes(':hierarchy:')).map((e:any)=>({from:e.from,to:e.to,resolution:e.resolution})); const row={variant,elapsedMs,sha256:result.documents[0].archive_sha256,document:result.documents[0],hierarchy,parsedHierarchy:parsed.hierarchy,objectIDs:parsed.objects.map((o:any)=>o.id),connections:parsed.connections,coverage:result.graph.coverage,truncated:result.graph.truncated,keyedRoots:generic.roots}; rows.push(row);console.log(JSON.stringify(row)); assert.equal(result.documents[0].connection_count,1);assert.equal(result.graph.truncated,false); assert.equal(result.documents[0].hierarchy_complete,variant!=='native-key'); assert.equal(hierarchy.length,variant==='native-key'?0:2); } await writeFile(join(out,'uikit-top-level-results.json'),JSON.stringify({runtime:process.version,platform:process.platform,arch:process.arch,rows},null,2)+'\n'); } finally {await rm(work,{recursive:true,force:true});} ``` All assertions passed. The XML native-key analyzer call took 24.061 ms and its binary counterpart took 5.466 ms. These are single-call wall-clock observations. Each final XML case and binary counterpart ran once in the saved executions. Temporary bundles were removed. ### Expected behavior Recognize the serialized `UINibTopLevelObjectsKey` root and project its two top-level objects, using the array decoding already demonstrated by the controls. A separate generic `root` key is not needed to identify this explicitly named root. The repository's [UIKit archive fixture](https://github.com/morluto/rea/blob/9066444f6b8e9ffe1607a9b02cde3895a37db431/tests/fixtures/interface-builder/UIKitOutlet.xml#L77-L82) uses `UINibTopLevelObjectsKey`; its extra `root` masks the misspelled fallback. As an independent source reference, [Microsoft's UINib implementation](https://github.com/microsoft/WinObjC/blob/94f6b5bfdc09d945e30607b56124b6f94a5b9d7b/Frameworks/UIKit/UINib.mm#L104-L109) reads that correctly spelled key directly for NSKeyedUnarchiver input. This is source evidence, not a claimed native-consumer run. The expected contract does not require loading classes or executing a target. ### Impact and scope The demonstrated named-root archives lose recoverable top-level structure while objects and the outlet remain available. The result truthfully marks hierarchy unsupported, so this report does not claim false complete hierarchy coverage. The executed generic keyed-archive decoder retains `UINibTopLevelObjectsKey → UID 10` and array 10's references to objects 1 and 2, permitting manual inspection. The root-alias and misspelled-key cases are diagnostic controls, not suggested changes to inspected artifacts. Actual app prevalence and native Apple acceptance of this rootless synthetic variant were not measured. ### REA version `rea-agents` 6.7.0, source revision `9066444f6b8e9ffe1607a9b02cde3895a37db431`. This is a source-checkout reproduction with locked dependencies; it does not claim execution of the published tarball. The research assignment confirmed 6.7.0 as npm latest. ### Runtime and platform Node.js v24.22.1; Linux x64; source `tsx` loader. No build, install, full suite, Apple compiler, native provider, target process, or CLI/MCP adapter was run. Binary controls used Python `plistlib` to encode the same synthetic archive dictionaries with actual plist UID records. ### Target and artifact context Synthetic XML and binary Foundation keyed archives modeled on REA's existing UIKit outlet fixture: a UIViewController, a UIView, one UIRuntimeOutletConnection, NSArray wrappers and four class descriptors. `$top` names connections, objects and top-level objects. All UIDs are in bounds. No embedded prototype nib, segue, symlink, malformed plist or class-swapper behavior is involved. The analyzer receives a placeholder target digest; public target admission was not tested. ### Interface Other ### Earliest failure stage Native analysis or reconstruction ### Relevant output or evidence Reduced observed output: ```json { "variant": "native-key", "document": {"object_count":6,"connection_count":1,"hierarchy_complete":false}, "hierarchy": [], "hierarchyCoverage": { "status":"unsupported", "reason":"archive_hierarchy_not_decoded", "examined":0, "omitted":0 }, "truncated": false, "keyedRoots": { "UINibConnectionsKey":{"CF$UID":9}, "UINibObjectsKey":{"CF$UID":10}, "UINibTopLevelObjectsKey":{"CF$UID":10} } } ``` The `root-alias` and `misspelled-key` controls each emit `document → 1` and `document → 2` with complete hierarchy coverage (`examined: 2`, `omitted: 0`); all cases preserve the one `view` outlet. Binary controls give the same hierarchy outcomes with binary `UID` reference markers. Failing archive SHA-256: XML `80f1df200aedac1c2c14c3ad0796da0698c29346b3b0cb548387a38560733f83`; binary `456b930592513bea48aa4a351c3f52ef7b5d67c3f088afd22e4d25fdfe29e680`. REA source revision is the one above. This establishes source-boundary behavior, not a public CLI/MCP or native UIKit execution. ### Typed outcome or recovery expected Two observed hierarchy containment links from the document to the explicitly named top-level objects, with retained archive provenance. ### Analysis or suspected cause Source-derived: [the root selection](https://github.com/morluto/rea/blob/9066444f6b8e9ffe1607a9b02cde3895a37db431/src/domain/apple/interfaceBuilderKeyedArchive.ts#L496-L497) reads `top.root ?? top.UITopLevelObjectsKey`, omitting `Nib` from the real key. The existing array and object decoders already project the same members when either artificial control makes that array reachable. This isolates root-key selection from member decoding or outlet interpretation. ### Additional context Preliminary read-only open/closed searches for both key spellings found only related closed [#2177](https://github.com/morluto/rea/issues/2177), whose reported problem was missing UIKit outlets and whose retained fixture includes a masking `root` alias. The current probe's outlet already decodes correctly. Open [#5693](https://github.com/morluto/rea/issues/5693) is an AppKit object-parent registry omission; [#5739](https://github.com/morluto/rea/issues/5739) concerns embedded prototype archives. Neither describes this keyed UIKit named-root typo. Evidence provenance: executed production parser, bundle analyzer and generic keyed-archive decoder; local verification: reproduced; reproduction completeness: complete for the supplied synthetic XML and binary inputs. Native Apple consumer qualification is source-supported but not executed here. No implementation changes were made. ## Triage assessment - Impact level: P2 - Assessment status: supported - Impact basis: The demonstrated correctly named UIKit root is ignored, removing all recoverable top-level hierarchy links from otherwise decoded synthetic archives. Paired alias and spelling controls isolate the lookup defect; hierarchy incompleteness is disclosed and raw evidence remains available. - Workaround status: available - Workaround basis: The executed generic keyed-archive decoder preserves the named root, array UID 10 and its member UIDs for manual reconstruction. Altering the inspected archive or relying on the artificial misspelling is not proposed as a workaround.
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#4190
No description provided.