[GH-ISSUE #5974] [Bug] Environment-state list views omit digit-string item counters #3643

Closed
opened 2026-10-31 00:42:40 +00:00 by bsakaguchi · 0 comments
Owner

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

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

A successful inspect-analysis-view request for environment-state omits a matching digit-string item total from a list plan's counters.

With one recorded GET /api/items response containing {"total":"1","items":[{"id":1}]}, the built CLI reports get_items as shape: "list", membership: "all", counters: []. The otherwise identical numeric response, {"total":1,"items":[{"id":1}]}, reports counters: ["total"].

Reproduction

Executed through the built CLI in two independent passes. Each pass inspected three generated HARs, then projected each command's actual Evidence through inspect-analysis-view. All commands exited 0. No browser, external analysis engine or provider double was used.

Save this script as counter_probe.py. From the built source checkout, run python3 counter_probe.py "$ARTIFACT_DIR", setting ARTIFACT_DIR to a writable directory for the generated inputs and outputs. NODE can select the Node executable; otherwise the script uses node on PATH.

import json, os, pathlib, subprocess, sys

work = pathlib.Path(sys.argv[1]).resolve()
work.mkdir(parents=True, exist_ok=True)
node = os.environ.get("NODE", "node")

def run(label, arguments):
    result = subprocess.run(
        [node, "scripts/rea.mjs", *arguments, "--json"],
        capture_output=True, text=True, timeout=60,
    )
    (work / (label + ".json")).write_text(result.stdout)
    (work / (label + ".stderr")).write_text(result.stderr)
    assert result.returncode == 0, (result.returncode, result.stdout, result.stderr)
    return json.loads(result.stdout)

for name, total in [("number", 1), ("string", "1"), ("partial", "50")]:
    body = json.dumps({"total": total, "items": [{"id": 1}]})
    entry = {
        "startedDateTime": "2020-01-01T00:00:00.000Z", "time": 1,
        "request": {"method": "GET", "url": "https://api.example.test/api/items",
                    "httpVersion": "HTTP/1.1", "cookies": [], "headers": [],
                    "queryString": [], "headersSize": -1, "bodySize": 0},
        "response": {"status": 200, "statusText": "OK", "httpVersion": "HTTP/1.1",
                     "cookies": [], "headers": [], "redirectURL": "", "headersSize": -1,
                     "bodySize": len(body), "content": {"size": len(body),
                         "mimeType": "application/json", "text": body}},
        "cache": {}, "timings": {"send": 0, "wait": 1, "receive": 0},
    }
    capture = work / (name + ".har")
    capture.write_text(json.dumps({"log": {"version": "1.2",
        "creator": {"name": "counter-example", "version": "1"}, "entries": [entry]}}))
    parent = run(name + ".capture", ["inspect-web-network-capture", str(capture), "har"])
    request = work / (name + ".request.json")
    request.write_text(json.dumps({"source": {"kind": "inline", "evidence": parent},
                                  "view": {"kind": "environment-state"}}))
    state = run(name + ".state", ["inspect-analysis-view", str(request)])
    view = state["normalized_result"]["views"][0]
    assert view["tool"] == "get_items" and view["shape"] == "list"
    print(json.dumps({"case": name, "total": total,
                      "membership": view["membership"], "counters": view["counters"]}))

Expected behavior

The matching string total should be identified consistently with the numeric total. For the one-item response above, the list plan should report counters: ["total"] while retaining the recorded string representation when reproducing the response. If a plan cannot reproduce that representation, it should fall back to a recorded view with a reason rather than silently omit the counter.

The environment-state list contract explicitly accepts item totals as numbers or strings of digits and describes counters equal to the number listed. The result schema describes counters as envelope members that count the listed entities.

Impact and scope

P2: a bounded semantic omission in the generated list plan. A consumer using counters to find the envelope members that track list length gets an incomplete answer for the documented digit-string representation. The original retained response body remains available, so callers can inspect it and interpret the total manually.

The executed case has one item, one query-free response, no continuation marker and a string total equal to the list length. A differing string total ("50") correctly produces recorded membership. Live state serving, later state changes and browser-scenario parents were not exercised; no raw-data loss or live-server corruption is claimed.

REA version

rea-agents 6.7.0; source revision 52415975942a4b9c53a817915b313ccc404f280e.

Runtime and platform

macOS 27.0.1 arm64; Node v24.22.0. Used the prebuilt runtime and installed locked dependencies. The checkout's exact .nvmrc version, 24.18.0, was not exercised.

Target and artifact context

Generated HAR 1.2 with one successful GET https://api.example.test/api/items response, JSON MIME type and a retained JSON envelope containing one identifiable item. The three fixtures differ only in the recorded total value: numeric 1, string "1", or string "50".

Interface

CLI

Earliest failure stage

Transport or result projection

Relevant output or evidence

Actual output reduced to the affected view fields:

{"case": "number", "total": 1, "membership": "all", "counters": ["total"]}
{"case": "string", "total": "1", "membership": "all", "counters": []}
{"case": "partial", "total": "50", "membership": "recorded", "counters": []}

The middle case's complete list view was:

{
  "tool": "get_items",
  "shape": "list",
  "type": "/api/items",
  "pointer": "/items",
  "fields": ["id"],
  "required_fields": ["id"],
  "membership": "all",
  "counters": [],
  "checked_record_ordinals": [0]
}

These values came from the CLI, not an isolated call to the counter helper. The documented CLI registration accepts the inline Evidence source used by the script.

Typed outcome or recovery expected

A successful environment-state AnalysisView whose complete-list plan includes the matching item-total member in counters, with response reproduction preserving its recorded JSON type; otherwise an explicit recorded-view fallback. No new tool or error category is needed.

Analysis or suspected cause

Source-derived: listFacts admits an item total through countOf and checks its parsed numeric value against items.length, but then collects counter names with parent[name] === items.length. The latter excludes "1" even though the completeness check accepted it.

Simply changing that comparison also needs to account for renderStateList, which currently replaces every counter with a number. The list reproduction gate compares rendered JSON against the recording. No candidate fix was tested.

Additional context

Fetched upstream main at 25e1a24a3ed3671a0148757555f18d16c29d936a; the relevant gate, rendering and result-schema files are unchanged from the tested revision. Current-main applicability was source-checked, not runtime-tested there.

PR #5352 does not change the counter gate or renderer. Related PRs #5787 and #5800 change list-line/query handling but still retain the strict counter-name comparison. Their broader list changes do not address this query-free digit-string case. No first-bad or last-known-good revision was established.

Originally created by @coygeek on GitHub (Oct 21, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/5974 ### 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 A successful `inspect-analysis-view` request for `environment-state` omits a matching digit-string item total from a list plan's `counters`. With one recorded `GET /api/items` response containing `{"total":"1","items":[{"id":1}]}`, the built CLI reports `get_items` as `shape: "list"`, `membership: "all"`, `counters: []`. The otherwise identical numeric response, `{"total":1,"items":[{"id":1}]}`, reports `counters: ["total"]`. ### Reproduction Executed through the built CLI in two independent passes. Each pass inspected three generated HARs, then projected each command's actual Evidence through `inspect-analysis-view`. All commands exited 0. No browser, external analysis engine or provider double was used. Save this script as `counter_probe.py`. From the built source checkout, run `python3 counter_probe.py "$ARTIFACT_DIR"`, setting `ARTIFACT_DIR` to a writable directory for the generated inputs and outputs. `NODE` can select the Node executable; otherwise the script uses `node` on PATH. ```python import json, os, pathlib, subprocess, sys work = pathlib.Path(sys.argv[1]).resolve() work.mkdir(parents=True, exist_ok=True) node = os.environ.get("NODE", "node") def run(label, arguments): result = subprocess.run( [node, "scripts/rea.mjs", *arguments, "--json"], capture_output=True, text=True, timeout=60, ) (work / (label + ".json")).write_text(result.stdout) (work / (label + ".stderr")).write_text(result.stderr) assert result.returncode == 0, (result.returncode, result.stdout, result.stderr) return json.loads(result.stdout) for name, total in [("number", 1), ("string", "1"), ("partial", "50")]: body = json.dumps({"total": total, "items": [{"id": 1}]}) entry = { "startedDateTime": "2020-01-01T00:00:00.000Z", "time": 1, "request": {"method": "GET", "url": "https://api.example.test/api/items", "httpVersion": "HTTP/1.1", "cookies": [], "headers": [], "queryString": [], "headersSize": -1, "bodySize": 0}, "response": {"status": 200, "statusText": "OK", "httpVersion": "HTTP/1.1", "cookies": [], "headers": [], "redirectURL": "", "headersSize": -1, "bodySize": len(body), "content": {"size": len(body), "mimeType": "application/json", "text": body}}, "cache": {}, "timings": {"send": 0, "wait": 1, "receive": 0}, } capture = work / (name + ".har") capture.write_text(json.dumps({"log": {"version": "1.2", "creator": {"name": "counter-example", "version": "1"}, "entries": [entry]}})) parent = run(name + ".capture", ["inspect-web-network-capture", str(capture), "har"]) request = work / (name + ".request.json") request.write_text(json.dumps({"source": {"kind": "inline", "evidence": parent}, "view": {"kind": "environment-state"}})) state = run(name + ".state", ["inspect-analysis-view", str(request)]) view = state["normalized_result"]["views"][0] assert view["tool"] == "get_items" and view["shape"] == "list" print(json.dumps({"case": name, "total": total, "membership": view["membership"], "counters": view["counters"]})) ``` ### Expected behavior The matching string total should be identified consistently with the numeric total. For the one-item response above, the list plan should report `counters: ["total"]` while retaining the recorded string representation when reproducing the response. If a plan cannot reproduce that representation, it should fall back to a recorded view with a reason rather than silently omit the counter. The [environment-state list contract](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/docs/web-network-captures.md#L567-L577) explicitly accepts item totals as numbers or strings of digits and describes counters equal to the number listed. The [result schema](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/domain/analysisView/environmentStateView.ts#L55-L65) describes `counters` as envelope members that count the listed entities. ### Impact and scope P2: a bounded semantic omission in the generated list plan. A consumer using `counters` to find the envelope members that track list length gets an incomplete answer for the documented digit-string representation. The original retained response body remains available, so callers can inspect it and interpret the total manually. The executed case has one item, one query-free response, no continuation marker and a string total equal to the list length. A differing string total (`"50"`) correctly produces recorded membership. Live state serving, later state changes and browser-scenario parents were not exercised; no raw-data loss or live-server corruption is claimed. ### REA version rea-agents 6.7.0; source revision `52415975942a4b9c53a817915b313ccc404f280e`. ### Runtime and platform macOS 27.0.1 arm64; Node v24.22.0. Used the prebuilt runtime and installed locked dependencies. The checkout's exact `.nvmrc` version, 24.18.0, was not exercised. ### Target and artifact context Generated HAR 1.2 with one successful `GET https://api.example.test/api/items` response, JSON MIME type and a retained JSON envelope containing one identifiable item. The three fixtures differ only in the recorded `total` value: numeric `1`, string `"1"`, or string `"50"`. ### Interface CLI ### Earliest failure stage Transport or result projection ### Relevant output or evidence Actual output reduced to the affected view fields: ```json {"case": "number", "total": 1, "membership": "all", "counters": ["total"]} {"case": "string", "total": "1", "membership": "all", "counters": []} {"case": "partial", "total": "50", "membership": "recorded", "counters": []} ``` The middle case's complete list view was: ```json { "tool": "get_items", "shape": "list", "type": "/api/items", "pointer": "/items", "fields": ["id"], "required_fields": ["id"], "membership": "all", "counters": [], "checked_record_ordinals": [0] } ``` These values came from the CLI, not an isolated call to the counter helper. The documented [CLI registration](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/cli/analysisViewCommands.ts#L79-L129) accepts the inline Evidence source used by the script. ### Typed outcome or recovery expected A successful `environment-state` AnalysisView whose complete-list plan includes the matching item-total member in `counters`, with response reproduction preserving its recorded JSON type; otherwise an explicit recorded-view fallback. No new tool or error category is needed. ### Analysis or suspected cause Source-derived: [`listFacts`](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/domain/analysisView/environmentStateGate.ts#L184-L197) admits an item total through `countOf` and checks its parsed numeric value against `items.length`, but then collects counter names with `parent[name] === items.length`. The latter excludes `"1"` even though the completeness check accepted it. Simply changing that comparison also needs to account for [`renderStateList`](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/domain/analysisView/environmentStateRules.ts#L416-L425), which currently replaces every counter with a number. The [list reproduction gate](https://github.com/morluto/rea/blob/52415975942a4b9c53a817915b313ccc404f280e/src/domain/analysisView/environmentStateGate.ts#L286-L309) compares rendered JSON against the recording. No candidate fix was tested. ### Additional context Fetched upstream main at `25e1a24a3ed3671a0148757555f18d16c29d936a`; the relevant gate, rendering and result-schema files are unchanged from the tested revision. Current-main applicability was source-checked, not runtime-tested there. PR #5352 does not change the counter gate or renderer. Related PRs #5787 and #5800 change list-line/query handling but still retain the strict counter-name comparison. Their broader list changes do not address this query-free digit-string case. No first-bad or last-known-good revision was established.
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#3643
No description provided.