[GH-ISSUE #6773] [Bug] Omitted optional resource size creates false removal and addition #4129

Closed
opened 2026-10-31 00:47:01 +00:00 by bsakaguchi · 0 comments
Owner

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

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

compare-web-captures reports a resource removed and another added when two retained inventories contain the same URL, origin, type and MIME type, and the only differing producer fact is that one record omits the optional contentSize. The real resource parser preserves this omission as content_size: null, meaning unknown, without rejecting or excluding the record. The CLI nevertheless returns resources.status: "changed", two changes, and overall_status: "changed".

The forward and reverse comparisons each ran once and exited 0 with empty stderr. A same-input control returned zero resource changes. These are generated retained fixtures passed through the production parser and CLI; no live browser capture was performed.

Reproduction

Use the prepared build of the source revision below with its pinned Node.js toolchain. REA_ROOT denotes that checkout. Save this generator as fixture.mjs outside the checkout:

import assert from 'node:assert/strict';
import {writeFileSync} from 'node:fs';
import {resolve} from 'node:path';
import {pathToFileURL} from 'node:url';

const repo = resolve(process.argv[2]);
const output = resolve(process.argv[3] ?? '.');
const load = path => import(pathToFileURL(resolve(repo, 'dist', path)).href);
const {captureResources} = await load('browser/CdpCaptureDocuments.js');
const {CdpCaptureCompleteness} = await load('browser/CdpCaptureCompleteness.js');
const {stableWebResources} = await load('domain/webInventory.js');
const {captureSnapshotSchema} = await load('domain/webCaptureDiffSchemas.js');
const origin = 'https://resource.example.test';
assert(process.env.CAPTURE_AT, 'Set CAPTURE_AT from the current Pacific date command');
const at = new Date(process.env.CAPTURE_AT).toISOString();
const make = contentSize => {
  const completeness = new CdpCaptureCompleteness();
  const raw = {frameTree:{frame:{id:'frame',url:origin+'/'},resources:[{
    url:origin+'/style.css',type:'Stylesheet',mimeType:'text/css',
    ...(contentSize === undefined ? {} : {contentSize}),
  }]}};
  const captured = captureResources(raw, new Set([origin]), completeness);
  const resources = stableWebResources(captured.items.map(({rawUrl,...resource}) => resource));
  return captureSnapshotSchema.parse({inspection:{
    browser:{product:'normalized-fixture',protocol_version:'1.3',revision:'fixture',user_agent:'fixture',js_version:'fixture'},
    target:{target_id:'page',type:'page',title:'Fixture',url:origin+'/',origin,attached:false},
    capture_window:{started_at:at,ended_at:at,observation_ms:0},
    completeness:completeness.snapshot(),
    frames:[],dom:{total_nodes:0,nodes:[]},
    accessibility:{total_nodes:0,nodes:[],text_capture:{status:'not_approved',retained_bytes:0,excluded_fields:0}},
    scripts:{total:0,items:[]},resources,workers:[],
    network:{requests:[],websocket_connections:[],coverage_started_at:at,prior_activity_available:false},
    console:{events:[],coverage_started_at:at,prior_activity_available:false},
    metadata:{responses:[],dom_urls:[],agent_hints:[],excluded_dom_urls:0,headers_allowlisted:true},
    storage:{origin,usage_bytes:null,quota_bytes:null,local_storage_keys:[],session_storage_keys:[],indexed_db_names:[],cache_names:[],values_redacted:true},
    limitations:['Synthetic normalized captures; only resource records use the real producer parser. No live browser.'],
  }});
};
const before=make(128), after=make(undefined);
assert.equal(before.inspection.resources.length,1);
assert.equal(after.inspection.resources.length,1);
assert.equal(after.inspection.resources[0].content_size,null);
assert.deepEqual(before.inspection.completeness,after.inspection.completeness);
for (const [name,value] of Object.entries({before,after}))
  writeFileSync(resolve(output,name+'.json'),JSON.stringify(value,null,2)+'\n');
console.log(JSON.stringify({resources:[before.inspection.resources[0],after.inspection.resources[0]],completeness:after.inspection.completeness},null,2));

Then run:

WORK="$(mktemp -d)"
CAPTURE_AT="$(TZ=America/Los_Angeles date --iso-8601=seconds)" node fixture.mjs "$REA_ROOT" "$WORK"
node "$REA_ROOT/scripts/rea.mjs" compare-web-captures "$WORK/before.json" "$WORK/after.json" --format json --full-output

Inspect data.normalized_result.dimensions.resources. The generator calls the actual captureResources, CdpCaptureCompleteness, stableWebResources and captureSnapshotSchema exports. It produces one valid Stylesheet record with size 128 and the otherwise identical record with size omitted. All URLs are inert recorded identities. Reverse the input paths to observe the same false change in the other direction; compare before.json to itself for the unchanged control. All three comparisons were executed before drafting this report.

Expected behavior

Unavailable byte-count evidence must not prove resource removal or addition. Preserve the known common resource facts, and report the unavailable size as unknown if it prevents complete equality. A resource absent from a complete inventory or an independently observed fact difference must remain distinguishable from a missing optional field.

The resource-size parser explicitly treats omission as unknown. The resource documentation permits the optional size to be absent. The comparison result itself states that a changed status proves an observed difference.

Impact and scope

P2. A supported optional-field omission produces two definite resource-membership changes and makes the overall offline result changed. This can mislead before/after resource analysis. The failure is bounded to comparison output: the two retained records preserve their actual facts and remain inspectable. The synthetic forward/reverse pair demonstrated the behavior; real-browser omission frequency was not measured.

A practical workaround is to inspect the retained resource arrays directly and compare the jointly known fields. In these generated inputs, those fields are identical and only the unknown byte count and derived key differ. No automatic workaround through the comparison command was tested.

REA version

Package 6.7.0; source revision 99d2d7ad22bcbd780919760008aba6a88e7d1077. The prepared compiled runtime from that checkout was used. The parent investigation established that rea-agents-6.7.0 is the latest published release; the published package was not independently executed for this finding.

Runtime and platform

Node.js v24.22.1; Linux x64. No browser, Hopper, Ghidra, or external service was started.

Target and artifact context

Two schema-validated normalized passive-capture JSON files containing one generated Stylesheet resource each. The resource records and completeness ledgers come from the actual capture producer functions. The remaining empty inventories are fixture data, not live observations.

Interface

CLI

Earliest failure stage

Native analysis or reconstruction

Relevant output or evidence

The forward public CLI call returned:

{
  "overall_status": "changed",
  "resources": {
    "status": "changed",
    "total_changes": 2,
    "changes": [
      {"identity":"res_090c74fa6b7d3cba1e22781437773457f1e8f9239e0195b8a75fe140c9c58ea1","change":"removed"},
      {"identity":"res_43725d29345b71bc51cf2b6ff101513ce3992060344205afadc0811cfb4f2c78","change":"added"}
    ],
    "reason": null
  }
}

The reverse call exchanged added/removed labels on those same identities. The same-input control returned resources: {"total_changes":0,"changes":[],"status":"unchanged","reason":null}. All three exited 0 and emitted no stderr. Both producer completeness ledgers were identical, with status: "complete_within_window", empty section lists/exclusions, and zero dropped events. Only resource content_size (128 versus null) and the resulting resource_key differed.

Evidence provenance: observed production-parser output and public CLI results. Local verification: reproduced with generated retained fixtures. Reproduction completeness: complete for offline comparison; live-provider capture behavior and frequency are unverified. No comparison or probe was rerun after this draft was written.

Typed outcome or recovery expected

Return an unknown or limited resource comparison where size evidence is absent, while preserving independently supported changes. Do not manufacture removal/addition from a resource observation that remains present in both inventories.

Analysis or suspected cause

Source-derived: stableWebResources hashes every resource field, including nullable content_size, into resource_key. The resource comparator treats that key as a membership identity and checks only resource-inventory section completeness. The producer correctly retains omitted size as null. Thus a change in observation completeness becomes a membership change even though every jointly known fact agrees.

Additional context

Searched open and closed issues using "content_size", "resource" "size" "comparison", "resource" "removed" "added" and "compare-web-captures" queries scoped to morluto/rea.

Related #4708 concerns malformed negative resource sizes becoming zero during Electron inspection; this fixture supplies no invalid size and exercises offline comparison of a valid omission. #6554 concerns missing script module metadata and script identities; this report concerns the separate resource producer, optional size, resource keys and resource comparison branch.

Proposed restoration checks, not executed: this known/unknown-size pair should not prove membership changes; same-input and actual complete-inventory removal controls should retain their intended behavior.

Triage assessment

  • Impact level: P2
  • Assessment status: supported
  • Impact basis: Production-parser fixtures and the public offline CLI demonstrate false resource-membership changes and a false overall changed result from a valid optional metadata omission; retained input data is preserved.
  • Workaround status: available
  • Workaround basis: The retained one-resource arrays preserve URL, type, MIME type and nullable size; direct inspection of the generated files distinguishes the unavailable size from resource absence without altering source or captures.
Originally created by @coygeek on GitHub (Oct 23, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/6773 ## 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 `compare-web-captures` reports a resource removed and another added when two retained inventories contain the same URL, origin, type and MIME type, and the only differing producer fact is that one record omits the optional `contentSize`. The real resource parser preserves this omission as `content_size: null`, meaning unknown, without rejecting or excluding the record. The CLI nevertheless returns `resources.status: "changed"`, two changes, and `overall_status: "changed"`. The forward and reverse comparisons each ran once and exited 0 with empty stderr. A same-input control returned zero resource changes. These are generated retained fixtures passed through the production parser and CLI; no live browser capture was performed. ## Reproduction Use the prepared build of the source revision below with its pinned Node.js toolchain. `REA_ROOT` denotes that checkout. Save this generator as `fixture.mjs` outside the checkout: ```javascript import assert from 'node:assert/strict'; import {writeFileSync} from 'node:fs'; import {resolve} from 'node:path'; import {pathToFileURL} from 'node:url'; const repo = resolve(process.argv[2]); const output = resolve(process.argv[3] ?? '.'); const load = path => import(pathToFileURL(resolve(repo, 'dist', path)).href); const {captureResources} = await load('browser/CdpCaptureDocuments.js'); const {CdpCaptureCompleteness} = await load('browser/CdpCaptureCompleteness.js'); const {stableWebResources} = await load('domain/webInventory.js'); const {captureSnapshotSchema} = await load('domain/webCaptureDiffSchemas.js'); const origin = 'https://resource.example.test'; assert(process.env.CAPTURE_AT, 'Set CAPTURE_AT from the current Pacific date command'); const at = new Date(process.env.CAPTURE_AT).toISOString(); const make = contentSize => { const completeness = new CdpCaptureCompleteness(); const raw = {frameTree:{frame:{id:'frame',url:origin+'/'},resources:[{ url:origin+'/style.css',type:'Stylesheet',mimeType:'text/css', ...(contentSize === undefined ? {} : {contentSize}), }]}}; const captured = captureResources(raw, new Set([origin]), completeness); const resources = stableWebResources(captured.items.map(({rawUrl,...resource}) => resource)); return captureSnapshotSchema.parse({inspection:{ browser:{product:'normalized-fixture',protocol_version:'1.3',revision:'fixture',user_agent:'fixture',js_version:'fixture'}, target:{target_id:'page',type:'page',title:'Fixture',url:origin+'/',origin,attached:false}, capture_window:{started_at:at,ended_at:at,observation_ms:0}, completeness:completeness.snapshot(), frames:[],dom:{total_nodes:0,nodes:[]}, accessibility:{total_nodes:0,nodes:[],text_capture:{status:'not_approved',retained_bytes:0,excluded_fields:0}}, scripts:{total:0,items:[]},resources,workers:[], network:{requests:[],websocket_connections:[],coverage_started_at:at,prior_activity_available:false}, console:{events:[],coverage_started_at:at,prior_activity_available:false}, metadata:{responses:[],dom_urls:[],agent_hints:[],excluded_dom_urls:0,headers_allowlisted:true}, storage:{origin,usage_bytes:null,quota_bytes:null,local_storage_keys:[],session_storage_keys:[],indexed_db_names:[],cache_names:[],values_redacted:true}, limitations:['Synthetic normalized captures; only resource records use the real producer parser. No live browser.'], }}); }; const before=make(128), after=make(undefined); assert.equal(before.inspection.resources.length,1); assert.equal(after.inspection.resources.length,1); assert.equal(after.inspection.resources[0].content_size,null); assert.deepEqual(before.inspection.completeness,after.inspection.completeness); for (const [name,value] of Object.entries({before,after})) writeFileSync(resolve(output,name+'.json'),JSON.stringify(value,null,2)+'\n'); console.log(JSON.stringify({resources:[before.inspection.resources[0],after.inspection.resources[0]],completeness:after.inspection.completeness},null,2)); ``` Then run: ```sh WORK="$(mktemp -d)" CAPTURE_AT="$(TZ=America/Los_Angeles date --iso-8601=seconds)" node fixture.mjs "$REA_ROOT" "$WORK" node "$REA_ROOT/scripts/rea.mjs" compare-web-captures "$WORK/before.json" "$WORK/after.json" --format json --full-output ``` Inspect `data.normalized_result.dimensions.resources`. The generator calls the actual `captureResources`, `CdpCaptureCompleteness`, `stableWebResources` and `captureSnapshotSchema` exports. It produces one valid Stylesheet record with size 128 and the otherwise identical record with size omitted. All URLs are inert recorded identities. Reverse the input paths to observe the same false change in the other direction; compare `before.json` to itself for the unchanged control. All three comparisons were executed before drafting this report. ## Expected behavior Unavailable byte-count evidence must not prove resource removal or addition. Preserve the known common resource facts, and report the unavailable size as unknown if it prevents complete equality. A resource absent from a complete inventory or an independently observed fact difference must remain distinguishable from a missing optional field. The [resource-size parser](https://github.com/morluto/rea/blob/99d2d7ad22bcbd780919760008aba6a88e7d1077/src/browser/CdpCaptureValues.ts#L34-L42) explicitly treats omission as unknown. The [resource documentation](https://github.com/morluto/rea/blob/99d2d7ad22bcbd780919760008aba6a88e7d1077/docs/browser-observation.md#L456-L466) permits the optional size to be absent. The comparison result itself states that a changed status proves an observed difference. ## Impact and scope P2. A supported optional-field omission produces two definite resource-membership changes and makes the overall offline result changed. This can mislead before/after resource analysis. The failure is bounded to comparison output: the two retained records preserve their actual facts and remain inspectable. The synthetic forward/reverse pair demonstrated the behavior; real-browser omission frequency was not measured. A practical workaround is to inspect the retained resource arrays directly and compare the jointly known fields. In these generated inputs, those fields are identical and only the unknown byte count and derived key differ. No automatic workaround through the comparison command was tested. ## REA version Package 6.7.0; source revision `99d2d7ad22bcbd780919760008aba6a88e7d1077`. The prepared compiled runtime from that checkout was used. The parent investigation established that `rea-agents-6.7.0` is the latest published release; the published package was not independently executed for this finding. ## Runtime and platform Node.js v24.22.1; Linux x64. No browser, Hopper, Ghidra, or external service was started. ## Target and artifact context Two schema-validated normalized passive-capture JSON files containing one generated Stylesheet resource each. The resource records and completeness ledgers come from the actual capture producer functions. The remaining empty inventories are fixture data, not live observations. ## Interface CLI ## Earliest failure stage Native analysis or reconstruction ## Relevant output or evidence The forward public CLI call returned: ```json { "overall_status": "changed", "resources": { "status": "changed", "total_changes": 2, "changes": [ {"identity":"res_090c74fa6b7d3cba1e22781437773457f1e8f9239e0195b8a75fe140c9c58ea1","change":"removed"}, {"identity":"res_43725d29345b71bc51cf2b6ff101513ce3992060344205afadc0811cfb4f2c78","change":"added"} ], "reason": null } } ``` The reverse call exchanged added/removed labels on those same identities. The same-input control returned `resources: {"total_changes":0,"changes":[],"status":"unchanged","reason":null}`. All three exited 0 and emitted no stderr. Both producer completeness ledgers were identical, with `status: "complete_within_window"`, empty section lists/exclusions, and zero dropped events. Only resource `content_size` (128 versus null) and the resulting `resource_key` differed. Evidence provenance: observed production-parser output and public CLI results. Local verification: reproduced with generated retained fixtures. Reproduction completeness: complete for offline comparison; live-provider capture behavior and frequency are unverified. No comparison or probe was rerun after this draft was written. ## Typed outcome or recovery expected Return an unknown or limited resource comparison where size evidence is absent, while preserving independently supported changes. Do not manufacture removal/addition from a resource observation that remains present in both inventories. ## Analysis or suspected cause Source-derived: [stableWebResources](https://github.com/morluto/rea/blob/99d2d7ad22bcbd780919760008aba6a88e7d1077/src/domain/webInventory.ts#L31-L48) hashes every resource field, including nullable `content_size`, into `resource_key`. The [resource comparator](https://github.com/morluto/rea/blob/99d2d7ad22bcbd780919760008aba6a88e7d1077/src/domain/webCaptureDiff.ts#L198-L204) treats that key as a membership identity and checks only resource-inventory section completeness. The [producer](https://github.com/morluto/rea/blob/99d2d7ad22bcbd780919760008aba6a88e7d1077/src/browser/CdpCaptureDocuments.ts#L194-L209) correctly retains omitted size as null. Thus a change in observation completeness becomes a membership change even though every jointly known fact agrees. ## Additional context Searched open and closed issues using `"content_size"`, `"resource" "size" "comparison"`, `"resource" "removed" "added"` and `"compare-web-captures"` queries scoped to `morluto/rea`. Related [#4708](https://github.com/morluto/rea/issues/4708) concerns malformed negative resource sizes becoming zero during Electron inspection; this fixture supplies no invalid size and exercises offline comparison of a valid omission. [#6554](https://github.com/morluto/rea/issues/6554) concerns missing script module metadata and script identities; this report concerns the separate resource producer, optional size, resource keys and resource comparison branch. Proposed restoration checks, not executed: this known/unknown-size pair should not prove membership changes; same-input and actual complete-inventory removal controls should retain their intended behavior. ## Triage assessment - Impact level: P2 - Assessment status: supported - Impact basis: Production-parser fixtures and the public offline CLI demonstrate false resource-membership changes and a false overall changed result from a valid optional metadata omission; retained input data is preserved. - Workaround status: available - Workaround basis: The retained one-resource arrays preserve URL, type, MIME type and nullable size; direct inspection of the generated files distinguishes the unavailable size from resource absence without altering source or captures.
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#4129
No description provided.