[GH-ISSUE #3952] [Bug] Bundle structural fingerprints collapse bigint, directive and assignment changes #1560

Closed
opened 2026-10-16 21:07:09 +00:00 by bsakaguchi · 1 comment
Owner

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

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

analyze-javascript-application returns the same babel-ast-v2 bundle-module structural fingerprint when only a bigint value, bigint object key, directive value, or assignment operator changes. The exact factory-source hashes differ. This violates the documented promise that structural fingerprints retain literals, operators and object keys. compare-application-versions accepts those results and pairs the affected modules using the medium-confidence structural tier, although it correctly reports their content as changed.

Reproduction

  1. From the baseline checkout with the built runtime and dependencies available, use the pinned Node.js runtime and run the following shell block. It creates disposable application directories, isolates CLI home state, runs both supported CLI operations, and prints only the relevant observations. The Python interpreter is used only to create fixtures and read JSON.
work=$(mktemp -d)
VERIFY_FIXTURE_ROOT="$work" python3 - <<'PY'
import json
import os
import subprocess
from pathlib import Path

root = Path(os.environ["VERIFY_FIXTURE_ROOT"])
(root / "home").mkdir()
env = dict(os.environ, HOME=str(root / "home"))
pairs = {
    "bigint": ("exports.v = 1n;", "exports.v = 2n;"),
    "assignment": ("exports.v = 1;", "exports.v += 1;"),
    "directive": ('"use strict";', '"use asm";'),
    "key": ("exports.v = {1n: 1};", "exports.v = {2n: 1};"),
    "string": ('exports.v = "left";', 'exports.v = "right";'),
    "rename": (
        "const key = selectKey(); exports.read = value[key];",
        "const selector = selectKey(); exports.read = value[selector];",
    ),
}
evidence = {}
for index, side in enumerate(("left", "right")):
    app = root / side
    app.mkdir()
    for name, bodies in pairs.items():
        source = (
            "globalThis.webpackChunkApp.push([[1], { 17(module, exports) { "
            + bodies[index] + " } }]);\n"
        )
        (app / (name + ".js")).write_text(source)
    with (root / (side + ".stderr")).open("w") as errors:
        result = subprocess.run(
            ["node", "scripts/rea.mjs", "analyze-javascript-application",
             str(app), "--json"],
            env=env, text=True, stdout=subprocess.PIPE, stderr=errors,
            check=True,
        )
    evidence[side] = json.loads(result.stdout)

def modules(value):
    return {
        observation["properties"]["path"]: observation
        for node in value["normalized_result"]["graph"]["nodes"]
        for observation in node["observations"]
        if "structural_fingerprint_sha256" in observation["properties"]
    }

left = modules(evidence["left"])
right = modules(evidence["right"])
for name in sorted(left):
    a, b = left[name], right[name]
    p, q = a["properties"], b["properties"]
    print(name, "source_equal", p["source_sha256"] == q["source_sha256"],
          "fingerprint_equal",
          p["structural_fingerprint_sha256"] == q["structural_fingerprint_sha256"],
          "coverage", a["evidence"]["coverage"]["status"],
          b["evidence"]["coverage"]["status"])

request = root / "compare-request.json"
request.write_text(json.dumps(evidence))
result = subprocess.run(
    ["node", "scripts/rea.mjs", "compare-application-versions",
     str(request), "--json"],
    env=env, text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE,
    check=True,
)
comparison = json.loads(result.stdout)["normalized_result"]
nodes = {node["node_id"]: node
         for node in evidence["left"]["normalized_result"]["graph"]["nodes"]}
for item in comparison["items"]:
    if item["match"].get("basis") == "structural-fingerprint":
        path = nodes[item["left_node_id"]]["observations"][0]["properties"]["path"]
        print("structural_match", path, item["status"],
              item["match"]["confidence"], item["dimensions"])
PY
  1. Compare fingerprint_equal with the body changes. The bigint, assignment, directive and key cases should differ; the string case is a distinction control, and the rename case should retain equality. The comparison output identifies which modules were paired using the structural tier.

Expected behavior

Complete bundle-module structural fingerprints retain literal values, operators and object keys while ignoring ordinary identifier names. Each of the four affected pairs should therefore have different structural fingerprints. The string control should remain distinct, and renaming the local computed-key variable should preserve the fingerprint. When these fixtures have no stronger matching identity, the affected modules should not match through structural equality.

Impact and scope

Particular valid syntax changes collapse into the same documented structural evidence and influence cross-version module pairing. Both effects were observed through supported CLI operations. Exact source evidence remains available and comparison still reports the affected content as changed; no real-world unrelated-module pairing or hidden change was established.

Workaround: Callers can inspect the retained exact factory-source digests and comparison content dimensions before accepting structural equality as evidence of literal or operator preservation. Those digests differed and the comparison marked all affected pairs changed in the observed outputs. This preserves access to the exact-change signal while avoiding reliance on the incorrect structural equality; it does not repair the fingerprint itself.

Assessment: P2.

REA version

rea 6.5.0 at source commit 4692bb3330

Runtime and platform

Node 24.18.0; macOS arm64

Target and artifact context

Inert JavaScript files in extracted application directories, using webpack chunk factories. No application code was executed.

Interface

CLI

Earliest failure stage

Native analysis or reconstruction

Relevant output or evidence

Observed two successful analysis CLI calls and one successful comparison CLI call, each with exit status 0. All recovered bundle-module observations had complete coverage, the algorithm was babel-ast-v2, and every pair had different exact factory-source digests. The extracted results were:

assignment.js source_equal False fingerprint_equal True coverage complete complete
bigint.js source_equal False fingerprint_equal True coverage complete complete
directive.js source_equal False fingerprint_equal True coverage complete complete
key.js source_equal False fingerprint_equal True coverage complete complete
rename.js source_equal False fingerprint_equal True coverage complete complete
string.js source_equal False fingerprint_equal False coverage complete complete

Both bigint modules returned 4aecec82f4de064c482ac69f6e20b46a4b4d77b9f8082781b4b93bfbe6427313. Both assignment modules returned becf460007d68d62dd5889d7eb0e607919aaa2e2c72d9ddf6d58c33c62d85311; both directive modules returned c5b879a5b88d83a399373284c923311247b535aa6c4c4802316e6d7a013bba75; both bigint-key modules returned e846e5d4d1debaf0438863fd619cecdb5c517fe6b16b6cb9d7e852f4437f275e.

The comparison reported five unique structural matches, including the four affected pairs and the intended rename control. Each was changed with confidence: medium and a content change dimension. No string-control structural match occurred. This demonstrates incorrect structural equality and its use in matching; it does not demonstrate an unchanged result or hidden content changes.

The runtime observations establish supported CLI reachability without calling internal helpers. The source explanation is derived from inspection rather than an implemented repair. These are bundle-module structural fingerprints; semantic function fingerprints are a separate result.

Typed outcome or recovery expected

Run the same fixture pairs through the analysis CLI. Require different structural fingerprints for all four literal, key and operator variants, with complete coverage and distinct exact factory-source digests. Keep the string distinction control and the computed-key rename equality control passing. Run the comparison CLI on the resulting unmodified Evidence envelopes and verify that the four affected pairs no longer acquire structural matches while the rename control remains structurally matchable. Preserve content-change reporting and the distinction between structural inference and exact identity.

Additional context

Freshness checked against origin/main at 22ec8bdb2f. The cited failing implementation files are unchanged from the reproduced baseline; this is a source freshness check, not a runtime rerun on that revision.

Originally created by @coygeek on GitHub (Oct 15, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/3952 ### 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 `analyze-javascript-application` returns the same `babel-ast-v2` bundle-module structural fingerprint when only a bigint value, bigint object key, directive value, or assignment operator changes. The exact factory-source hashes differ. This violates the documented promise that structural fingerprints retain literals, operators and object keys. `compare-application-versions` accepts those results and pairs the affected modules using the medium-confidence structural tier, although it correctly reports their content as changed. ### Reproduction 1. From the baseline checkout with the built runtime and dependencies available, use the pinned Node.js runtime and run the following shell block. It creates disposable application directories, isolates CLI home state, runs both supported CLI operations, and prints only the relevant observations. The Python interpreter is used only to create fixtures and read JSON. ```sh work=$(mktemp -d) VERIFY_FIXTURE_ROOT="$work" python3 - <<'PY' import json import os import subprocess from pathlib import Path root = Path(os.environ["VERIFY_FIXTURE_ROOT"]) (root / "home").mkdir() env = dict(os.environ, HOME=str(root / "home")) pairs = { "bigint": ("exports.v = 1n;", "exports.v = 2n;"), "assignment": ("exports.v = 1;", "exports.v += 1;"), "directive": ('"use strict";', '"use asm";'), "key": ("exports.v = {1n: 1};", "exports.v = {2n: 1};"), "string": ('exports.v = "left";', 'exports.v = "right";'), "rename": ( "const key = selectKey(); exports.read = value[key];", "const selector = selectKey(); exports.read = value[selector];", ), } evidence = {} for index, side in enumerate(("left", "right")): app = root / side app.mkdir() for name, bodies in pairs.items(): source = ( "globalThis.webpackChunkApp.push([[1], { 17(module, exports) { " + bodies[index] + " } }]);\n" ) (app / (name + ".js")).write_text(source) with (root / (side + ".stderr")).open("w") as errors: result = subprocess.run( ["node", "scripts/rea.mjs", "analyze-javascript-application", str(app), "--json"], env=env, text=True, stdout=subprocess.PIPE, stderr=errors, check=True, ) evidence[side] = json.loads(result.stdout) def modules(value): return { observation["properties"]["path"]: observation for node in value["normalized_result"]["graph"]["nodes"] for observation in node["observations"] if "structural_fingerprint_sha256" in observation["properties"] } left = modules(evidence["left"]) right = modules(evidence["right"]) for name in sorted(left): a, b = left[name], right[name] p, q = a["properties"], b["properties"] print(name, "source_equal", p["source_sha256"] == q["source_sha256"], "fingerprint_equal", p["structural_fingerprint_sha256"] == q["structural_fingerprint_sha256"], "coverage", a["evidence"]["coverage"]["status"], b["evidence"]["coverage"]["status"]) request = root / "compare-request.json" request.write_text(json.dumps(evidence)) result = subprocess.run( ["node", "scripts/rea.mjs", "compare-application-versions", str(request), "--json"], env=env, text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=True, ) comparison = json.loads(result.stdout)["normalized_result"] nodes = {node["node_id"]: node for node in evidence["left"]["normalized_result"]["graph"]["nodes"]} for item in comparison["items"]: if item["match"].get("basis") == "structural-fingerprint": path = nodes[item["left_node_id"]]["observations"][0]["properties"]["path"] print("structural_match", path, item["status"], item["match"]["confidence"], item["dimensions"]) PY ``` 2. Compare `fingerprint_equal` with the body changes. The bigint, assignment, directive and key cases should differ; the string case is a distinction control, and the rename case should retain equality. The comparison output identifies which modules were paired using the structural tier. ### Expected behavior Complete bundle-module structural fingerprints retain literal values, operators and object keys while ignoring ordinary identifier names. Each of the four affected pairs should therefore have different structural fingerprints. The string control should remain distinct, and renaming the local computed-key variable should preserve the fingerprint. When these fixtures have no stronger matching identity, the affected modules should not match through structural equality. ### Impact and scope Particular valid syntax changes collapse into the same documented structural evidence and influence cross-version module pairing. Both effects were observed through supported CLI operations. Exact source evidence remains available and comparison still reports the affected content as changed; no real-world unrelated-module pairing or hidden change was established. Workaround: Callers can inspect the retained exact factory-source digests and comparison content dimensions before accepting structural equality as evidence of literal or operator preservation. Those digests differed and the comparison marked all affected pairs changed in the observed outputs. This preserves access to the exact-change signal while avoiding reliance on the incorrect structural equality; it does not repair the fingerprint itself. Assessment: P2. ### REA version rea 6.5.0 at source commit 4692bb33300c7cb77b095b6cc0c33da861c1b860 ### Runtime and platform Node 24.18.0; macOS arm64 ### Target and artifact context Inert JavaScript files in extracted application directories, using webpack chunk factories. No application code was executed. ### Interface CLI ### Earliest failure stage Native analysis or reconstruction ### Relevant output or evidence Observed two successful analysis CLI calls and one successful comparison CLI call, each with exit status 0. All recovered bundle-module observations had complete coverage, the algorithm was `babel-ast-v2`, and every pair had different exact factory-source digests. The extracted results were: ```text assignment.js source_equal False fingerprint_equal True coverage complete complete bigint.js source_equal False fingerprint_equal True coverage complete complete directive.js source_equal False fingerprint_equal True coverage complete complete key.js source_equal False fingerprint_equal True coverage complete complete rename.js source_equal False fingerprint_equal True coverage complete complete string.js source_equal False fingerprint_equal False coverage complete complete ``` Both bigint modules returned `4aecec82f4de064c482ac69f6e20b46a4b4d77b9f8082781b4b93bfbe6427313`. Both assignment modules returned `becf460007d68d62dd5889d7eb0e607919aaa2e2c72d9ddf6d58c33c62d85311`; both directive modules returned `c5b879a5b88d83a399373284c923311247b535aa6c4c4802316e6d7a013bba75`; both bigint-key modules returned `e846e5d4d1debaf0438863fd619cecdb5c517fe6b16b6cb9d7e852f4437f275e`. The comparison reported five unique structural matches, including the four affected pairs and the intended rename control. Each was `changed` with `confidence: medium` and a `content` change dimension. No string-control structural match occurred. This demonstrates incorrect structural equality and its use in matching; it does not demonstrate an `unchanged` result or hidden content changes. - Expected source: [docs/javascript-artifact-reconstruction.md:296-306](https://github.com/morluto/rea/blob/4692bb33300c7cb77b095b6cc0c33da861c1b860/docs/javascript-artifact-reconstruction.md#L296) promises literal, operator and object-key retention for complete bundle-module structural fingerprints. [docs/javascript-application-workflows.md:154-165](https://github.com/morluto/rea/blob/4692bb33300c7cb77b095b6cc0c33da861c1b860/docs/javascript-application-workflows.md#L154) documents unique-only comparison tiers and medium-confidence structural inference. - Failure source: Observed `normalized_result.graph.nodes[].observations[].properties` from the analysis CLI and `normalized_result.items[].match` from the comparison CLI for the inline fixtures. Source-derived explanation is in [src/domain/javascript/javascriptAstFingerprint.ts:78-105](https://github.com/morluto/rea/blob/4692bb33300c7cb77b095b6cc0c33da861c1b860/src/domain/javascript/javascriptAstFingerprint.ts#L78), which does not encode bigint values, directive values or assignment operators. The object-key helper at [src/domain/javascript/javascriptAstValues.ts:63-69](https://github.com/morluto/rea/blob/4692bb33300c7cb77b095b6cc0c33da861c1b860/src/domain/javascript/javascriptAstValues.ts#L63) also omits bigint keys. [src/domain/javascript/javascriptStaticAnalysisBundler.ts:258-293](https://github.com/morluto/rea/blob/4692bb33300c7cb77b095b6cc0c33da861c1b860/src/domain/javascript/javascriptStaticAnalysisBundler.ts#L258) computes and exposes recovered factory fingerprints. The runtime observations establish supported CLI reachability without calling internal helpers. The source explanation is derived from inspection rather than an implemented repair. These are bundle-module structural fingerprints; semantic function fingerprints are a separate result. ### Typed outcome or recovery expected Run the same fixture pairs through the analysis CLI. Require different structural fingerprints for all four literal, key and operator variants, with complete coverage and distinct exact factory-source digests. Keep the string distinction control and the computed-key rename equality control passing. Run the comparison CLI on the resulting unmodified Evidence envelopes and verify that the four affected pairs no longer acquire structural matches while the rename control remains structurally matchable. Preserve content-change reporting and the distinction between structural inference and exact identity. ### Additional context Freshness checked against origin/main at 22ec8bdb2f7d4626587f23b1eed4a207df600386. The cited failing implementation files are unchanged from the reproduced baseline; this is a source freshness check, not a runtime rerun on that revision.
Author
Owner

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

Fixed on main in 0ef938f4c. Structural fingerprint v3 preserves bigint/directive values, assignment operators, fixed keys and execution-affecting syntax while retaining legitimate rename matching.

AST, worker, reconstruction and public CLI comparison regressions cover the reported collisions; exact source digests remain available.

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

<!-- gh-comment-id:6206027347 --> @morluto commented on GitHub (Oct 16, 2026): Fixed on `main` in [0ef938f4c](https://github.com/morluto/rea/commit/0ef938f4c5faa48a0ec4ef4574390eb6f05cfce6). Structural fingerprint v3 preserves bigint/directive values, assignment operators, fixed keys and execution-affecting syntax while retaining legitimate rename matching. AST, worker, reconstruction and public CLI comparison regressions cover the reported collisions; exact source digests remain available. 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#1560
No description provided.