[GH-ISSUE #6662] [Bug] Authored archiveUID fields override Interface Builder object identities #4040

Closed
opened 2026-10-31 00:46:17 +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/6662

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

A valid authored integer field named archiveUID changes the object ID chosen by Interface Builder's NSKeyedArchiver parser. In the synthetic archive below, object-table entry 1 stores archiveUID: 2; entries 1 and 2 then both receive UI object ID "2". The final graph retains only the first view, drops the distinct second view, and reports the outlet to entry 1 as destination_object_missing_or_external even though that object is present.

All coverage facets are complete with zero omissions, hierarchy_complete is true, and graph.truncated is false. Renaming only the authored field from archiveUID to businessUID preserves both views and resolves the same outlet. A second production boundary, decodeKeyedArchiveBytes, retains object-table IDs 1 and 2 and their separate values for the failing bytes.

Reproduction

Actually executed: the following source-boundary probe generates two inert XML NSKeyedArchiver archives that differ only in the application field name. It invokes the domain parser, bundle analyzer and generic keyed-archive decoder, records their results, and removes the temporary bundles. ArchiveProbeView is a synthetic application class with declared NSView ancestry. No application class is loaded or executed.

Save this script as authored-identity-probe.mts in an existing scratch directory outside the checkout. From the source checkout with locked dependencies and Node.js v24.22.1, run this command with that directory substituted for the generic path:

probe_dir=/tmp/rea-apple-probe
node --import ./node_modules/tsx/dist/loader.mjs "$probe_dir/authored-identity-probe.mts" "$PWD" "$probe_dir"

The final argument retains the generated archives; it does not alter parsing. This is the executed probe, with session-specific paths supplied only as arguments:

import { mkdir, mkdtemp, writeFile, rm } from 'node:fs/promises';
import { join } from 'node:path';
import { tmpdir } from 'node:os';
import { pathToFileURL } from 'node:url';
const checkout = process.argv[2]!;
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(value: unknown): string {
  if (typeof value === 'number') return '<integer>' + value + '</integer>';
  if (typeof value === 'string') return '<string>' + value + '</string>';
  if (Array.isArray(value)) return '<array>' + value.map(xml).join('') + '</array>';
  return '<dict>' + Object.entries(value as object).map(([k, v]) => '<key>' + k + '</key>' + xml(v)).join('') + '</dict>';
}
const root = await mkdtemp(join(tmpdir(), 'rea-identity-probe-'));
try {
  for (const field of ['businessUID', 'archiveUID']) {
    const archive = { $archiver: 'NSKeyedArchiver', $version: 100000,
      $objects: ['$null',
        { $class: uid(4), [field]: 2, title: 'First', NSSubviews: [uid(2)] },
        { $class: uid(4), title: 'Second' },
        { $class: uid(5), NSSource: uid(2), NSDestination: uid(1), NSLabel: 'peer' },
        { $classname: 'ArchiveProbeView', $classes: ['ArchiveProbeView', 'NSView', 'NSObject'] },
        { $classname: 'NSNibOutletConnector', $classes: ['NSNibOutletConnector', 'NSNibConnector', 'NSObject'] }],
      $top: { root: uid(1) } };
    const bytes = Buffer.from('<?xml version="1.0"?><plist version="1.0">' + xml(archive) + '</plist>');
    if (process.argv[3]) await writeFile(join(process.argv[3], 'identity-' + field + '.nib'), bytes);
    const bundlePath = join(root, field + '.app');
    const resources = join(bundlePath, 'Contents', 'Resources');
    await mkdir(resources, { recursive: true });
    await writeFile(join(resources, 'Main.nib'), bytes);
    const parsed = parseInterfaceBuilderRecords(archive);
    const result = await analyzeInterfaceBuilderBundle({ bundlePath, targetSha256: 'a'.repeat(64) });
    const table = decodeKeyedArchiveBytes(bytes, { offset: 0, limit: 20 });
    console.log(JSON.stringify({ variant: field, parsedObjects: parsed.objects, document: result.documents[0],
      nodes: result.graph.nodes, edges: result.graph.edges,
      coverage: result.graph.coverage, truncated: result.graph.truncated,
      keyedObjects: table.objects.filter((o: any) => o.id === 1 || o.id === 2) }));
  }
} finally { await rm(root, { recursive: true, force: true }); }

The final saved execution exited 0 and observed the identity collision, missing view and false unresolved outlet, with the paired field-name control succeeding. Earlier probe setup initially omitted the required target binding; that invocation failed at result-schema validation and is not evidence of this defect.

Expected behavior

Keep the authored archiveUID: 2 as ordinary application data. Without an authored Interface Builder object ID, entries 1 and 2 must retain distinct identities corresponding to their object-table indices, and references to them must use the same identities. The outlet's serialized target is entry 1 and must remain resolvable.

REA's identity preservation contract and reference-stub implementation tie the stub's archiveUID to its actual table index. Its generic keyed-archive contract explicitly states that archive identity is the original object-table index, which that decoder also preserved in the executed control. None of these contracts reserve an application's serialized field named archiveUID as metadata.

Impact and scope

Observed: two distinct archived views coalesce into one graph node, a present outlet destination is labeled missing or external, and the result gives no omission warning. The original bytes, raw authored field and generic keyed-archive object table remain available. This is a bounded non-security graph-accuracy defect.

Only this XML fixture was executed. A binary-plist archive reaches the same domain identity selection in source, but the binary variant was not tested. Real-app prevalence and earlier known-good versions are unknown. The demonstrated fallback is inspecting the generic keyed-archive table, which preserved IDs 1 and 2 and both values; it does not automatically repair the Interface Builder UI graph.

REA version

rea-agents 6.7.0, source revision 23def04096869b85c4d3c14a73ed09168483c9f6. Version 6.7.0 was the latest published version checked during this investigation. This run exercised the source checkout above, not the release tarball.

Runtime and platform

Node.js v24.22.1; Linux x86_64; locked checkout dependencies, with the tsx loader. No Apple tool, macOS UI, native provider, target process, CLI/MCP adapter, full suite, or build was run.

Target and artifact context

Synthetic XML NSKeyedArchiver plist at Contents/Resources/Main.nib: nil entry, two ArchiveProbeView objects, one NSNibOutletConnector, and their class descriptors. Every reference is in bounds. The failing archive SHA-256 is fddace57330e0e9a0fe0f02b99c0603df4a1cec6cc3b1c2810957e4aa0f3b21a; the field-name control's hash is c6ffcf7963f26f5b2687dbf01b6d64b6393928e12145d841e9eba720395ed859. These are synthetic archives, not Apple compiler output.

Interface

Other

Earliest failure stage

Native analysis or reconstruction

Relevant output or evidence

Reduced from the saved results:

{
  "variant": "archiveUID",
  "objectIdentities": [
    {"id":"2","name":"First","class_name":"ArchiveProbeView"},
    {"id":"2","name":"Second","class_name":"ArchiveProbeView"},
    {"id":"3","name":"NSNibOutletConnector","class_name":"NSNibOutletConnector"}
  ],
  "views": [{"name":"First","id":"2"}],
  "outletTo": [{"to":null,"resolution":"unresolved","reason":"destination_object_missing_or_external"}],
  "document": {"object_count":3,"connection_count":1,"hierarchy_complete":true},
  "truncated": false
}

The control has view IDs "1" and "2", names First and Second, and an observed outlet target ending in object:["1"]. The generic keyed-archive decoder reports separate object IDs 1 and 2 for both inputs. All bundle-analysis coverage facets report complete with zero omissions in both cases.

Source references:

Defaults were used: max_documents: 64, max_objects: 20000, max_connections: 40000. Each case contains one small archive and stays far below those ceilings. The target SHA-256 supplied to the source-boundary analyzer is a placeholder, so public target admission is not covered.

Typed outcome or recovery expected

Distinct graph identities for the two observed objects, their preserved authored data, and an observed outlet target matching object-table entry 1. If identity is genuinely unavailable, retain the uncertainty and account for affected records; an application field is not evidence that the referenced object is missing.

Analysis or suspected cause

Source-derived cause: resolved archive fields and generated reference-stub metadata share the name archiveUID. The object parser consults an authored numeric value as if it were the stub's table index, but reference generation and hierarchy traversal continue to use the real table UID. A subsequent graph-map collision discards one view. The field-name control and generic keyed-archive decoder isolate this to Interface Builder identity selection rather than malformed XML, missing class information, or a dangling reference.

Additional context

The supplied open-issue snapshot had no matching authored-archiveUID identity report. This is distinct from #5638's ordinary one-key UID dictionaries: this fixture uses real CF$UID reference dictionaries and an ordinary numeric field inside a multi-field archived object. It is distinct from #5692's collection projection: the outlet targets a single local view.

Evidence provenance: observed production domain parser, bundle-analysis boundary and generic keyed-archive boundary; local verification: reproduced; reproduction completeness: complete for the supplied XML fixture and field-name control. Real Apple compilation, binary input and public adapters remain untested. Temporary bundles were removed. No source file was changed.

Triage assessment

  • Impact level: P2
  • Assessment status: supported
  • Impact basis: A particular ordinary application field deterministically coalesces two distinct views and breaks a valid outlet in returned UI evidence without an omission signal. The demonstrated failure is bounded; real-app prevalence is unknown.
  • Workaround status: available
  • Workaround basis: The executed generic keyed-archive decoder retained separate table IDs 1 and 2 and both authored values for the same failing bytes, allowing manual evidence inspection. It does not restore the automated Interface Builder graph, and renaming fields in someone else's archive is not proposed as a workaround.
Originally created by @coygeek on GitHub (Oct 23, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/6662 ### 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 A valid authored integer field named `archiveUID` changes the object ID chosen by Interface Builder's NSKeyedArchiver parser. In the synthetic archive below, object-table entry 1 stores `archiveUID: 2`; entries 1 and 2 then both receive UI object ID `"2"`. The final graph retains only the first view, drops the distinct second view, and reports the outlet to entry 1 as `destination_object_missing_or_external` even though that object is present. All coverage facets are complete with zero omissions, `hierarchy_complete` is true, and `graph.truncated` is false. Renaming only the authored field from `archiveUID` to `businessUID` preserves both views and resolves the same outlet. A second production boundary, `decodeKeyedArchiveBytes`, retains object-table IDs 1 and 2 and their separate values for the failing bytes. ### Reproduction Actually executed: the following source-boundary probe generates two inert XML NSKeyedArchiver archives that differ only in the application field name. It invokes the domain parser, bundle analyzer and generic keyed-archive decoder, records their results, and removes the temporary bundles. `ArchiveProbeView` is a synthetic application class with declared `NSView` ancestry. No application class is loaded or executed. Save this script as `authored-identity-probe.mts` in an existing scratch directory outside the checkout. From the source checkout with locked dependencies and Node.js v24.22.1, run this command with that directory substituted for the generic path: ```sh probe_dir=/tmp/rea-apple-probe node --import ./node_modules/tsx/dist/loader.mjs "$probe_dir/authored-identity-probe.mts" "$PWD" "$probe_dir" ``` The final argument retains the generated archives; it does not alter parsing. This is the executed probe, with session-specific paths supplied only as arguments: ```ts import { mkdir, mkdtemp, writeFile, rm } from 'node:fs/promises'; import { join } from 'node:path'; import { tmpdir } from 'node:os'; import { pathToFileURL } from 'node:url'; const checkout = process.argv[2]!; 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(value: unknown): string { if (typeof value === 'number') return '<integer>' + value + '</integer>'; if (typeof value === 'string') return '<string>' + value + '</string>'; if (Array.isArray(value)) return '<array>' + value.map(xml).join('') + '</array>'; return '<dict>' + Object.entries(value as object).map(([k, v]) => '<key>' + k + '</key>' + xml(v)).join('') + '</dict>'; } const root = await mkdtemp(join(tmpdir(), 'rea-identity-probe-')); try { for (const field of ['businessUID', 'archiveUID']) { const archive = { $archiver: 'NSKeyedArchiver', $version: 100000, $objects: ['$null', { $class: uid(4), [field]: 2, title: 'First', NSSubviews: [uid(2)] }, { $class: uid(4), title: 'Second' }, { $class: uid(5), NSSource: uid(2), NSDestination: uid(1), NSLabel: 'peer' }, { $classname: 'ArchiveProbeView', $classes: ['ArchiveProbeView', 'NSView', 'NSObject'] }, { $classname: 'NSNibOutletConnector', $classes: ['NSNibOutletConnector', 'NSNibConnector', 'NSObject'] }], $top: { root: uid(1) } }; const bytes = Buffer.from('<?xml version="1.0"?><plist version="1.0">' + xml(archive) + '</plist>'); if (process.argv[3]) await writeFile(join(process.argv[3], 'identity-' + field + '.nib'), bytes); const bundlePath = join(root, field + '.app'); const resources = join(bundlePath, 'Contents', 'Resources'); await mkdir(resources, { recursive: true }); await writeFile(join(resources, 'Main.nib'), bytes); const parsed = parseInterfaceBuilderRecords(archive); const result = await analyzeInterfaceBuilderBundle({ bundlePath, targetSha256: 'a'.repeat(64) }); const table = decodeKeyedArchiveBytes(bytes, { offset: 0, limit: 20 }); console.log(JSON.stringify({ variant: field, parsedObjects: parsed.objects, document: result.documents[0], nodes: result.graph.nodes, edges: result.graph.edges, coverage: result.graph.coverage, truncated: result.graph.truncated, keyedObjects: table.objects.filter((o: any) => o.id === 1 || o.id === 2) })); } } finally { await rm(root, { recursive: true, force: true }); } ``` The final saved execution exited 0 and observed the identity collision, missing view and false unresolved outlet, with the paired field-name control succeeding. Earlier probe setup initially omitted the required target binding; that invocation failed at result-schema validation and is not evidence of this defect. ### Expected behavior Keep the authored `archiveUID: 2` as ordinary application data. Without an authored Interface Builder object ID, entries 1 and 2 must retain distinct identities corresponding to their object-table indices, and references to them must use the same identities. The outlet's serialized target is entry 1 and must remain resolvable. REA's [identity preservation contract](https://github.com/morluto/rea/blob/23def04096869b85c4d3c14a73ed09168483c9f6/AGENTS.md#L15-L21) and [reference-stub implementation](https://github.com/morluto/rea/blob/23def04096869b85c4d3c14a73ed09168483c9f6/src/domain/apple/interfaceBuilderKeyedArchive.ts#L256-L268) tie the stub's `archiveUID` to its actual table index. Its generic keyed-archive contract explicitly states that [archive identity is the original object-table index](https://github.com/morluto/rea/blob/23def04096869b85c4d3c14a73ed09168483c9f6/src/domain/apple/keyedArchive.ts#L38-L49), which that decoder also preserved in the executed control. None of these contracts reserve an application's serialized field named `archiveUID` as metadata. ### Impact and scope Observed: two distinct archived views coalesce into one graph node, a present outlet destination is labeled missing or external, and the result gives no omission warning. The original bytes, raw authored field and generic keyed-archive object table remain available. This is a bounded non-security graph-accuracy defect. Only this XML fixture was executed. A binary-plist archive reaches the same domain identity selection in source, but the binary variant was not tested. Real-app prevalence and earlier known-good versions are unknown. The demonstrated fallback is inspecting the generic keyed-archive table, which preserved IDs 1 and 2 and both values; it does not automatically repair the Interface Builder UI graph. ### REA version `rea-agents` 6.7.0, source revision `23def04096869b85c4d3c14a73ed09168483c9f6`. Version 6.7.0 was the latest published version checked during this investigation. This run exercised the source checkout above, not the release tarball. ### Runtime and platform Node.js v24.22.1; Linux x86_64; locked checkout dependencies, with the `tsx` loader. No Apple tool, macOS UI, native provider, target process, CLI/MCP adapter, full suite, or build was run. ### Target and artifact context Synthetic XML NSKeyedArchiver plist at `Contents/Resources/Main.nib`: nil entry, two `ArchiveProbeView` objects, one `NSNibOutletConnector`, and their class descriptors. Every reference is in bounds. The failing archive SHA-256 is `fddace57330e0e9a0fe0f02b99c0603df4a1cec6cc3b1c2810957e4aa0f3b21a`; the field-name control's hash is `c6ffcf7963f26f5b2687dbf01b6d64b6393928e12145d841e9eba720395ed859`. These are synthetic archives, not Apple compiler output. ### Interface Other ### Earliest failure stage Native analysis or reconstruction ### Relevant output or evidence Reduced from the saved results: ```json { "variant": "archiveUID", "objectIdentities": [ {"id":"2","name":"First","class_name":"ArchiveProbeView"}, {"id":"2","name":"Second","class_name":"ArchiveProbeView"}, {"id":"3","name":"NSNibOutletConnector","class_name":"NSNibOutletConnector"} ], "views": [{"name":"First","id":"2"}], "outletTo": [{"to":null,"resolution":"unresolved","reason":"destination_object_missing_or_external"}], "document": {"object_count":3,"connection_count":1,"hierarchy_complete":true}, "truncated": false } ``` The control has view IDs `"1"` and `"2"`, names `First` and `Second`, and an observed outlet target ending in `object:["1"]`. The generic keyed-archive decoder reports separate object IDs 1 and 2 for both inputs. All bundle-analysis coverage facets report complete with zero omissions in both cases. Source references: - [Authored numeric `fields.archiveUID` takes precedence over the actual table index](https://github.com/morluto/rea/blob/23def04096869b85c4d3c14a73ed09168483c9f6/src/domain/apple/interfaceBuilderKeyedArchive.ts#L397-L416). - [Reference stubs still use the true referenced UID](https://github.com/morluto/rea/blob/23def04096869b85c4d3c14a73ed09168483c9f6/src/domain/apple/interfaceBuilderKeyedArchive.ts#L256-L268). - [Duplicate graph node IDs return success without retaining the second node](https://github.com/morluto/rea/blob/23def04096869b85c4d3c14a73ed09168483c9f6/src/domain/apple/interfaceBuilderGraph.ts#L104-L113). - [Parsed object IDs directly key graph objects](https://github.com/morluto/rea/blob/23def04096869b85c4d3c14a73ed09168483c9f6/src/domain/apple/interfaceBuilderGraph.ts#L141-L156). Defaults were used: `max_documents: 64`, `max_objects: 20000`, `max_connections: 40000`. Each case contains one small archive and stays far below those ceilings. The target SHA-256 supplied to the source-boundary analyzer is a placeholder, so public target admission is not covered. ### Typed outcome or recovery expected Distinct graph identities for the two observed objects, their preserved authored data, and an observed outlet target matching object-table entry 1. If identity is genuinely unavailable, retain the uncertainty and account for affected records; an application field is not evidence that the referenced object is missing. ### Analysis or suspected cause Source-derived cause: resolved archive fields and generated reference-stub metadata share the name `archiveUID`. The object parser consults an authored numeric value as if it were the stub's table index, but reference generation and hierarchy traversal continue to use the real table UID. A subsequent graph-map collision discards one view. The field-name control and generic keyed-archive decoder isolate this to Interface Builder identity selection rather than malformed XML, missing class information, or a dangling reference. ### Additional context The supplied open-issue snapshot had no matching authored-`archiveUID` identity report. This is distinct from #5638's ordinary one-key UID dictionaries: this fixture uses real `CF$UID` reference dictionaries and an ordinary numeric field inside a multi-field archived object. It is distinct from #5692's collection projection: the outlet targets a single local view. Evidence provenance: observed production domain parser, bundle-analysis boundary and generic keyed-archive boundary; local verification: reproduced; reproduction completeness: complete for the supplied XML fixture and field-name control. Real Apple compilation, binary input and public adapters remain untested. Temporary bundles were removed. No source file was changed. ## Triage assessment - Impact level: P2 - Assessment status: supported - Impact basis: A particular ordinary application field deterministically coalesces two distinct views and breaks a valid outlet in returned UI evidence without an omission signal. The demonstrated failure is bounded; real-app prevalence is unknown. - Workaround status: available - Workaround basis: The executed generic keyed-archive decoder retained separate table IDs 1 and 2 and both authored values for the same failing bytes, allowing manual evidence inspection. It does not restore the automated Interface Builder graph, and renaming fields in someone else's archive 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#4040
No description provided.