[GH-ISSUE #3175] [Bug] Keyed UIKit event connections are missing from the action graph #1112

Closed
opened 2026-10-15 18:31:10 +00:00 by bsakaguchi · 1 comment
Owner

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

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

rea decode-interface-builder accepts a synthetic keyed nib containing UIRuntimeEventConnection, but reports that connection as unsupported. The public CLI exits successfully and returns one decoded document and one counted connection, with graph.truncated: false. It produces no action nodes, no objc_selector nodes and no target_action edges for the button and controller.

The connection appears as an unresolved contains edge with reason: "unsupported_connection_type" and to: null. The unknown connector node still retains UILabel: "tapped:", UIEventMask: 64, UISource.archiveUID: 1 and UIDestination.archiveUID: 2. A separate observed hierarchy edge links the document to the connector. The defect concerns the typed action graph; the raw archived facts remain available.

The CLI reproduction used REA_ANALYSIS_PROVIDER=auto and the artifact graph provider. No native analysis engine is required for this static decoding operation.

Reproduction

  1. Start from a clean checkout at 58300885207133422af235e0f2b10658cf958b03 and prepare its runtime:

    npm ci
    npm run build:cached
    
  2. Create an isolated fixture directory and home/config directories from the repository root:

    mkdir -p .repro-keyed-event/home
    export HOME="$PWD/.repro-keyed-event/home"
    export XDG_CONFIG_HOME="$HOME/config"
    export XDG_CACHE_HOME="$HOME/cache"
    export XDG_DATA_HOME="$HOME/data"
    
  3. Create the complete app bundle and keyed nib inline:

    python3 - <<'PYTHON'
    from pathlib import Path
    import plistlib
    import struct
    
    app = Path('.repro-keyed-event/Example.app')
    resources = app / 'Contents/Resources'
    resources.mkdir(parents=True, exist_ok=True)
    macos = app / 'Contents/MacOS'
    macos.mkdir(parents=True, exist_ok=True)
    (macos / 'App').write_bytes(
        struct.pack('>8I', 0xfeedfacf, 0x0100000c, 0, 2, 0, 0, 0, 0)
    )
    (app / 'Contents/Info.plist').write_bytes(
        plistlib.dumps({'CFBundleExecutable': 'App'})
    )
    def uid(index):
        return {'CF$UID': index}
    objects = [
        '$null',
        {'$class': uid(5)},
        {'$class': uid(6)},
        {'$class': uid(7), 'UISource': uid(1), 'UIDestination': uid(2),
         'UILabel': uid(4), 'UIEventMask': 64},
        {'$class': uid(8), 'NS.string': 'tapped:'},
        {'$classname': 'UIButton',
         '$classes': ['UIButton', 'UIControl', 'UIView', 'NSObject']},
        {'$classname': 'UIViewController',
         '$classes': ['UIViewController', 'NSObject']},
        {'$classname': 'UIRuntimeEventConnection',
         '$classes': ['UIRuntimeEventConnection', 'NSObject']},
        {'$classname': 'NSString', '$classes': ['NSString', 'NSObject']},
    ]
    archive = {'$archiver': 'NSKeyedArchiver', '$version': 100000,
               '$objects': objects, '$top': {'root': uid(3)}}
    (resources / 'Main.nib').write_bytes(plistlib.dumps(archive, sort_keys=False))
    PYTHON
    
  4. Decode it through the CLI:

    REA_LOG_LEVEL=silent REA_ANALYSIS_PROVIDER=auto   node scripts/rea.mjs decode-interface-builder   .repro-keyed-event/Example.app --format json   > .repro-keyed-event/result.json
    
  5. Print the relevant typed facts and retained raw attributes:

    python3 - <<'PYTHON'
    from pathlib import Path
    import json
    result = json.loads(Path('.repro-keyed-event/result.json').read_text())
    analysis = result['normalized_result']
    graph = analysis['graph']
    connector = next(n for n in graph['nodes']
                     if n['name'] == 'UIRuntimeEventConnection')
    print(json.dumps({
        'connection_count': analysis['documents'][0]['connection_count'],
        'actions': [n['name'] for n in graph['nodes'] if n['kind'] == 'action'],
        'selectors': [n['name'] for n in graph['nodes']
                      if n['kind'] == 'objc_selector'],
        'target_action_count': sum(e['relation'] == 'target_action'
                                   for e in graph['edges']),
        'unresolved_edges': [{k: e.get(k) for k in
                              ('relation', 'resolution', 'reason', 'to')}
                             for e in graph['edges']
                             if e['resolution'] == 'unresolved'],
        'retained_selector': connector['attributes']['UILabel'],
        'retained_event_mask': connector['attributes']['UIEventMask'],
        'retained_source_uid': connector['attributes']['UISource']['archiveUID'],
        'retained_target_uid': connector['attributes']['UIDestination']['archiveUID'],
        'truncated': graph['truncated'],
    }, indent=2))
    PYTHON
    

    The public CLI invocation and these archive contents were independently reproduced against the prepared baseline runtime. Dependency installation and building are clean-checkout prerequisites; they were not rerun during verification.

Expected behavior

docs/native-investigation.md:324-338 documents decoding compiled nibs into objects, actions and connections. src/contracts/artifactToolContracts.ts:90-96 explicitly supports keyed property lists and NIBArchive object tables. tests/boundary/filesystem/interfaceBuilderBundle.test.ts:1044-1109 establishes action semantics for the same UIKit event class in NIBArchive.

The keyed connection should produce an action named tapped:, preserve event-mask value 64, expose the selector, and produce observed target_action relationships from UIButton to the action and from the action to UIViewController. A matching method implementation in the executable is not required to report the archived action facts.

Impact and scope

The missing action and selector nodes and incorrect connection representation can mislead consumers of typed action graphs. The verified case is a supported synthetic XML NSKeyedArchiver nib. Its frequency in compiled third-party applications was not measured. The decoder succeeds without truncation and does not mutate the target.

Manual recovery is available because the returned connector attributes retain the selector, event mask and both endpoint UIDs. Callers can inspect those attributes and match the UIDs to Interface Builder object IDs. That workaround recovers the archived facts but does not restore typed action edges for automated graph consumers.

The verifier changed only the connection class name to UIRuntimeActionConnection and observed action tapped:, an objc_selector node and three target_action edges, including the correct button and controller endpoints. An executed keyed outlet control produced an observed outlet_to edge. A focused NIBArchive producer control recognized UIRuntimeEventConnection as an action with event-mask value 64. These comparisons distinguish the keyed event-classification failure from endpoint resolution and graph limits.

REA version

6.4.0 (main at 5830088520)

Runtime and platform

Node 26.10.0; macOS arm64. The verifier did not state the macOS version.

Target and artifact context

Synthetic Apple app bundle with a Mach-O header executable, an executable-resolving Info.plist, and an XML NSKeyedArchiver Main.nib. The nib contains a UIButton, UIViewController, UIRuntimeEventConnection, an NSString label tapped:, and UIEventMask: 64.

Interface

CLI

Earliest failure stage

Native analysis or reconstruction

Relevant output or evidence

connection_count: 1
actions: []
selectors: []
target_action_count: 0
unresolved connection: {"relation":"contains","resolution":"unresolved","reason":"unsupported_connection_type","to":null}
retained_selector: tapped:
retained_event_mask: 64
retained_source_uid: 1
retained_target_uid: 2
truncated: false

Typed outcome or recovery expected

The same fixture should return action tapped:, its selector, event-mask value 64, and observed target_action edges from the button to the action and from the action to the controller. It should no longer report unsupported_connection_type for this connection, and the raw archived values should remain available. Either existing event-mask attribute spelling can preserve the value.

Preserve existing NIBArchive event-action behavior and keyed outlet behavior. As a malformed-reference control, remove UISource from the connector and rerun. That control currently retains connection_count: 1, partial coverage with reason connection_unresolved, and one omitted connection. It must continue to report the unresolved connection rather than silently drop it or invent a resolved source.

Analysis or suspected cause

Source-derived at the stated baseline: src/domain/apple/interfaceBuilderKeyedArchive.ts:359-406 recognizes the connection and resolves its UI-prefixed endpoints and label. Its classifier at src/domain/apple/interfaceBuilderKeyedArchive.ts:521-529 accepts outlet/action/segue substrings. UIRuntimeEventConnection matches none of those and returns other. The unknown-connection branch at src/domain/apple/interfaceBuilderGraph.ts:394-403 projects that kind as the observed unsupported connection edge.

The sibling producer at src/artifacts/apple/InterfaceBuilderAnalysis.ts:547-577 maps UIKit event connections in NIBArchive tables to actions and preserves the event mask. The independently executed class-name comparison and NIBArchive control support the classification diagnosis.

Additional context

Related closed issue #2177 restores keyed UIKit outlet fields, merged PR #913 restores NIBArchive UIKit event actions, and merged PR #2484 records ownerless connector omissions; this report concerns keyed event-action classification after endpoint and label resolution already succeeds.

The verified assessment is P2 because this is a bounded static graph correctness defect with raw facts retained and an available manual recovery path. Real-app prevalence remains unmeasured. Live MCP transport, native app execution and real ibtool compilation were not exercised; the public CLI provides the primary failure evidence.

Originally created by @coygeek on GitHub (Oct 14, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/3175 ### 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 `rea decode-interface-builder` accepts a synthetic keyed nib containing `UIRuntimeEventConnection`, but reports that connection as unsupported. The public CLI exits successfully and returns one decoded document and one counted connection, with `graph.truncated: false`. It produces no action nodes, no `objc_selector` nodes and no `target_action` edges for the button and controller. The connection appears as an unresolved `contains` edge with `reason: "unsupported_connection_type"` and `to: null`. The unknown connector node still retains `UILabel: "tapped:"`, `UIEventMask: 64`, `UISource.archiveUID: 1` and `UIDestination.archiveUID: 2`. A separate observed hierarchy edge links the document to the connector. The defect concerns the typed action graph; the raw archived facts remain available. The CLI reproduction used `REA_ANALYSIS_PROVIDER=auto` and the artifact graph provider. No native analysis engine is required for this static decoding operation. ### Reproduction 1. Start from a clean checkout at `58300885207133422af235e0f2b10658cf958b03` and prepare its runtime: ```sh npm ci npm run build:cached ``` 2. Create an isolated fixture directory and home/config directories from the repository root: ```sh mkdir -p .repro-keyed-event/home export HOME="$PWD/.repro-keyed-event/home" export XDG_CONFIG_HOME="$HOME/config" export XDG_CACHE_HOME="$HOME/cache" export XDG_DATA_HOME="$HOME/data" ``` 3. Create the complete app bundle and keyed nib inline: ```sh python3 - <<'PYTHON' from pathlib import Path import plistlib import struct app = Path('.repro-keyed-event/Example.app') resources = app / 'Contents/Resources' resources.mkdir(parents=True, exist_ok=True) macos = app / 'Contents/MacOS' macos.mkdir(parents=True, exist_ok=True) (macos / 'App').write_bytes( struct.pack('>8I', 0xfeedfacf, 0x0100000c, 0, 2, 0, 0, 0, 0) ) (app / 'Contents/Info.plist').write_bytes( plistlib.dumps({'CFBundleExecutable': 'App'}) ) def uid(index): return {'CF$UID': index} objects = [ '$null', {'$class': uid(5)}, {'$class': uid(6)}, {'$class': uid(7), 'UISource': uid(1), 'UIDestination': uid(2), 'UILabel': uid(4), 'UIEventMask': 64}, {'$class': uid(8), 'NS.string': 'tapped:'}, {'$classname': 'UIButton', '$classes': ['UIButton', 'UIControl', 'UIView', 'NSObject']}, {'$classname': 'UIViewController', '$classes': ['UIViewController', 'NSObject']}, {'$classname': 'UIRuntimeEventConnection', '$classes': ['UIRuntimeEventConnection', 'NSObject']}, {'$classname': 'NSString', '$classes': ['NSString', 'NSObject']}, ] archive = {'$archiver': 'NSKeyedArchiver', '$version': 100000, '$objects': objects, '$top': {'root': uid(3)}} (resources / 'Main.nib').write_bytes(plistlib.dumps(archive, sort_keys=False)) PYTHON ``` 4. Decode it through the CLI: ```sh REA_LOG_LEVEL=silent REA_ANALYSIS_PROVIDER=auto node scripts/rea.mjs decode-interface-builder .repro-keyed-event/Example.app --format json > .repro-keyed-event/result.json ``` 5. Print the relevant typed facts and retained raw attributes: ```sh python3 - <<'PYTHON' from pathlib import Path import json result = json.loads(Path('.repro-keyed-event/result.json').read_text()) analysis = result['normalized_result'] graph = analysis['graph'] connector = next(n for n in graph['nodes'] if n['name'] == 'UIRuntimeEventConnection') print(json.dumps({ 'connection_count': analysis['documents'][0]['connection_count'], 'actions': [n['name'] for n in graph['nodes'] if n['kind'] == 'action'], 'selectors': [n['name'] for n in graph['nodes'] if n['kind'] == 'objc_selector'], 'target_action_count': sum(e['relation'] == 'target_action' for e in graph['edges']), 'unresolved_edges': [{k: e.get(k) for k in ('relation', 'resolution', 'reason', 'to')} for e in graph['edges'] if e['resolution'] == 'unresolved'], 'retained_selector': connector['attributes']['UILabel'], 'retained_event_mask': connector['attributes']['UIEventMask'], 'retained_source_uid': connector['attributes']['UISource']['archiveUID'], 'retained_target_uid': connector['attributes']['UIDestination']['archiveUID'], 'truncated': graph['truncated'], }, indent=2)) PYTHON ``` The public CLI invocation and these archive contents were independently reproduced against the prepared baseline runtime. Dependency installation and building are clean-checkout prerequisites; they were not rerun during verification. ### Expected behavior `docs/native-investigation.md:324-338` documents decoding compiled nibs into objects, actions and connections. `src/contracts/artifactToolContracts.ts:90-96` explicitly supports keyed property lists and NIBArchive object tables. `tests/boundary/filesystem/interfaceBuilderBundle.test.ts:1044-1109` establishes action semantics for the same UIKit event class in NIBArchive. The keyed connection should produce an action named `tapped:`, preserve event-mask value 64, expose the selector, and produce observed `target_action` relationships from `UIButton` to the action and from the action to `UIViewController`. A matching method implementation in the executable is not required to report the archived action facts. ### Impact and scope The missing action and selector nodes and incorrect connection representation can mislead consumers of typed action graphs. The verified case is a supported synthetic XML NSKeyedArchiver nib. Its frequency in compiled third-party applications was not measured. The decoder succeeds without truncation and does not mutate the target. Manual recovery is available because the returned connector attributes retain the selector, event mask and both endpoint UIDs. Callers can inspect those attributes and match the UIDs to Interface Builder object IDs. That workaround recovers the archived facts but does not restore typed action edges for automated graph consumers. The verifier changed only the connection class name to `UIRuntimeActionConnection` and observed action `tapped:`, an `objc_selector` node and three `target_action` edges, including the correct button and controller endpoints. An executed keyed outlet control produced an observed `outlet_to` edge. A focused NIBArchive producer control recognized `UIRuntimeEventConnection` as an action with event-mask value 64. These comparisons distinguish the keyed event-classification failure from endpoint resolution and graph limits. ### REA version 6.4.0 (main at 58300885207133422af235e0f2b10658cf958b03) ### Runtime and platform Node 26.10.0; macOS arm64. The verifier did not state the macOS version. ### Target and artifact context Synthetic Apple app bundle with a Mach-O header executable, an executable-resolving `Info.plist`, and an XML NSKeyedArchiver `Main.nib`. The nib contains a `UIButton`, `UIViewController`, `UIRuntimeEventConnection`, an `NSString` label `tapped:`, and `UIEventMask: 64`. ### Interface CLI ### Earliest failure stage Native analysis or reconstruction ### Relevant output or evidence ```text connection_count: 1 actions: [] selectors: [] target_action_count: 0 unresolved connection: {"relation":"contains","resolution":"unresolved","reason":"unsupported_connection_type","to":null} retained_selector: tapped: retained_event_mask: 64 retained_source_uid: 1 retained_target_uid: 2 truncated: false ``` ### Typed outcome or recovery expected The same fixture should return action `tapped:`, its selector, event-mask value 64, and observed `target_action` edges from the button to the action and from the action to the controller. It should no longer report `unsupported_connection_type` for this connection, and the raw archived values should remain available. Either existing event-mask attribute spelling can preserve the value. Preserve existing NIBArchive event-action behavior and keyed outlet behavior. As a malformed-reference control, remove `UISource` from the connector and rerun. That control currently retains `connection_count: 1`, partial coverage with reason `connection_unresolved`, and one omitted connection. It must continue to report the unresolved connection rather than silently drop it or invent a resolved source. ### Analysis or suspected cause Source-derived at the stated baseline: `src/domain/apple/interfaceBuilderKeyedArchive.ts:359-406` recognizes the connection and resolves its UI-prefixed endpoints and label. Its classifier at `src/domain/apple/interfaceBuilderKeyedArchive.ts:521-529` accepts outlet/action/segue substrings. `UIRuntimeEventConnection` matches none of those and returns `other`. The unknown-connection branch at `src/domain/apple/interfaceBuilderGraph.ts:394-403` projects that kind as the observed unsupported connection edge. The sibling producer at `src/artifacts/apple/InterfaceBuilderAnalysis.ts:547-577` maps UIKit event connections in NIBArchive tables to actions and preserves the event mask. The independently executed class-name comparison and NIBArchive control support the classification diagnosis. ### Additional context Related closed issue #2177 restores keyed UIKit outlet fields, merged PR [#913](https://github.com/morluto/rea/pull/913) restores NIBArchive UIKit event actions, and merged PR [#2484](https://github.com/morluto/rea/pull/2484) records ownerless connector omissions; this report concerns keyed event-action classification after endpoint and label resolution already succeeds. The verified assessment is P2 because this is a bounded static graph correctness defect with raw facts retained and an available manual recovery path. Real-app prevalence remains unmeasured. Live MCP transport, native app execution and real `ibtool` compilation were not exercised; the public CLI provides the primary failure evidence.
bsakaguchi 2026-10-15 18:31:10 +00:00
  • closed this issue
  • added the
    bug
    label
Author
Owner

@morluto commented on GitHub (Oct 14, 2026):

Delivered on main in fffb07419.

Keyed-archive classification omitted UIRuntimeEventConnection while a sibling projection already understood it. The shared owner now retains it as an action connection, including the event mask.

Validation: Keyed archive/Interface Builder graph tests and Apple artifact qualification; the original application was not replayed. Full exact-revision qualification passed.

<!-- gh-comment-id:6177606231 --> @morluto commented on GitHub (Oct 14, 2026): Delivered on main in [fffb07419](https://github.com/morluto/rea/commit/fffb0741936067530d1ce9777461cd53e87398c2). Keyed-archive classification omitted UIRuntimeEventConnection while a sibling projection already understood it. The shared owner now retains it as an action connection, including the event mask. Validation: Keyed archive/Interface Builder graph tests and Apple artifact qualification; the original application was not replayed. Full exact-revision [qualification](https://github.com/morluto/rea/actions/runs/38629313022) passed.
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#1112
No description provided.