[GH-ISSUE #4228] [Bug] Semantic property seeds reject a retained empty-string property name #1661

Closed
opened 2026-10-16 21:08:41 +00:00 by bsakaguchi · 3 comments
Owner

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

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

trace-javascript-semantics rejects {"kind":"property","name":""} even when successful analyze-javascript-application Evidence retains exactly one property slot with that legal name. The same unchanged Evidence succeeds with the empty property's semantic-node ID and a present nonempty property name. The rejected query exits 1 with invalid_request before seed matching; the retained node and its downstream relation remain available through exact-node selection.

Reproduction

Use either tested source revision below, install its locked dependencies with npm ci, and build with npm run build:cached. Run this standalone block from the repository root with the pinned Node on PATH. It creates an isolated application, home, configuration, cache and temporary directory, executes only the harmless inline fixture as a JavaScript oracle, invokes the public CLI, and removes its temporary files. The block was executed successfully at both tested revisions.

python3 - <<'PYREPRO'
import json
import os
from pathlib import Path
import subprocess
import tempfile

with tempfile.TemporaryDirectory(prefix="rea-property-review-") as directory:
    root = Path(directory)
    for name in ["app", "home", "config", "cache", "temp"]:
        (root / name).mkdir()
    source = "const routes = {'': 'HOME', x: 'NAMED'}; const root = routes['']; const named = routes.x;\n"
    fixture = root / "app" / "main.mjs"
    fixture.write_text(source)
    original = fixture.read_bytes()
    environment = {
        "PATH": os.environ["PATH"], "HOME": str(root / "home"),
        "XDG_CONFIG_HOME": str(root / "config"),
        "XDG_CACHE_HOME": str(root / "cache"), "TMPDIR": str(root / "temp"),
        "REA_LOG_LEVEL": "silent", "GOMAXPROCS": "2", "UV_THREADPOOL_SIZE": "2"
    }
    oracle = subprocess.run(["node", "--input-type=module", "--eval", source +
        "console.log(JSON.stringify({keys:Object.keys(routes),root,named}));"],
        env=environment, text=True, capture_output=True, timeout=30, check=True)
    assert json.loads(oracle.stdout) == {"keys": ["", "x"], "root": "HOME", "named": "NAMED"}
    def cli(command, argument, *options):
        completed = subprocess.run(["node", "scripts/rea.mjs", command,
            str(argument), *options, "--json"], env=environment,
            capture_output=True, text=True, timeout=60)
        return completed.returncode, json.loads(completed.stdout)
    code, application = cli("analyze-javascript-application", root / "app",
                            "--artifact-format", "directory")
    assert code == 0 and application["operation"] == "analyze_javascript_application"
    graph = application["normalized_result"]["semantic_graph"]
    def slots(name):
        return [node for node in graph["nodes"] if node["kind"] == "property-slot"
                and node["properties"].get("name") == name]
    assert len(slots("")) == len(slots("x")) == 1
    empty_node = slots("")[0]
    assert empty_node["label"] == '""'
    def trace(label, seed):
        request = root / (label + ".json")
        request.write_text(json.dumps({"application": application,
            "query": {"seed": seed, "direction": "forward-influence"}}))
        return cli("trace-javascript-semantics", request)
    rejected_code, rejected = trace("empty", {"kind": "property", "name": ""})
    assert rejected_code == 1 and rejected["code"] == "invalid_request"
    assert rejected["details"]["issues"] == [{"path": ["query", "seed", "name"],
                "reason": "out_of_range", "minimum": 1}]
    controls = {}
    for label, seed in [
        ("exact", {"kind": "semantic-node", "node_id": empty_node["node_id"]}),
        ("named", {"kind": "property", "name": "x"}),
        ("absent", {"kind": "property", "name": "missing"})
    ]:
        code, evidence = trace(label, seed)
        assert code == 0 and evidence["operation"] == "trace_javascript_semantics"
        result = evidence["normalized_result"]
        expected = [] if label == "absent" else [slots("" if label == "exact" else "x")[0]["node_id"]]
        assert result["seed_node_ids"] == expected
        assert result["coverage"]["status"] == "partial"
        controls[label] = {"status": result["status"], "seeds": len(expected),
            "nodes": len(result["nodes"]), "relations": len(result["relations"]),
            "coverage": result["coverage"]["status"]}
        assert len(result["nodes"]) == (0 if label == "absent" else 2)
        assert len(result["relations"]) == (0 if label == "absent" else 1)
    assert fixture.read_bytes() == original
    print(json.dumps({"oracle": json.loads(oracle.stdout), "analysis_exit": 0,
        "producer_property_names": sorted([node["properties"]["name"] for node in graph["nodes"] if node["kind"] == "property-slot"]),
        "parent_semantic_coverage": graph["coverage"]["status"],
        "empty": {"exit": rejected_code, "code": rejected["code"],
                  "issues": rejected["details"]["issues"]},
        "controls": controls, "source_unchanged": True}, indent=2))
PYREPRO

The empty-name assertion records the current rejection. The other queries exercise exact-node, present-name and absent-name selection on the identical unmodified producer Evidence, including ordinary graph validation and binding. The recipe also verifies unchanged source bytes.

Expected behavior

The property-name seed should select the retained property-slot whose exact properties.name is the requested empty string. For this unique-slot fixture it should return the same seed, node IDs and relation IDs as the exact-node control, with two nodes and one relation. Present nonempty-name selection, zero matches for an absent name, and honest partial coverage must remain intact.

The semantic tracing contract accepts property seeds and promises every matching seed and its reachable subgraph. The property matcher compares the exact preserved name. The static property-key decoder distinguishes a legal empty key from an unknown key; producer tests deliberately preserve that empty identity with a valid display label. The identity/display rule and real-constraint rule reinforce this selector contract. This expectation rests on the query contract and matching semantics as well as producer representability.

Impact and scope

  • Impact level: P2
  • Assessment status: supported
  • Impact basis: A bounded class of legal retained property identities cannot be selected by the documented property-name query in either CLI or MCP. The failure prevents that query and its downstream trace, but the parent Evidence retains the data and the tested exact-node control recovers it. Broader prevalence and untested query kinds are unknown.
  • Workaround status: available
  • Workaround basis: For the tested input, find the unique property-slot with properties.name === "" in the returned graph and pass its node_id as a semantic-node seed. The executed CLI and MCP controls return that property and its downstream relation without changing the source or Evidence. Renaming the fixture's property is not offered as a workaround.

REA version

Reproduced from source baseline 62bf09809ef7bb224028916424f8f07652a20f4f and independently replayed after rebuilding upstream main at d9aa06d93d95aa237927b83a433b495133302ef9. A source-only freshness recheck of true upstream main edfd5025b096b6b8f566b7b0971a376300131d8d found no changes in the JavaScript analysis, query, public-adapter or cited contract paths compared with the replayed revision. That newer revision was inspected, not executed. The source manifest reports 6.5.0; these are repository builds, not an installed-release reproduction. Published-package applicability is NOT_ENOUGH_INFO because no installed release run or complete release-applicability proof was performed.

Runtime and platform

Repository-pinned Node v24.18.0 and npm 11.16.0 on macOS arm64, with locked dependencies retained. The current-main build completed all three preparation tasks successfully. No browser, external service, or native analysis engine is needed.

Target and artifact context

A local JavaScript application directory containing the complete inline main.mjs fixture. Trace requests carry full unmodified Evidence from the successful public analysis. This is built-in static analysis, with no managed/runtime graph, missing graph, reconstructed Evidence, or retained-session prerequisite.

Interface

CLI and MCP. The current-main replay exercised CLI; the independent baseline verification also exercised the compiled production stdio MCP server.

Earliest failure stage

Request parsing or validation

Relevant output or evidence

The standalone CLI replay produced the following compact results at both tested revisions:

Node oracle: {"keys":["","x"],"root":"HOME","named":"NAMED"}
Producer: exit 0, property names ["", "x"], semantic coverage unknown
Empty property: exit 1, code invalid_request
Issue: {"path":["query","seed","name"],"reason":"out_of_range","minimum":1}
Exact empty-node control: exit 0, found, 1 seed, 2 nodes, 1 relation, partial coverage
Present x control: exit 0, found, 1 seed, 2 nodes, 1 relation, partial coverage
Absent missing control: exit 0, partial, 0 seeds, 0 nodes, 0 relations, partial coverage
Source unchanged: true

The baseline MCP verification returned isError: true and Input validation error: Invalid arguments for tool trace_javascript_semantics: query.seed.name: Too small: expected string to have >=1 characters. The exact-node, present-name and absent-name controls returned the same counts and partial coverage as CLI, and a subsequent client ping succeeded. The three selected baseline test files passed all 85 tests, including one temporary public MCP acceptance test that recorded the observed failure and controls. Passing those checks does not establish implementation restoration.

  • Evidence provenance: observed
  • Local verification: reproduced
  • Reproduction completeness: complete

Typed outcome or recovery expected

Repeat the unchanged producer and empty property-name trace. Require the selected seed and returned graph identities to equal the exact-node control, and preserve present-name selection, absent-name emptiness and existing partial coverage. CLI and MCP should agree on admission and matching. This closure check leaves the implementation open.

Analysis or suspected cause

Source-derived explanation: the property seed schema imposes a minimum name length of one. The CLI trace input and MCP trace input compose that same query schema. This explains the admission failure before the exact-name matcher runs; it does not prescribe a particular fix.

Additional context

Open #3986 loses a WebMCP input-property name during producer projection. Open #4213 admits a nonempty endpoint seed but omits a response carrier. This report instead concerns rejection of an already retained property identity before matching. Closed #783 scoped an empty-key consumer audit to static decoding, environment keys and method labels; the reproduced query-admission defect remains on current main.

No installed-package, other-platform, full-suite or post-fix restoration run was performed. Production prevalence and the last known good revision are NOT_ENOUGH_INFO because no prevalence study or regression boundary was established. The demonstrated exact-node workaround applies to the unique retained empty slot tested here; it is not a complete-coverage claim for untested graphs.

Originally created by @coygeek on GitHub (Oct 15, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/4228 ## 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 `trace-javascript-semantics` rejects `{"kind":"property","name":""}` even when successful `analyze-javascript-application` Evidence retains exactly one property slot with that legal name. The same unchanged Evidence succeeds with the empty property's `semantic-node` ID and a present nonempty property name. The rejected query exits 1 with `invalid_request` before seed matching; the retained node and its downstream relation remain available through exact-node selection. ## Reproduction Use either tested source revision below, install its locked dependencies with `npm ci`, and build with `npm run build:cached`. Run this standalone block from the repository root with the pinned Node on `PATH`. It creates an isolated application, home, configuration, cache and temporary directory, executes only the harmless inline fixture as a JavaScript oracle, invokes the public CLI, and removes its temporary files. The block was executed successfully at both tested revisions. ```sh python3 - <<'PYREPRO' import json import os from pathlib import Path import subprocess import tempfile with tempfile.TemporaryDirectory(prefix="rea-property-review-") as directory: root = Path(directory) for name in ["app", "home", "config", "cache", "temp"]: (root / name).mkdir() source = "const routes = {'': 'HOME', x: 'NAMED'}; const root = routes['']; const named = routes.x;\n" fixture = root / "app" / "main.mjs" fixture.write_text(source) original = fixture.read_bytes() environment = { "PATH": os.environ["PATH"], "HOME": str(root / "home"), "XDG_CONFIG_HOME": str(root / "config"), "XDG_CACHE_HOME": str(root / "cache"), "TMPDIR": str(root / "temp"), "REA_LOG_LEVEL": "silent", "GOMAXPROCS": "2", "UV_THREADPOOL_SIZE": "2" } oracle = subprocess.run(["node", "--input-type=module", "--eval", source + "console.log(JSON.stringify({keys:Object.keys(routes),root,named}));"], env=environment, text=True, capture_output=True, timeout=30, check=True) assert json.loads(oracle.stdout) == {"keys": ["", "x"], "root": "HOME", "named": "NAMED"} def cli(command, argument, *options): completed = subprocess.run(["node", "scripts/rea.mjs", command, str(argument), *options, "--json"], env=environment, capture_output=True, text=True, timeout=60) return completed.returncode, json.loads(completed.stdout) code, application = cli("analyze-javascript-application", root / "app", "--artifact-format", "directory") assert code == 0 and application["operation"] == "analyze_javascript_application" graph = application["normalized_result"]["semantic_graph"] def slots(name): return [node for node in graph["nodes"] if node["kind"] == "property-slot" and node["properties"].get("name") == name] assert len(slots("")) == len(slots("x")) == 1 empty_node = slots("")[0] assert empty_node["label"] == '""' def trace(label, seed): request = root / (label + ".json") request.write_text(json.dumps({"application": application, "query": {"seed": seed, "direction": "forward-influence"}})) return cli("trace-javascript-semantics", request) rejected_code, rejected = trace("empty", {"kind": "property", "name": ""}) assert rejected_code == 1 and rejected["code"] == "invalid_request" assert rejected["details"]["issues"] == [{"path": ["query", "seed", "name"], "reason": "out_of_range", "minimum": 1}] controls = {} for label, seed in [ ("exact", {"kind": "semantic-node", "node_id": empty_node["node_id"]}), ("named", {"kind": "property", "name": "x"}), ("absent", {"kind": "property", "name": "missing"}) ]: code, evidence = trace(label, seed) assert code == 0 and evidence["operation"] == "trace_javascript_semantics" result = evidence["normalized_result"] expected = [] if label == "absent" else [slots("" if label == "exact" else "x")[0]["node_id"]] assert result["seed_node_ids"] == expected assert result["coverage"]["status"] == "partial" controls[label] = {"status": result["status"], "seeds": len(expected), "nodes": len(result["nodes"]), "relations": len(result["relations"]), "coverage": result["coverage"]["status"]} assert len(result["nodes"]) == (0 if label == "absent" else 2) assert len(result["relations"]) == (0 if label == "absent" else 1) assert fixture.read_bytes() == original print(json.dumps({"oracle": json.loads(oracle.stdout), "analysis_exit": 0, "producer_property_names": sorted([node["properties"]["name"] for node in graph["nodes"] if node["kind"] == "property-slot"]), "parent_semantic_coverage": graph["coverage"]["status"], "empty": {"exit": rejected_code, "code": rejected["code"], "issues": rejected["details"]["issues"]}, "controls": controls, "source_unchanged": True}, indent=2)) PYREPRO ``` The empty-name assertion records the current rejection. The other queries exercise exact-node, present-name and absent-name selection on the identical unmodified producer Evidence, including ordinary graph validation and binding. The recipe also verifies unchanged source bytes. ## Expected behavior The property-name seed should select the retained `property-slot` whose exact `properties.name` is the requested empty string. For this unique-slot fixture it should return the same seed, node IDs and relation IDs as the exact-node control, with two nodes and one relation. Present nonempty-name selection, zero matches for an absent name, and honest partial coverage must remain intact. The [semantic tracing contract](https://github.com/morluto/rea/blob/d9aa06d93d95aa237927b83a433b495133302ef9/docs/javascript-application-workflows.md#L107-L117) accepts property seeds and promises every matching seed and its reachable subgraph. The [property matcher](https://github.com/morluto/rea/blob/d9aa06d93d95aa237927b83a433b495133302ef9/src/domain/javascript/javascriptSemanticQueryIdentity.ts#L72-L94) compares the exact preserved name. The [static property-key decoder](https://github.com/morluto/rea/blob/d9aa06d93d95aa237927b83a433b495133302ef9/src/domain/javascript/javascriptAstValues.ts#L76-L104) distinguishes a legal empty key from an unknown key; [producer tests](https://github.com/morluto/rea/blob/d9aa06d93d95aa237927b83a433b495133302ef9/src/application/javascript/JavaScriptSemanticGraphBuilder.test.ts#L500-L535) deliberately preserve that empty identity with a valid display label. The [identity/display rule](https://github.com/morluto/rea/blob/d9aa06d93d95aa237927b83a433b495133302ef9/AGENTS.md#L29) and [real-constraint rule](https://github.com/morluto/rea/blob/d9aa06d93d95aa237927b83a433b495133302ef9/docs/tool-design.md#L67-L68) reinforce this selector contract. This expectation rests on the query contract and matching semantics as well as producer representability. ## Impact and scope - Impact level: P2 - Assessment status: supported - Impact basis: A bounded class of legal retained property identities cannot be selected by the documented property-name query in either CLI or MCP. The failure prevents that query and its downstream trace, but the parent Evidence retains the data and the tested exact-node control recovers it. Broader prevalence and untested query kinds are unknown. - Workaround status: available - Workaround basis: For the tested input, find the unique property-slot with `properties.name === ""` in the returned graph and pass its `node_id` as a semantic-node seed. The executed CLI and MCP controls return that property and its downstream relation without changing the source or Evidence. Renaming the fixture's property is not offered as a workaround. ## REA version Reproduced from source baseline [62bf09809ef7bb224028916424f8f07652a20f4f](https://github.com/morluto/rea/tree/62bf09809ef7bb224028916424f8f07652a20f4f) and independently replayed after rebuilding upstream main at [d9aa06d93d95aa237927b83a433b495133302ef9](https://github.com/morluto/rea/tree/d9aa06d93d95aa237927b83a433b495133302ef9). A source-only freshness recheck of true upstream main [edfd5025b096b6b8f566b7b0971a376300131d8d](https://github.com/morluto/rea/tree/edfd5025b096b6b8f566b7b0971a376300131d8d) found no changes in the JavaScript analysis, query, public-adapter or cited contract paths compared with the replayed revision. That newer revision was inspected, not executed. The source manifest reports 6.5.0; these are repository builds, not an installed-release reproduction. Published-package applicability is NOT_ENOUGH_INFO because no installed release run or complete release-applicability proof was performed. ## Runtime and platform Repository-pinned Node v24.18.0 and npm 11.16.0 on macOS arm64, with locked dependencies retained. The current-main build completed all three preparation tasks successfully. No browser, external service, or native analysis engine is needed. ## Target and artifact context A local JavaScript application directory containing the complete inline `main.mjs` fixture. Trace requests carry full unmodified Evidence from the successful public analysis. This is built-in static analysis, with no managed/runtime graph, missing graph, reconstructed Evidence, or retained-session prerequisite. ## Interface CLI and MCP. The current-main replay exercised CLI; the independent baseline verification also exercised the compiled production stdio MCP server. ## Earliest failure stage Request parsing or validation ## Relevant output or evidence The standalone CLI replay produced the following compact results at both tested revisions: ```text Node oracle: {"keys":["","x"],"root":"HOME","named":"NAMED"} Producer: exit 0, property names ["", "x"], semantic coverage unknown Empty property: exit 1, code invalid_request Issue: {"path":["query","seed","name"],"reason":"out_of_range","minimum":1} Exact empty-node control: exit 0, found, 1 seed, 2 nodes, 1 relation, partial coverage Present x control: exit 0, found, 1 seed, 2 nodes, 1 relation, partial coverage Absent missing control: exit 0, partial, 0 seeds, 0 nodes, 0 relations, partial coverage Source unchanged: true ``` The baseline MCP verification returned `isError: true` and `Input validation error: Invalid arguments for tool trace_javascript_semantics: query.seed.name: Too small: expected string to have >=1 characters`. The exact-node, present-name and absent-name controls returned the same counts and partial coverage as CLI, and a subsequent client ping succeeded. The three selected baseline test files passed all 85 tests, including one temporary public MCP acceptance test that recorded the observed failure and controls. Passing those checks does not establish implementation restoration. - Evidence provenance: observed - Local verification: reproduced - Reproduction completeness: complete ## Typed outcome or recovery expected Repeat the unchanged producer and empty property-name trace. Require the selected seed and returned graph identities to equal the exact-node control, and preserve present-name selection, absent-name emptiness and existing partial coverage. CLI and MCP should agree on admission and matching. This closure check leaves the implementation open. ## Analysis or suspected cause Source-derived explanation: the [property seed schema](https://github.com/morluto/rea/blob/d9aa06d93d95aa237927b83a433b495133302ef9/src/domain/javascript/javascriptSemanticQuerySchemas.ts#L42-L45) imposes a minimum name length of one. The [CLI trace input](https://github.com/morluto/rea/blob/d9aa06d93d95aa237927b83a433b495133302ef9/src/domain/javascript/javascriptSemanticTraceSchemas.ts#L13-L16) and [MCP trace input](https://github.com/morluto/rea/blob/d9aa06d93d95aa237927b83a433b495133302ef9/src/contracts/javascript/applicationWorkflowInputContracts.ts#L24-L27) compose that same query schema. This explains the admission failure before the exact-name matcher runs; it does not prescribe a particular fix. ## Additional context Open [#3986](https://github.com/morluto/rea/issues/3986) loses a WebMCP input-property name during producer projection. Open [#4213](https://github.com/morluto/rea/issues/4213) admits a nonempty endpoint seed but omits a response carrier. This report instead concerns rejection of an already retained property identity before matching. Closed [#783](https://github.com/morluto/rea/issues/783) scoped an empty-key consumer audit to static decoding, environment keys and method labels; the reproduced query-admission defect remains on current main. No installed-package, other-platform, full-suite or post-fix restoration run was performed. Production prevalence and the last known good revision are NOT_ENOUGH_INFO because no prevalence study or regression boundary was established. The demonstrated exact-node workaround applies to the unique retained empty slot tested here; it is not a complete-coverage claim for untested graphs.
Author
Owner

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

I am working on this admission mismatch. I will keep the change scoped to property-name seeds and verify empty-name selection against exact-node selection through CLI and MCP, preserving the other seed constraints.

<!-- gh-comment-id:6201654310 --> @Abdooo2235 commented on GitHub (Oct 15, 2026): I am working on this admission mismatch. I will keep the change scoped to property-name seeds and verify empty-name selection against exact-node selection through CLI and MCP, preserving the other seed constraints.
Author
Owner

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

@coygeek the fix is available in #4239. The new CLI/MCP regression failed on the baseline and the focused suites now pass 87/87; static and docs checks also passed. The change only relaxes the property-name seed constraint and preserves exact matching. Draft pending human readiness.

<!-- gh-comment-id:6201737484 --> @Abdooo2235 commented on GitHub (Oct 15, 2026): @coygeek the fix is available in #4239. The new CLI/MCP regression failed on the baseline and the focused suites now pass 87/87; static and docs checks also passed. The change only relaxes the property-name seed constraint and preserves exact matching. Draft pending human readiness.
Author
Owner

@official-rex commented on GitHub (Oct 15, 2026):

Allow me to work on this issue

<!-- gh-comment-id:6202347677 --> @official-rex commented on GitHub (Oct 15, 2026): Allow me to work on this issue
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#1661
No description provided.