[GH-ISSUE #5635] [Bug] inspect-asset-catalog omits AppKit NSCustomResource image references #3414

Closed
opened 2026-10-31 00:40:06 +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/5635

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

inspect-asset-catalog returns the Brand / Brand.png rendition from a generated app's real Assets.car, but returns resource_key_matches: [] despite an AppKit menu item explicitly using that image. decode-interface-builder retains the reference as NSCustomResource with nibValues.NSClassName: "NSImage" and nibValues.NSResourceName: "Brand", with kind: "unknown" and graph.truncated: false. A second explicit reference, MissingBrand, also produces no row. Both commands exit 0.

Reproduction

Prerequisites: macOS, Xcode providing actool/ibtool, system assetutil, Python 3 and jq; a source checkout with locked dependencies installed and CLI built. Run this from the checkout root with Bash. The generated app is inspected as data and never launched; HOME/XDG/state directories are isolated. The verifier executed the fixture, and a separate submitter replay reproduced the same result at the baseline below.

set -eu
work_dir="$(mktemp -d)"
mkdir -p "$work_dir"/{home,cfg,data,cache,state,tmp,build} "$work_dir/C.xcassets/Brand.imageset" "$work_dir/App.app/Contents/MacOS" "$work_dir/App.app/Contents/Resources"
cat > "$work_dir/C.xcassets/Contents.json" <<'JSON'
{"info":{"author":"xcode","version":1}}
JSON
cat > "$work_dir/C.xcassets/Brand.imageset/Contents.json" <<'JSON'
{"images":[{"idiom":"universal","scale":"1x","filename":"Brand.png"}],"info":{"author":"xcode","version":1}}
JSON
python3 - "$work_dir/C.xcassets/Brand.imageset/Brand.png" <<'PNG'
import struct,sys,zlib
def chunk(k,d):
    return struct.pack(">I",len(d))+k+d+struct.pack(">I",zlib.crc32(k+d)&0xffffffff)
open(sys.argv[1],"wb").write(b"\x89PNG\r\n\x1a\n"+chunk(b"IHDR",struct.pack(">IIBBBBB",1,1,8,6,0,0,0))+chunk(b"IDAT",zlib.compress(b"\x00\x00\x00\xff\xff"))+chunk(b"IEND",b""))
PNG
xcrun actool --compile "$work_dir/build" --platform macosx --minimum-deployment-target 14.0 --output-partial-info-plist "$work_dir/build/partial.plist" "$work_dir/C.xcassets" > "$work_dir/actool.txt"
cat > "$work_dir/Menu.xib" <<'XML'
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="23000" targetRuntime="MacOSX.Cocoa">
<dependencies><deployment identifier="macosx"/><plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="23000"/></dependencies>
<objects>
<customObject id="-2" userLabel="File's Owner" customClass="NSApplication"/>
<customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/>
<customObject id="-3" userLabel="Application" customClass="NSObject"/>
<menu title="Main" systemMenu="main" id="m1"><items>
<menuItem title="Item" image="Brand" id="mi1"><modifierMask key="keyEquivalentModifierMask"/></menuItem>
<menuItem title="Missing" image="MissingBrand" id="mi2"><modifierMask key="keyEquivalentModifierMask"/></menuItem>
</items></menu>
</objects></document>
XML
xcrun ibtool --compile "$work_dir/App.app/Contents/Resources/Menu.nib" "$work_dir/Menu.xib" > "$work_dir/ibtool.txt"
cp "$work_dir/build/Assets.car" "$work_dir/App.app/Contents/Resources/Assets.car"
cp /usr/bin/true "$work_dir/App.app/Contents/MacOS/App"
cat > "$work_dir/App.app/Contents/Info.plist" <<'PLIST'
<plist version="1.0"><dict><key>CFBundleIdentifier</key><string>test.resource</string><key>CFBundleExecutable</key><string>App</string><key>CFBundlePackageType</key><string>APPL</string></dict></plist>
PLIST
run_rea() {
  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 "$@" --json
}
run_rea inspect-asset-catalog "$work_dir/App.app" > "$work_dir/catalog.json"
run_rea decode-interface-builder "$work_dir/App.app" > "$work_dir/decoded.json"
/usr/bin/assetutil --info "$work_dir/build/Assets.car" > "$work_dir/assetutil.json"
jq '.normalized_result | {records: [.records[] | {kind,asset_name,rendition_name}],resource_key_matches}' "$work_dir/catalog.json"
jq '.normalized_result.graph | {truncated,nodes: [.nodes[] | select(.attributes.class_name == "NSCustomResource") | {kind,attributes}]}' "$work_dir/decoded.json"
jq '[.[] | select(.Name == "Brand") | {Name,RenditionName}]' "$work_dir/assetutil.json"

Expected behavior

Return Brand as matched with the single Brand.png rendition, and MissingBrand as unmatched with no renditions. Preserve provenance and keep unrelated menu titles out of resource-key rows.

docs/native-investigation.md:356-359 documents exact UI resource-name matches for app-bundle asset-catalog inspection. This ibtool-produced archive explicitly identifies a named image, and assetutil independently confirms its exact catalog name. No rendition-byte extraction or arbitrary-string inference is needed.

Impact and scope

P2: the documented join omits explicit image-reference facts for this observed AppKit encoding, including a reference whose rendition appears in the same result. Broader prevalence was not measured. The generic limitation about recognized fields remains present; an empty join does not prove asset absence. A practical workaround is to join retained NSClassName/NSResourceName values from decode-interface-builder with the catalog's asset_name manually. No live application was launched or modified.

REA version

rea-agents 6.7.0, source commit 5b676ba9f302228b44c28ab076525abed3e647c1. Freshly fetched origin/main at edbd291a28a70e2c0b984ff374d5a6b437280e82: none of the cited implementation/documentation files changed since the baseline. This main recheck is source comparison, not a runtime execution of that newer revision.

Runtime and platform

Node.js v26.12.0; macOS 27.0.1; arm64; Xcode 27.1 (27A9275); system assetutil. Hopper/Ghidra are not involved.

Target and artifact context

Generated inert macOS app wrapper with a copied system executable, an actool-compiled asset catalog containing one Brand rendition, and an ibtool-compiled AppKit menu nib containing Brand and MissingBrand image references.

Interface

CLI

Earliest failure stage

Native analysis or reconstruction

Relevant output or evidence

Observed catalog subset:

{
  "records": [
    {"kind":"catalog","asset_name":null,"rendition_name":null},
    {"kind":"rendition","asset_name":"Brand","rendition_name":"Brand.png"}
  ],
  "resource_key_matches": []
}

Observed decoded node subset, with graph.truncated: false:

{
  "kind": "unknown",
  "attributes": {
    "class_name": "NSCustomResource",
    "nibValues": {"NSClassName":"NSImage","NSResourceName":"Brand"}
  }
}

The graph also retains MissingBrand, NSMenuCheckmark and NSMenuMixedState with that shape; none produces a resource-key row. Independent assetutil --info lists exactly one Name: "Brand", RenditionName: "Brand.png" record. Evidence provenance: observed through the public CLI and real Apple tools, without substituted catalog metadata.

Typed outcome or recovery expected

Exact resource-key match rows with source node/archive provenance. For restoration, check the same fixture for one matched Brand rendition and an unmatched MissingBrand; keep system image references distinguishable from catalog matches and exclude menu titles Main, Item and Missing. These are proposed post-fix checks; no fix was tested.

Analysis or suspected cause

Source-derived reasoning: the recognized-field set excludes NSResourceName. The NIBArchive producer projects only recognized resource fields, while the collector requires kind: "resource" and recognized direct attributes. The observed NSCustomResource node fails both conditions, so a field-only addition would not establish restoration. This is REA's resource interpretation, not an identified bundled-dependency failure.

Additional context

Still reproduces after #2176 and its merged repair #2248, commit fd985dbc5b9435918732c0cc1825060af2a3a47e, which is already in the tested baseline. That report concerns recognized NSImageName/imageName fields on decoded NSImage NIBArchive resource nodes. This report is limited to the uncovered NSCustomResource / NSClassName: NSImage / NSResourceName encoding produced by ibtool. The related PRs #2249 and #2256 were consolidated into #2248.

Prepared with AI assistance and independently reproduced. No earlier known-good version was established; MCP and other platforms were not tested.

Originally created by @coygeek on GitHub (Oct 20, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/5635 ### 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 `inspect-asset-catalog` returns the `Brand` / `Brand.png` rendition from a generated app's real `Assets.car`, but returns `resource_key_matches: []` despite an AppKit menu item explicitly using that image. `decode-interface-builder` retains the reference as `NSCustomResource` with `nibValues.NSClassName: "NSImage"` and `nibValues.NSResourceName: "Brand"`, with `kind: "unknown"` and `graph.truncated: false`. A second explicit reference, `MissingBrand`, also produces no row. Both commands exit 0. ### Reproduction Prerequisites: macOS, Xcode providing actool/ibtool, system assetutil, Python 3 and jq; a source checkout with locked dependencies installed and CLI built. Run this from the checkout root with Bash. The generated app is inspected as data and never launched; HOME/XDG/state directories are isolated. The verifier executed the fixture, and a separate submitter replay reproduced the same result at the baseline below. ```sh set -eu work_dir="$(mktemp -d)" mkdir -p "$work_dir"/{home,cfg,data,cache,state,tmp,build} "$work_dir/C.xcassets/Brand.imageset" "$work_dir/App.app/Contents/MacOS" "$work_dir/App.app/Contents/Resources" cat > "$work_dir/C.xcassets/Contents.json" <<'JSON' {"info":{"author":"xcode","version":1}} JSON cat > "$work_dir/C.xcassets/Brand.imageset/Contents.json" <<'JSON' {"images":[{"idiom":"universal","scale":"1x","filename":"Brand.png"}],"info":{"author":"xcode","version":1}} JSON python3 - "$work_dir/C.xcassets/Brand.imageset/Brand.png" <<'PNG' import struct,sys,zlib def chunk(k,d): return struct.pack(">I",len(d))+k+d+struct.pack(">I",zlib.crc32(k+d)&0xffffffff) open(sys.argv[1],"wb").write(b"\x89PNG\r\n\x1a\n"+chunk(b"IHDR",struct.pack(">IIBBBBB",1,1,8,6,0,0,0))+chunk(b"IDAT",zlib.compress(b"\x00\x00\x00\xff\xff"))+chunk(b"IEND",b"")) PNG xcrun actool --compile "$work_dir/build" --platform macosx --minimum-deployment-target 14.0 --output-partial-info-plist "$work_dir/build/partial.plist" "$work_dir/C.xcassets" > "$work_dir/actool.txt" cat > "$work_dir/Menu.xib" <<'XML' <?xml version="1.0" encoding="UTF-8"?> <document type="com.apple.InterfaceBuilder3.Cocoa.XIB" version="3.0" toolsVersion="23000" targetRuntime="MacOSX.Cocoa"> <dependencies><deployment identifier="macosx"/><plugIn identifier="com.apple.InterfaceBuilder.CocoaPlugin" version="23000"/></dependencies> <objects> <customObject id="-2" userLabel="File's Owner" customClass="NSApplication"/> <customObject id="-1" userLabel="First Responder" customClass="FirstResponder"/> <customObject id="-3" userLabel="Application" customClass="NSObject"/> <menu title="Main" systemMenu="main" id="m1"><items> <menuItem title="Item" image="Brand" id="mi1"><modifierMask key="keyEquivalentModifierMask"/></menuItem> <menuItem title="Missing" image="MissingBrand" id="mi2"><modifierMask key="keyEquivalentModifierMask"/></menuItem> </items></menu> </objects></document> XML xcrun ibtool --compile "$work_dir/App.app/Contents/Resources/Menu.nib" "$work_dir/Menu.xib" > "$work_dir/ibtool.txt" cp "$work_dir/build/Assets.car" "$work_dir/App.app/Contents/Resources/Assets.car" cp /usr/bin/true "$work_dir/App.app/Contents/MacOS/App" cat > "$work_dir/App.app/Contents/Info.plist" <<'PLIST' <plist version="1.0"><dict><key>CFBundleIdentifier</key><string>test.resource</string><key>CFBundleExecutable</key><string>App</string><key>CFBundlePackageType</key><string>APPL</string></dict></plist> PLIST run_rea() { 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 "$@" --json } run_rea inspect-asset-catalog "$work_dir/App.app" > "$work_dir/catalog.json" run_rea decode-interface-builder "$work_dir/App.app" > "$work_dir/decoded.json" /usr/bin/assetutil --info "$work_dir/build/Assets.car" > "$work_dir/assetutil.json" jq '.normalized_result | {records: [.records[] | {kind,asset_name,rendition_name}],resource_key_matches}' "$work_dir/catalog.json" jq '.normalized_result.graph | {truncated,nodes: [.nodes[] | select(.attributes.class_name == "NSCustomResource") | {kind,attributes}]}' "$work_dir/decoded.json" jq '[.[] | select(.Name == "Brand") | {Name,RenditionName}]' "$work_dir/assetutil.json" ``` ### Expected behavior Return `Brand` as `matched` with the single `Brand.png` rendition, and `MissingBrand` as `unmatched` with no renditions. Preserve provenance and keep unrelated menu titles out of resource-key rows. [docs/native-investigation.md:356-359](https://github.com/morluto/rea/blob/5b676ba9f302228b44c28ab076525abed3e647c1/docs/native-investigation.md#L356-L359) documents exact UI resource-name matches for app-bundle asset-catalog inspection. This ibtool-produced archive explicitly identifies a named image, and assetutil independently confirms its exact catalog name. No rendition-byte extraction or arbitrary-string inference is needed. ### Impact and scope P2: the documented join omits explicit image-reference facts for this observed AppKit encoding, including a reference whose rendition appears in the same result. Broader prevalence was not measured. The generic limitation about recognized fields remains present; an empty join does not prove asset absence. A practical workaround is to join retained `NSClassName`/`NSResourceName` values from `decode-interface-builder` with the catalog's `asset_name` manually. No live application was launched or modified. ### REA version `rea-agents` 6.7.0, source commit `5b676ba9f302228b44c28ab076525abed3e647c1`. Freshly fetched `origin/main` at `edbd291a28a70e2c0b984ff374d5a6b437280e82`: none of the cited implementation/documentation files changed since the baseline. This main recheck is source comparison, not a runtime execution of that newer revision. ### Runtime and platform Node.js v26.12.0; macOS 27.0.1; arm64; Xcode 27.1 (27A9275); system assetutil. Hopper/Ghidra are not involved. ### Target and artifact context Generated inert macOS app wrapper with a copied system executable, an actool-compiled asset catalog containing one `Brand` rendition, and an ibtool-compiled AppKit menu nib containing `Brand` and `MissingBrand` image references. ### Interface CLI ### Earliest failure stage Native analysis or reconstruction ### Relevant output or evidence Observed catalog subset: ```json { "records": [ {"kind":"catalog","asset_name":null,"rendition_name":null}, {"kind":"rendition","asset_name":"Brand","rendition_name":"Brand.png"} ], "resource_key_matches": [] } ``` Observed decoded node subset, with `graph.truncated: false`: ```json { "kind": "unknown", "attributes": { "class_name": "NSCustomResource", "nibValues": {"NSClassName":"NSImage","NSResourceName":"Brand"} } } ``` The graph also retains `MissingBrand`, `NSMenuCheckmark` and `NSMenuMixedState` with that shape; none produces a resource-key row. Independent `assetutil --info` lists exactly one `Name: "Brand"`, `RenditionName: "Brand.png"` record. Evidence provenance: observed through the public CLI and real Apple tools, without substituted catalog metadata. ### Typed outcome or recovery expected Exact resource-key match rows with source node/archive provenance. For restoration, check the same fixture for one matched `Brand` rendition and an unmatched `MissingBrand`; keep system image references distinguishable from catalog matches and exclude menu titles `Main`, `Item` and `Missing`. These are proposed post-fix checks; no fix was tested. ### Analysis or suspected cause Source-derived reasoning: [the recognized-field set](https://github.com/morluto/rea/blob/5b676ba9f302228b44c28ab076525abed3e647c1/src/domain/apple/appleAssetCatalog.ts#L6-L16) excludes `NSResourceName`. [The NIBArchive producer](https://github.com/morluto/rea/blob/5b676ba9f302228b44c28ab076525abed3e647c1/src/artifacts/apple/InterfaceBuilderAnalysis.ts#L537-L558) projects only recognized resource fields, while [the collector](https://github.com/morluto/rea/blob/5b676ba9f302228b44c28ab076525abed3e647c1/src/artifacts/apple/AppleAssetCatalogAnalysis.ts#L220-L249) requires `kind: "resource"` and recognized direct attributes. The observed `NSCustomResource` node fails both conditions, so a field-only addition would not establish restoration. This is REA's resource interpretation, not an identified bundled-dependency failure. ### Additional context Still reproduces after #2176 and its merged repair #2248, commit `fd985dbc5b9435918732c0cc1825060af2a3a47e`, which is already in the tested baseline. That report concerns recognized `NSImageName`/`imageName` fields on decoded `NSImage` NIBArchive resource nodes. This report is limited to the uncovered `NSCustomResource` / `NSClassName: NSImage` / `NSResourceName` encoding produced by ibtool. The related PRs #2249 and #2256 were consolidated into #2248. Prepared with AI assistance and independently reproduced. No earlier known-good version was established; MCP and other platforms were not tested.
Author
Owner

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

Additional independently reproduced iOS trigger with the same root cause: the nib-reference join does not recognize this image-reference form. A minimal iOS XIB with a UIImageView whose image is Icons/Star, compiled with ibtool --compile, plus an asset catalog with a Provides Namespace folder Icons containing Star.imageset, compiled with actool --compile build --platform iphoneos --minimum-deployment-target 17.0, reproduces with node scripts/rea.mjs inspect-asset-catalog Match.app --json: the catalog lists asset_name: "Icons/Star" / rendition_name: "star.png", but resource_key_matches: [] instead of a matched row. A separate XIB referencing absent Icons/Absent also yields no row instead of an unmatched row. decode-interface-builder on the compiled nib retains class_name: "UIImageNibPlaceholder", kind: "placeholder", and nibValues.UIResourceName: "Icons/Star" (or "Icons/Absent" for the control); both decoded graphs and catalog results report truncated: false, and all commands exit 0. Source inspection confirms that collectInterfaceBuilderResourceKeys requires kind === "resource" and the recognized-field set omits UIResourceName, so both conditions exclude this encoding. Tested at baseline 9d8e7803326a382cbbb3ffdb900f5e93e5442d68; fetched main at d0067ba34a39ea47dd7e11bb2e6169bc0938cc3f includes 6a6adf703, which touches these files but preserves both exclusions: verified at baseline; main not re-run.

<!-- gh-comment-id:6306415400 --> @coygeek commented on GitHub (Oct 21, 2026): Additional independently reproduced iOS trigger with the same root cause: the nib-reference join does not recognize this image-reference form. A minimal iOS XIB with a UIImageView whose image is `Icons/Star`, compiled with `ibtool --compile`, plus an asset catalog with a Provides Namespace folder `Icons` containing `Star.imageset`, compiled with `actool --compile build --platform iphoneos --minimum-deployment-target 17.0`, reproduces with `node scripts/rea.mjs inspect-asset-catalog Match.app --json`: the catalog lists `asset_name: "Icons/Star"` / `rendition_name: "star.png"`, but `resource_key_matches: []` instead of a matched row. A separate XIB referencing absent `Icons/Absent` also yields no row instead of an unmatched row. `decode-interface-builder` on the compiled nib retains `class_name: "UIImageNibPlaceholder"`, `kind: "placeholder"`, and `nibValues.UIResourceName: "Icons/Star"` (or `"Icons/Absent"` for the control); both decoded graphs and catalog results report `truncated: false`, and all commands exit 0. Source inspection confirms that `collectInterfaceBuilderResourceKeys` requires `kind === "resource"` and the recognized-field set omits `UIResourceName`, so both conditions exclude this encoding. Tested at baseline `9d8e7803326a382cbbb3ffdb900f5e93e5442d68`; fetched main at `d0067ba34a39ea47dd7e11bb2e6169bc0938cc3f` includes `6a6adf703`, which touches these files but preserves both exclusions: verified at baseline; main not re-run.
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#3414
No description provided.