[GH-ISSUE #5914] [Bug] Nested export-shape additions and removals lose proven ancestor absence #3590

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

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

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

The public CLI reports a nested property as unknown when the opposite return shape proves its containing object absent.

Comparing export default () => ({ kind: "result" }); with export default () => ({ kind: "result", nested: { total: 1 } }); selects both exports and pairs their variants by /kind. /nested is correctly added, but /nested/total is unknown with left presence unknown-coverage. The summary is one addition and one unknown, and comparison coverage is partial. Both source graphs and return-shape projections are complete, with no omissions.

Reversing the inputs reproduces the corresponding removal problem. These are executed CLI observations, not inferred runtime behavior of the application.

Reproduction

Run from the built source checkout with Node.js and Python 3. This script executes two real application analyses and compares their full Evidence records in both directions. It reproduces the sequence run locally and removes its fixture directories afterward. The inspected JavaScript is never executed.

import json
from pathlib import Path
import subprocess
import tempfile

def rea(*args):
    completed = subprocess.run(
        ["node", "scripts/rea.mjs", *args, "--format", "json"],
        capture_output=True, text=True, check=True,
    )
    return json.loads(completed.stdout)

with tempfile.TemporaryDirectory() as directory:
    root = Path(directory)
    sources = {
        "left": 'export default () => ({ kind: "result" });\n',
        "right": 'export default () => ({ kind: "result", nested: { total: 1 } });\n',
    }
    evidence = {}
    for side, source in sources.items():
        app = root / side
        app.mkdir()
        (app / "parser.mjs").write_text(source)
        evidence[side] = rea("analyze-javascript-application", str(app),
                             "--artifact-format", "directory")
    for label, left, right in [("addition", "left", "right"),
                               ("removal", "right", "left")]:
        request = root / "comparison.json"
        request.write_text(json.dumps({
            "left": evidence[left], "right": evidence[right],
            "left_module_path": "parser.mjs", "left_export_name": "default",
            "right_module_path": "parser.mjs", "right_export_name": "default",
        }))
        result = rea("compare-javascript-export-shapes", str(request))["normalized_result"]
        print(label)
        print(json.dumps({
            "summary": result["summary"],
            "coverage": result["coverage"],
            "changes": [{key: change[key] for key in ("path", "status", "presence")}
                        for change in result["changes"]],
        }, indent=2))

Both directions reproduced in the initial command sequence and again with this script. All commands exited successfully.

Expected behavior

For this fixture, /nested is absent on one side under complete root-property coverage. Its descendant /nested/total is therefore also absent on that side. Both paths should be added in the forward comparison and removed in the reverse comparison. Expected forward summary: { "added": 2, "removed": 0, "changed": 0, "unknown": 0 }; expected reverse summary: { "added": 0, "removed": 2, "changed": 0, "unknown": 0 }. Coverage should be complete-within-inputs.

The documented presence contract distinguishes property presence from value resolution and says complete-coverage presence-only gaps do not count as unknown. A present but unresolved intermediate property must continue to preserve uncertainty; a complete outer inventory alone does not prove that property's descendants absent.

Impact and scope

P2 correctness defect: the tested whole-object addition/removal understates definite changes and introduces false partial coverage. The containing-object row and full property inventories remain visible, so callers can inspect those facts manually. There is no observed mutation or loss of source Evidence. Deeper nesting and other container kinds were not separately tested. MCP uses the same comparison workflow according to source inspection, but its transport was not exercised.

REA version

6.7.0; source revision 52415975942a4b9c53a817915b313ccc404f280e.

Runtime and platform

macOS 27.0.1 arm64, Node.js v26.12.0. Existing built CLI; no external analysis provider required.

Target and artifact context

Two ESM application directories, each containing one parser.mjs. Each default export is an expression-bodied arrow returning a literal object. Both variants have the unique literal discriminator /kind = result.

Interface

CLI

Earliest failure stage

Native analysis or reconstruction

Relevant output or evidence

Reduced forward normalized_result from the executed CLI:

{
  "summary": { "added": 1, "removed": 0, "changed": 0, "unknown": 1 },
  "changes": [
    {
      "path": "/nested/total",
      "status": "unknown",
      "presence": { "left": "unknown-coverage", "right": "present" },
      "left": { "availability": "absent" },
      "right": { "availability": "literal", "value": 1 }
    },
    {
      "path": "/nested",
      "status": "added",
      "presence": { "left": "absent", "right": "present" }
    }
  ],
  "coverage": {
    "status": "partial",
    "left_graph_status": "complete",
    "right_graph_status": "complete",
    "paired_variants": 1
  }
}

Every omission count is zero. The returned left inventory contains only /kind, with complete coverage at "". The right inventory contains /kind, /nested, and /nested/total, with complete coverage at "" and /nested. Both original return-shape projections have status: "complete" and projection_complete: true.

Reverse comparison reports { "added": 0, "removed": 1, "changed": 0, "unknown": 1 }, with /nested removed and /nested/total unknown because right presence is unknown-coverage. Coverage is partial again.

Typed outcome or recovery expected

Propagate established ancestor absence to descendant presence. For the forward child row, return status: "added" with presence.left: "absent"; reverse it for removal. Preserve unknown coverage when an intermediate property is present but unresolved, or its presence cannot be established.

Analysis or suspected cause

Source-derived: path enumeration includes the nested right-side leaf. fieldPresence consults only coverage at the immediate parent. The absent left /nested has no coverage entry of its own, so the child loses the absence already established at /nested. fieldChangeStatus consequently emits unknown, which comparisonPartial uses to downgrade coverage.

Additional context

Related earlier changes retained container paths and separated presence from unresolved values. This fixture reaches that implemented behavior, but the descendant still loses an established ancestor absence. No last-known-good version or repair has been established. An upstream source comparison against 647717c050ff4dbaef83fb1c5555ef5c33979cd4 found no changes to the cited comparison files or workflow documentation; execution on that later revision is not claimed.

Originally created by @coygeek on GitHub (Oct 21, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/5914 ### 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 The public CLI reports a nested property as unknown when the opposite return shape proves its containing object absent. Comparing `export default () => ({ kind: "result" });` with `export default () => ({ kind: "result", nested: { total: 1 } });` selects both exports and pairs their variants by `/kind`. `/nested` is correctly added, but `/nested/total` is unknown with left presence `unknown-coverage`. The summary is one addition and one unknown, and comparison coverage is partial. Both source graphs and return-shape projections are complete, with no omissions. Reversing the inputs reproduces the corresponding removal problem. These are executed CLI observations, not inferred runtime behavior of the application. ### Reproduction Run from the built source checkout with Node.js and Python 3. This script executes two real application analyses and compares their full Evidence records in both directions. It reproduces the sequence run locally and removes its fixture directories afterward. The inspected JavaScript is never executed. ```python import json from pathlib import Path import subprocess import tempfile def rea(*args): completed = subprocess.run( ["node", "scripts/rea.mjs", *args, "--format", "json"], capture_output=True, text=True, check=True, ) return json.loads(completed.stdout) with tempfile.TemporaryDirectory() as directory: root = Path(directory) sources = { "left": 'export default () => ({ kind: "result" });\n', "right": 'export default () => ({ kind: "result", nested: { total: 1 } });\n', } evidence = {} for side, source in sources.items(): app = root / side app.mkdir() (app / "parser.mjs").write_text(source) evidence[side] = rea("analyze-javascript-application", str(app), "--artifact-format", "directory") for label, left, right in [("addition", "left", "right"), ("removal", "right", "left")]: request = root / "comparison.json" request.write_text(json.dumps({ "left": evidence[left], "right": evidence[right], "left_module_path": "parser.mjs", "left_export_name": "default", "right_module_path": "parser.mjs", "right_export_name": "default", })) result = rea("compare-javascript-export-shapes", str(request))["normalized_result"] print(label) print(json.dumps({ "summary": result["summary"], "coverage": result["coverage"], "changes": [{key: change[key] for key in ("path", "status", "presence")} for change in result["changes"]], }, indent=2)) ``` Both directions reproduced in the initial command sequence and again with this script. All commands exited successfully. ### Expected behavior For this fixture, `/nested` is absent on one side under complete root-property coverage. Its descendant `/nested/total` is therefore also absent on that side. Both paths should be added in the forward comparison and removed in the reverse comparison. Expected forward summary: `{ "added": 2, "removed": 0, "changed": 0, "unknown": 0 }`; expected reverse summary: `{ "added": 0, "removed": 2, "changed": 0, "unknown": 0 }`. Coverage should be `complete-within-inputs`. The [documented presence contract](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/docs/javascript-application-workflows.md#L415-L430) distinguishes property presence from value resolution and says complete-coverage presence-only gaps do not count as unknown. A present but unresolved intermediate property must continue to preserve uncertainty; a complete outer inventory alone does not prove that property's descendants absent. ### Impact and scope P2 correctness defect: the tested whole-object addition/removal understates definite changes and introduces false partial coverage. The containing-object row and full property inventories remain visible, so callers can inspect those facts manually. There is no observed mutation or loss of source Evidence. Deeper nesting and other container kinds were not separately tested. MCP uses the same comparison workflow according to source inspection, but its transport was not exercised. ### REA version 6.7.0; source revision `52415975942a4b9c53a817915b313ccc404f280e`. ### Runtime and platform macOS 27.0.1 arm64, Node.js v26.12.0. Existing built CLI; no external analysis provider required. ### Target and artifact context Two ESM application directories, each containing one `parser.mjs`. Each default export is an expression-bodied arrow returning a literal object. Both variants have the unique literal discriminator `/kind` = `result`. ### Interface CLI ### Earliest failure stage Native analysis or reconstruction ### Relevant output or evidence Reduced forward `normalized_result` from the executed CLI: ```json { "summary": { "added": 1, "removed": 0, "changed": 0, "unknown": 1 }, "changes": [ { "path": "/nested/total", "status": "unknown", "presence": { "left": "unknown-coverage", "right": "present" }, "left": { "availability": "absent" }, "right": { "availability": "literal", "value": 1 } }, { "path": "/nested", "status": "added", "presence": { "left": "absent", "right": "present" } } ], "coverage": { "status": "partial", "left_graph_status": "complete", "right_graph_status": "complete", "paired_variants": 1 } } ``` Every omission count is zero. The returned left inventory contains only `/kind`, with complete coverage at `""`. The right inventory contains `/kind`, `/nested`, and `/nested/total`, with complete coverage at `""` and `/nested`. Both original return-shape projections have `status: "complete"` and `projection_complete: true`. Reverse comparison reports `{ "added": 0, "removed": 1, "changed": 0, "unknown": 1 }`, with `/nested` removed and `/nested/total` unknown because right presence is `unknown-coverage`. Coverage is partial again. ### Typed outcome or recovery expected Propagate established ancestor absence to descendant presence. For the forward child row, return `status: "added"` with `presence.left: "absent"`; reverse it for removal. Preserve unknown coverage when an intermediate property is present but unresolved, or its presence cannot be established. ### Analysis or suspected cause Source-derived: [path enumeration](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/domain/javascript/javascriptExportShapeVariants.ts#L305-L344) includes the nested right-side leaf. [fieldPresence](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/domain/javascript/javascriptExportShapeVariants.ts#L408-L429) consults only coverage at the immediate parent. The absent left `/nested` has no coverage entry of its own, so the child loses the absence already established at `/nested`. [fieldChangeStatus](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/domain/javascript/javascriptExportShapeVariants.ts#L383-L399) consequently emits unknown, which [comparisonPartial](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/domain/javascript/javascriptExportShapeComparison.ts#L184-L193) uses to downgrade coverage. ### Additional context Related earlier changes retained container paths and separated presence from unresolved values. This fixture reaches that implemented behavior, but the descendant still loses an established ancestor absence. No last-known-good version or repair has been established. An upstream source comparison against `647717c050ff4dbaef83fb1c5555ef5c33979cd4` found no changes to the cited comparison files or workflow documentation; execution on that later revision is not claimed.
Author
Owner

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

Checked the reported fix on main at d87e73f2db.

fieldPresence now walks ancestor JSON pointers when the field is missing. A complete coverage entry or an explicitly absent ancestor establishes descendant absence; a present unresolved ancestor or incomplete coverage stops the walk with unknown-coverage. Thus complete root coverage on the report's {kind:"result"} side propagates through missing /nested to /nested/total, but nested:unresolved remains unknown.

Implementation: javascriptExportShapeVariants.ts, javascriptExportShapeComparison.test.ts.

The relevant regression checks in javascriptExportShapeComparison.test.ts, javascriptExportContainerCoverage.test.ts passed in the focused test run on this revision. This closure is based on the current source and those checks; provider fixtures do not establish a real-engine run. Closing as completed on main.

<!-- gh-comment-id:6380718481 --> @coygeek commented on GitHub (Oct 24, 2026): Checked the reported fix on `main` at d87e73f2db50be72d33a4b3c7199af35518b441c. fieldPresence now walks ancestor JSON pointers when the field is missing. A complete coverage entry or an explicitly absent ancestor establishes descendant absence; a present unresolved ancestor or incomplete coverage stops the walk with unknown-coverage. Thus complete root coverage on the report's {kind:"result"} side propagates through missing /nested to /nested/total, but nested:unresolved remains unknown. Implementation: [javascriptExportShapeVariants.ts](https://github.com/morluto/rea/blob/d87e73f2db50be72d33a4b3c7199af35518b441c/src/domain/javascript/javascriptExportShapeVariants.ts#L412), [javascriptExportShapeComparison.test.ts](https://github.com/morluto/rea/blob/d87e73f2db50be72d33a4b3c7199af35518b441c/tests/boundary/filesystem/javascriptExportShapeComparison.test.ts#L92). The relevant regression checks in [javascriptExportShapeComparison.test.ts](https://github.com/morluto/rea/blob/d87e73f2db50be72d33a4b3c7199af35518b441c/tests/boundary/filesystem/javascriptExportShapeComparison.test.ts), [javascriptExportContainerCoverage.test.ts](https://github.com/morluto/rea/blob/d87e73f2db50be72d33a4b3c7199af35518b441c/tests/boundary/filesystem/javascriptExportContainerCoverage.test.ts) passed in the focused test run on this revision. This closure is based on the current source and those checks; provider fixtures do not establish a real-engine run. Closing as completed on main.
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#3590
No description provided.