[GH-ISSUE #3876] [Bug] decode-interface-builder silently rounds keyed-plist integers above 2^53 #1495

Closed
opened 2026-10-16 21:06:10 +00:00 by bsakaguchi · 2 comments
Owner

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

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 rounds keyed-plist integers outside JavaScript's exact integer range and reports the rounded value as a complete, untruncated decode. A nib attribute stored as 9007199254740993 is returned as 9007199254740992 with archive_decode marked complete and no precision-loss limitation, while rea inspect-keyed-archive on the same file keeps the exact digits.

In both XML and binary encodings, the graph reports large as 9007199254740992, truncated: false, and archive_decode with status: "complete". All four commands exit successfully. The result's limitations contain only the four general notices about recognized fields, target-action selectors, geometry, and private archive classes; none mentions numeric precision. inspect-keyed-archive on the same file returns {"$plist_type":"integer","decimal":"9007199254740993"}.

Reproduction

  1. In an empty directory, create a minimal app bundle whose Contents/Resources/Main.nib is an XML keyed archive with one integer attribute 9007199254740993. The copied /usr/bin/true only satisfies app-target admission; REA does not execute it.
python3 - <<'PY'
import pathlib, plistlib, shutil
p = pathlib.Path("Precision.app/Contents")
(p / "Resources").mkdir(parents=True)
(p / "MacOS").mkdir()
(p / "Info.plist").write_bytes(plistlib.dumps({"CFBundleExecutable": "Probe", "CFBundleIdentifier": "org.example.probe"}))
shutil.copyfile("/usr/bin/true", p / "MacOS/Probe")
archive = {
    "$archiver": "NSKeyedArchiver",
    "$version": 100000,
    "$top": {"root": {"CF$UID": 1}},
    "$objects": [
        "$null",
        {"$class": {"CF$UID": 2}, "large": 9007199254740993},
        {"$classname": "UIView", "$classes": ["UIView", "NSObject"]},
    ],
}
(p / "Resources/Main.nib").write_bytes(plistlib.dumps(archive, fmt=plistlib.FMT_XML, sort_keys=False))
PY
  1. Decode it with both commands, then convert the nib to a binary plist and repeat.
rea decode-interface-builder Precision.app --json > xml.json
rea inspect-keyed-archive Precision.app Contents/Resources/Main.nib --json > xml-keyed.json
/usr/bin/plutil -convert binary1 Precision.app/Contents/Resources/Main.nib
rea decode-interface-builder Precision.app --json > binary.json
rea inspect-keyed-archive Precision.app Contents/Resources/Main.nib --json > binary-keyed.json
  1. Print the decoded attribute, truncation flag, and archive-decode coverage next to the keyed-archive value.
python3 - <<'PY'
import json, pathlib
for encoding in ["xml", "binary"]:
    result = json.loads(pathlib.Path(encoding + ".json").read_text())["normalized_result"]
    keyed = json.loads(pathlib.Path(encoding + "-keyed.json").read_text())["normalized_result"]
    print(json.dumps({"encoding": encoding, "decoded_attributes": result["graph"]["nodes"][1]["attributes"], "truncated": result["graph"]["truncated"], "archive_decode": result["graph"]["coverage"][-1], "keyed_value": keyed["objects"][1]["value"]}))
PY

Expected behavior

The Interface Builder graph either keeps the archived integer's exact digits (for example with the exact-decimal form inspect-keyed-archive already uses) or discloses that the value lost precision, instead of reporting a rounded number as a complete decode. AGENTS.md asks REA to preserve local evidence and metadata, and to preserve source values and explicit unknowns when normalization loses information.

Impact and scope

Static UI graph Evidence carries an incorrect numeric value for valid keyed-plist integers above 2^53 while claiming a complete decode; the demonstrated scope is one synthetic UIView archive in each plist encoding, and prevalence in shipped apps was not measured.

Workaround: available. rea inspect-keyed-archive on the affected archive preserved this field's exact value and object index in the same reproduction.

Reporter's impact estimate: P2 (supported).

REA version

6.5.0, built from source at main commit 034342ba28 (latest npm release is 6.5.0)

Runtime and platform

Node.js v26.10.0; macOS 27.0.1 arm64

Target and artifact context

Apple app bundle with an XML or binary NSKeyedArchiver nib

Interface

CLI

Earliest failure stage

Native analysis or reconstruction

Relevant output or evidence

  • Expected source: AGENTS.md lines 13 and 29 (preserve local evidence and metadata; preserve source values and explicit unknowns when normalization loses information); src/contracts/artifactToolContracts.ts:92-95 (decode_interface_builder decodes keyed property lists into an object graph)
  • Failure source: observed CLI output of rea decode-interface-builder on the fixture above for XML and binary plists, compared with rea inspect-keyed-archive on the same file
  • Evidence provenance: observed
  • Local verification: reproduced
  • Reproduction completeness: complete
{"encoding":"xml","decoded_attributes":{"large":9007199254740992,"interface_builder_object_id":"1","class_name":"UIView"},"truncated":false,"archive_decode":{"facet":"archive_decode","status":"complete","reason":null,"examined":1,"omitted":0},"keyed_value":{"$class":{"CF$UID":2},"large":{"$plist_type":"integer","decimal":"9007199254740993"}}}
{"encoding":"binary","decoded_attributes":{"large":9007199254740992,"interface_builder_object_id":"1","class_name":"UIView"},"truncated":false,"archive_decode":{"facet":"archive_decode","status":"complete","reason":null,"examined":1,"omitted":0},"keyed_value":{"large":{"$plist_type":"integer","decimal":"9007199254740993"},"$class":{"UID":2}}}

Source-derived reasoning (not runtime-verified beyond the output above): src/artifacts/apple/InterfaceBuilderAnalysis.ts:138-144 calls decodePlist, whose implementation at lines 703-725 passes parseBinary or parseXmlPropertyList output through projectPlistValue; the ordinary-number branch at src/domain/apple/plistValue.ts:62 keeps the already rounded number, and the attributes reach the graph at src/domain/apple/interfaceBuilderGraph.ts:117-120. Unlike src/artifacts/apple/KeyedArchiveReader.ts:86-111, this path does not apply original-byte number metadata or createPlistNumberProjection.

Typed outcome or recovery expected

The Interface Builder graph should carry the integer's exact decimal digits, or mark the attribute and archive_decode coverage as precision-limited, rather than a rounded number with complete coverage.

Restoration check:

On the fixture above, rea decode-interface-builder Precision.app --json currently reports "large":9007199254740992 with complete archive_decode coverage in both plist encodings. After a fix, the same command reports the value 9007199254740993 exactly, or marks it as precision-limited in the attribute or coverage. Control: rea inspect-keyed-archive Precision.app Contents/Resources/Main.nib --json continues to report "decimal":"9007199254740993", and a safe integer such as 42 continues to decode as the plain number 42.

Analysis or suspected cause

Source-derived: src/artifacts/apple/InterfaceBuilderAnalysis.ts:703-725 decodes plists through projectPlistValue, whose number branch at src/domain/apple/plistValue.ts:62 keeps the rounded JavaScript number; src/artifacts/apple/KeyedArchiveReader.ts:86-111 applies original-byte number metadata and createPlistNumberProjection, which this path skips.

Additional context

Related open #3373 covers hexadecimal XML integers (0x2A) becoming zero in inspect-keyed-archive. This report concerns decimal integers in decode-interface-builder, reproduces with binary plists too, and uses inspect-keyed-archive as the passing control.

This report came from AI-assisted source review with local reproduction where stated above; the evidence labels distinguish observed runs from source-only reasoning.

Originally created by @coygeek on GitHub (Oct 15, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/3876 ### 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` rounds keyed-plist integers outside JavaScript's exact integer range and reports the rounded value as a complete, untruncated decode. A nib attribute stored as `9007199254740993` is returned as `9007199254740992` with `archive_decode` marked complete and no precision-loss limitation, while `rea inspect-keyed-archive` on the same file keeps the exact digits. In both XML and binary encodings, the graph reports `large` as `9007199254740992`, `truncated: false`, and `archive_decode` with `status: "complete"`. All four commands exit successfully. The result's limitations contain only the four general notices about recognized fields, target-action selectors, geometry, and private archive classes; none mentions numeric precision. `inspect-keyed-archive` on the same file returns `{"$plist_type":"integer","decimal":"9007199254740993"}`. ### Reproduction 1. In an empty directory, create a minimal app bundle whose `Contents/Resources/Main.nib` is an XML keyed archive with one integer attribute `9007199254740993`. The copied `/usr/bin/true` only satisfies app-target admission; REA does not execute it. ```sh python3 - <<'PY' import pathlib, plistlib, shutil p = pathlib.Path("Precision.app/Contents") (p / "Resources").mkdir(parents=True) (p / "MacOS").mkdir() (p / "Info.plist").write_bytes(plistlib.dumps({"CFBundleExecutable": "Probe", "CFBundleIdentifier": "org.example.probe"})) shutil.copyfile("/usr/bin/true", p / "MacOS/Probe") archive = { "$archiver": "NSKeyedArchiver", "$version": 100000, "$top": {"root": {"CF$UID": 1}}, "$objects": [ "$null", {"$class": {"CF$UID": 2}, "large": 9007199254740993}, {"$classname": "UIView", "$classes": ["UIView", "NSObject"]}, ], } (p / "Resources/Main.nib").write_bytes(plistlib.dumps(archive, fmt=plistlib.FMT_XML, sort_keys=False)) PY ``` 2. Decode it with both commands, then convert the nib to a binary plist and repeat. ```sh rea decode-interface-builder Precision.app --json > xml.json rea inspect-keyed-archive Precision.app Contents/Resources/Main.nib --json > xml-keyed.json /usr/bin/plutil -convert binary1 Precision.app/Contents/Resources/Main.nib rea decode-interface-builder Precision.app --json > binary.json rea inspect-keyed-archive Precision.app Contents/Resources/Main.nib --json > binary-keyed.json ``` 3. Print the decoded attribute, truncation flag, and archive-decode coverage next to the keyed-archive value. ```sh python3 - <<'PY' import json, pathlib for encoding in ["xml", "binary"]: result = json.loads(pathlib.Path(encoding + ".json").read_text())["normalized_result"] keyed = json.loads(pathlib.Path(encoding + "-keyed.json").read_text())["normalized_result"] print(json.dumps({"encoding": encoding, "decoded_attributes": result["graph"]["nodes"][1]["attributes"], "truncated": result["graph"]["truncated"], "archive_decode": result["graph"]["coverage"][-1], "keyed_value": keyed["objects"][1]["value"]})) PY ``` ### Expected behavior The Interface Builder graph either keeps the archived integer's exact digits (for example with the exact-decimal form `inspect-keyed-archive` already uses) or discloses that the value lost precision, instead of reporting a rounded number as a complete decode. `AGENTS.md` asks REA to preserve local evidence and metadata, and to preserve source values and explicit unknowns when normalization loses information. ### Impact and scope Static UI graph Evidence carries an incorrect numeric value for valid keyed-plist integers above 2^53 while claiming a complete decode; the demonstrated scope is one synthetic UIView archive in each plist encoding, and prevalence in shipped apps was not measured. Workaround: available. `rea inspect-keyed-archive` on the affected archive preserved this field's exact value and object index in the same reproduction. Reporter's impact estimate: P2 (supported). ### REA version 6.5.0, built from source at main commit 034342ba28c8e56c3746be92b78aa7299afa02bd (latest npm release is 6.5.0) ### Runtime and platform Node.js v26.10.0; macOS 27.0.1 arm64 ### Target and artifact context Apple app bundle with an XML or binary NSKeyedArchiver nib ### Interface CLI ### Earliest failure stage Native analysis or reconstruction ### Relevant output or evidence - Expected source: AGENTS.md lines 13 and 29 (preserve local evidence and metadata; preserve source values and explicit unknowns when normalization loses information); src/contracts/artifactToolContracts.ts:92-95 (`decode_interface_builder` decodes keyed property lists into an object graph) - Failure source: observed CLI output of `rea decode-interface-builder` on the fixture above for XML and binary plists, compared with `rea inspect-keyed-archive` on the same file - Evidence provenance: observed - Local verification: reproduced - Reproduction completeness: complete ```json {"encoding":"xml","decoded_attributes":{"large":9007199254740992,"interface_builder_object_id":"1","class_name":"UIView"},"truncated":false,"archive_decode":{"facet":"archive_decode","status":"complete","reason":null,"examined":1,"omitted":0},"keyed_value":{"$class":{"CF$UID":2},"large":{"$plist_type":"integer","decimal":"9007199254740993"}}} {"encoding":"binary","decoded_attributes":{"large":9007199254740992,"interface_builder_object_id":"1","class_name":"UIView"},"truncated":false,"archive_decode":{"facet":"archive_decode","status":"complete","reason":null,"examined":1,"omitted":0},"keyed_value":{"large":{"$plist_type":"integer","decimal":"9007199254740993"},"$class":{"UID":2}}} ``` Source-derived reasoning (not runtime-verified beyond the output above): `src/artifacts/apple/InterfaceBuilderAnalysis.ts:138-144` calls `decodePlist`, whose implementation at lines 703-725 passes `parseBinary` or `parseXmlPropertyList` output through `projectPlistValue`; the ordinary-number branch at `src/domain/apple/plistValue.ts:62` keeps the already rounded number, and the attributes reach the graph at `src/domain/apple/interfaceBuilderGraph.ts:117-120`. Unlike `src/artifacts/apple/KeyedArchiveReader.ts:86-111`, this path does not apply original-byte number metadata or `createPlistNumberProjection`. ### Typed outcome or recovery expected The Interface Builder graph should carry the integer's exact decimal digits, or mark the attribute and archive_decode coverage as precision-limited, rather than a rounded number with complete coverage. Restoration check: On the fixture above, `rea decode-interface-builder Precision.app --json` currently reports `"large":9007199254740992` with complete `archive_decode` coverage in both plist encodings. After a fix, the same command reports the value `9007199254740993` exactly, or marks it as precision-limited in the attribute or coverage. Control: `rea inspect-keyed-archive Precision.app Contents/Resources/Main.nib --json` continues to report `"decimal":"9007199254740993"`, and a safe integer such as `42` continues to decode as the plain number `42`. ### Analysis or suspected cause Source-derived: src/artifacts/apple/InterfaceBuilderAnalysis.ts:703-725 decodes plists through projectPlistValue, whose number branch at src/domain/apple/plistValue.ts:62 keeps the rounded JavaScript number; src/artifacts/apple/KeyedArchiveReader.ts:86-111 applies original-byte number metadata and createPlistNumberProjection, which this path skips. ### Additional context Related open #3373 covers hexadecimal XML integers (`0x2A`) becoming zero in `inspect-keyed-archive`. This report concerns decimal integers in `decode-interface-builder`, reproduces with binary plists too, and uses `inspect-keyed-archive` as the passing control. This report came from AI-assisted source review with local reproduction where stated above; the evidence labels distinguish observed runs from source-only reasoning.
Author
Owner

@xu91102 commented on GitHub (Oct 15, 2026):

Reproduced on current main 933f216698fc7162c234e22fefe97315faa80676 with Node 24.18.0 on macOS arm64, before making a fix. Both XML and binary keyed .nib fixtures containing the integer 9007199254740993 produce the numeric attribute 9007199254740992 through the production decode-interface-builder --json command, with complete archive coverage. The corresponding value 42 remains correct in both formats. The same large fixtures retain exact decimal text through inspect-keyed-archive; the earlier cf0a38a build also reproduced the rounding through actual stdio MCP.

I am preparing a focused correction using the existing plist literal/projection owner rather than another decoder. It will preserve UID/object identities and safe-number controls, and keep ambiguous integer/real provenance explicit. No fix or PR has been published yet. Searches for this issue and the relevant plist/Interface Builder terms found no existing PR covering this numeric projection gap.

Verified fix published (2026-10-15)

The fix is committed and normally pushed to the fork commit on codex/interface-builder-plist-integers. The reviewed fix is now published as Draft PR #3972. Final-commit CI 38693920240 has completed successfully; the final result is recorded below.

Validation: 16 distinct files, 176 tests passed and one existing ibtool conditional skip; check (six tasks plus module/architecture guards), documentation (six tasks plus five conformance tests), test discovery, actual CLI and stdio MCP parity, and independent Standards/Spec review passed on the frozen commit. Safe values and graph/source identities remain covered; ambiguous values remain explicit. Normal commit/pre-push hooks preserved the reviewed bytes.

Local execution was macOS arm64 / Node 24.18.0. Windows/Linux/native execution and upstream CI are not claimed; no performance or token-cost improvement is claimed. A single ordinary Draft creation attempt for the first prepared fix was rejected by GitHub with xu91102 does not have the correct permissions to execute CreatePullRequest; the reason remains unknown. No alternate account or creation route was tried.

Publication update: after the maintainer closed earlier proposals #2060 and #3297 as covered on main, a fresh rebuilt-main CLI reproduction and duplicate check still confirmed this issue. The ordinary fork-to-upstream Draft creation succeeded: https://github.com/morluto/rea/pull/3972. The earlier creation denial remains historical evidence; its cause was not established.

Final-head CI result (2026-10-15)

PR #3972, head 2db05f4befc5710475eae4e3643475a636a179bc: CI 38693920240 attempt1 completed SUCCESS. Complete raw logs (2,180,985 bytes) and synthetic checkout parents were checked. The portable report aggregates source plus five shards: 15,214 passed / 135 skipped; it is not an additional execution. The fix-specific owner and public CLI regression files actually ran and passed. Linux x64 package baseline succeeded; Windows execution and real Apple E2E lanes were skipped. Local independent review, remote review, CI and human readiness remain separate.

<!-- gh-comment-id:6185644193 --> @xu91102 commented on GitHub (Oct 15, 2026): Reproduced on current main `933f216698fc7162c234e22fefe97315faa80676` with Node 24.18.0 on macOS arm64, before making a fix. Both XML and binary keyed `.nib` fixtures containing the integer `9007199254740993` produce the numeric attribute `9007199254740992` through the production `decode-interface-builder --json` command, with complete archive coverage. The corresponding value `42` remains correct in both formats. The same large fixtures retain exact decimal text through `inspect-keyed-archive`; the earlier `cf0a38a` build also reproduced the rounding through actual stdio MCP. I am preparing a focused correction using the existing plist literal/projection owner rather than another decoder. It will preserve UID/object identities and safe-number controls, and keep ambiguous integer/real provenance explicit. No fix or PR has been published yet. Searches for this issue and the relevant plist/Interface Builder terms found no existing PR covering this numeric projection gap. ### Verified fix published (2026-10-15) The fix is committed and normally pushed to [the fork commit](https://github.com/xu91102/rea/commit/2db05f4befc5710475eae4e3643475a636a179bc) on `codex/interface-builder-plist-integers`. The reviewed fix is now published as Draft PR #3972. Final-commit [CI 38693920240](https://github.com/morluto/rea/actions/runs/38693920240) has completed successfully; the final result is recorded below. Validation: 16 distinct files, 176 tests passed and one existing ibtool conditional skip; check (six tasks plus module/architecture guards), documentation (six tasks plus five conformance tests), test discovery, actual CLI and stdio MCP parity, and independent Standards/Spec review passed on the frozen commit. Safe values and graph/source identities remain covered; ambiguous values remain explicit. Normal commit/pre-push hooks preserved the reviewed bytes. Local execution was macOS arm64 / Node 24.18.0. Windows/Linux/native execution and upstream CI are not claimed; no performance or token-cost improvement is claimed. A single ordinary Draft creation attempt for the first prepared fix was rejected by GitHub with `xu91102 does not have the correct permissions to execute CreatePullRequest`; the reason remains unknown. No alternate account or creation route was tried. Publication update: after the maintainer closed earlier proposals #2060 and #3297 as covered on main, a fresh rebuilt-main CLI reproduction and duplicate check still confirmed this issue. The ordinary fork-to-upstream Draft creation succeeded: https://github.com/morluto/rea/pull/3972. The earlier creation denial remains historical evidence; its cause was not established. ### Final-head CI result (2026-10-15) PR #3972, head `2db05f4befc5710475eae4e3643475a636a179bc`: [CI 38693920240](https://github.com/morluto/rea/actions/runs/38693920240) attempt1 completed SUCCESS. Complete raw logs (2,180,985 bytes) and synthetic checkout parents were checked. The portable report aggregates source plus five shards: 15,214 passed / 135 skipped; it is not an additional execution. The fix-specific owner and public CLI regression files actually ran and passed. Linux x64 package baseline succeeded; Windows execution and real Apple E2E lanes were skipped. Local independent review, remote review, CI and human readiness remain separate.
Author
Owner

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

Fixed on main in 2202e3f0b. Interface Builder projection restores byte-derived exact plist integers after graph classification without changing reference identity.

Public CLI controls cover XML/binary large integers, safe values, collisions and incomplete metadata.

Hosted main CI is still pending; this closure is based on the implemented fix and the scoped regression evidence.

<!-- gh-comment-id:6206045540 --> @morluto commented on GitHub (Oct 16, 2026): Fixed on `main` in [2202e3f0b](https://github.com/morluto/rea/commit/2202e3f0ba556d40cc6e598df1ecbdf1eda56c13). Interface Builder projection restores byte-derived exact plist integers after graph classification without changing reference identity. Public CLI controls cover XML/binary large integers, safe values, collisions and incomplete metadata. Hosted main CI is still pending; this closure is based on the implemented fix and the scoped regression evidence.
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#1495
No description provided.