[GH-ISSUE #6554] [Bug] Missing script module metadata is reported as a removal and addition #3943

Closed
opened 2026-10-31 00:45:28 +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/6554

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 offline compare-web-captures CLI reports a removed script and an added script when the only producer-fact difference is that isModule: false was supplied in one capture and omitted in the other. The production metadata parser converts that omission to is_module: null, which explicitly means unknown. URL, producer hash, length, language, and source-map facts are identical, and both inventories contain exactly one script.

Reproduction

  1. With the installed package from REA version already available, save the following generator as fixture.mjs in an empty disposable directory. Pass its package root as the argument; REA_PACKAGE_ROOT below denotes that absolute package directory. All URLs are inert synthetic identities.
  2. Run node fixture.mjs "$REA_PACKAGE_ROOT" in that directory, then node "$REA_PACKAGE_ROOT/scripts/rea.mjs" compare-web-captures before.json after.json --json.
  3. Inspect the indicated dimension under normalized_result.dimensions.

The equivalent fixture generation, production-schema/domain comparison, and public CLI command were executed locally. Each failing CLI comparison ran once and exited 0 with empty stderr. No live browser capture was attempted.

import { writeFileSync } from 'node:fs';
import { resolve } from 'node:path';
import { pathToFileURL } from 'node:url';
const root = resolve(process.argv[2]);
const load = p => import(pathToFileURL(`${root}/dist/${p}`).href);
const t = new Date(0).toISOString();
const base = () => ({
  browser: { product: 'fixture', protocol_version: '1.3', revision: 'fixture', user_agent: 'fixture', js_version: 'fixture' },
  target: { target_id: 'page', type: 'page', title: 'Fixture', url: 'https://app.example.test/', origin: 'https://app.example.test', attached: false },
  capture_window: { started_at: t, ended_at: t, observation_ms: 0 },
  completeness: { status: 'complete_within_window', conditions: ['complete_within_window'], policy_filtered_sections: [], attach_limited_sections: [], truncated_sections: [], unavailable_sections: [], excluded: [], dropped_events: { scripts: 0, network_requests: 0, console_events: 0, websocket_connections: 0, websocket_frames: 0, webmcp_tools: 0, timeline_events: 0, total: 0 } },
  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: t, prior_activity_available: false },
  console: { events: [], coverage_started_at: t, prior_activity_available: false },
  metadata: { responses: [], dom_urls: [], agent_hints: [], excluded_dom_urls: 0, headers_allowlisted: true },
  storage: { origin: 'https://app.example.test', usage_bytes: null, quota_bytes: null, local_storage_keys: [], session_storage_keys: [], indexed_db_names: [], cache_names: [], values_redacted: true },
  limitations: ['Synthetic normalized capture; no live browser.'],
});
const { scriptMetadataValues } = await load('browser/CdpCaptureValues.js');
const { stableWebScriptKey } = await load('domain/webInventory.js');
const capture = declareModule => {
  const c = base();
  const m = scriptMetadataValues({ hash: 'a'.repeat(64), length: 4, ...(declareModule ? { isModule: false } : {}) });
  const item = { url: 'https://app.example.test/app.js', origin: 'https://app.example.test', cdp_hash: m.hash, length: m.length, is_module: m.isModule, language: 'JavaScript', source_map_url: null, source: { included: false, reason: 'not_requested' }, resource_reconciliation: { status: 'unmatched', reason: 'no_exact_sanitized_url' } };
  c.scripts = { total: 1, items: [{ script_key: stableWebScriptKey(item), ...item }] };
  return c;
};
const before = capture(true), after = capture(false);
for (const [name, capture] of [['before', before], ['after', after]])
  writeFileSync(`${name}.json`, JSON.stringify({ inspection: capture }));

Expected behavior

A missing producer module classification should remain unknown, rather than prove that the unchanged known script was removed and another added. The metadata parser contract says omitted or invalid declarations remain unknown, and the normalized schema retains a nullable module classification. The comparison's changed-status contract requires an observed difference. A known false to known true change remains a distinct, comparable input.

Impact and scope

One published-runtime CLI comparison produced two definite script inventory changes from a missing metadata fact. Its production-boundary same-input control reported scripts.status: "unchanged". The overall result was also changed. This can mislead before/after analysis when producer metadata completeness differs; the rate of such omissions in real browsers was not measured. No script source content was read or changed.

REA version

Published npm package rea-agents 6.7.0; relevant current-source paths were also inspected at 6c4d880f2dd9ca7d42593be916f90ec19b8a09d7.

Runtime and platform

Node.js v26.12.1; Linux x64. No Hopper, Ghidra, browser, network service, or device was used.

Target and artifact context

Two synthetic normalized passive captures containing one script each. The production scriptMetadataValues and stableWebScriptKey functions constructed the nullable metadata and actual keys, rather than manually assigning mismatched identities.

Interface

CLI

Earliest failure stage

Native analysis or reconstruction

Relevant output or evidence

Observed public CLI result, obtained using only local JSON files:

{
  "status": "changed",
  "total_changes": 2,
  "changes": [
    {
      "identity": "scr_083fc7d928c0e40da068f1967e19fa688f8e4a6a9f1bc4b7d10926a03002ef14",
      "change": "added"
    },
    {
      "identity": "scr_9683f3b651072c0624fa740d8630107f5c62eb3f8ffc625f4ccc6d7c3ae5db33",
      "change": "removed"
    }
  ],
  "reason": null
}

The script facts were identical except for is_module: false versus is_module: null and the resulting key. The same-input production-boundary control returned zero script changes and unchanged status. No post-draft rerun was performed.

Analysis or suspected cause

Source-derived: stable script keys include the nullable module flag, and comparison treats these keys as complete membership identities. Thus loss of the classification changes the key and is reported as two inventory membership changes, despite the parser's explicit unknown representation.

Additional context

This differs from #3879, which concerns unequal retained source digests being ignored. This fixture does not retain source; its failing signal is false membership changes caused solely by unavailable producer metadata.

Restoration check (not executed): the supplied known-versus-unknown module pair must not prove removal/addition; a genuinely removed script in complete inventories and a comparable known module-flag change must remain reportable.

Triage assessment

  • Impact level: P2
  • Assessment status: supported
  • Impact basis: The offline comparison emits a definite website-change result from incomplete observation facts on the bounded synthetic input; no target state is modified.
  • Workaround status: unverified
  • Workaround basis: Recapturing complete matching facts could avoid the affected input, but no live-provider recapture or workaround was executed.
Originally created by @coygeek on GitHub (Oct 23, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/6554 ### 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 offline `compare-web-captures` CLI reports a removed script and an added script when the only producer-fact difference is that `isModule: false` was supplied in one capture and omitted in the other. The production metadata parser converts that omission to `is_module: null`, which explicitly means unknown. URL, producer hash, length, language, and source-map facts are identical, and both inventories contain exactly one script. ### Reproduction 1. With the installed package from REA version already available, save the following generator as `fixture.mjs` in an empty disposable directory. Pass its package root as the argument; `REA_PACKAGE_ROOT` below denotes that absolute package directory. All URLs are inert synthetic identities. 2. Run `node fixture.mjs "$REA_PACKAGE_ROOT"` in that directory, then `node "$REA_PACKAGE_ROOT/scripts/rea.mjs" compare-web-captures before.json after.json --json`. 3. Inspect the indicated dimension under `normalized_result.dimensions`. The equivalent fixture generation, production-schema/domain comparison, and public CLI command were executed locally. Each failing CLI comparison ran once and exited 0 with empty stderr. No live browser capture was attempted. ```javascript import { writeFileSync } from 'node:fs'; import { resolve } from 'node:path'; import { pathToFileURL } from 'node:url'; const root = resolve(process.argv[2]); const load = p => import(pathToFileURL(`${root}/dist/${p}`).href); const t = new Date(0).toISOString(); const base = () => ({ browser: { product: 'fixture', protocol_version: '1.3', revision: 'fixture', user_agent: 'fixture', js_version: 'fixture' }, target: { target_id: 'page', type: 'page', title: 'Fixture', url: 'https://app.example.test/', origin: 'https://app.example.test', attached: false }, capture_window: { started_at: t, ended_at: t, observation_ms: 0 }, completeness: { status: 'complete_within_window', conditions: ['complete_within_window'], policy_filtered_sections: [], attach_limited_sections: [], truncated_sections: [], unavailable_sections: [], excluded: [], dropped_events: { scripts: 0, network_requests: 0, console_events: 0, websocket_connections: 0, websocket_frames: 0, webmcp_tools: 0, timeline_events: 0, total: 0 } }, 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: t, prior_activity_available: false }, console: { events: [], coverage_started_at: t, prior_activity_available: false }, metadata: { responses: [], dom_urls: [], agent_hints: [], excluded_dom_urls: 0, headers_allowlisted: true }, storage: { origin: 'https://app.example.test', usage_bytes: null, quota_bytes: null, local_storage_keys: [], session_storage_keys: [], indexed_db_names: [], cache_names: [], values_redacted: true }, limitations: ['Synthetic normalized capture; no live browser.'], }); const { scriptMetadataValues } = await load('browser/CdpCaptureValues.js'); const { stableWebScriptKey } = await load('domain/webInventory.js'); const capture = declareModule => { const c = base(); const m = scriptMetadataValues({ hash: 'a'.repeat(64), length: 4, ...(declareModule ? { isModule: false } : {}) }); const item = { url: 'https://app.example.test/app.js', origin: 'https://app.example.test', cdp_hash: m.hash, length: m.length, is_module: m.isModule, language: 'JavaScript', source_map_url: null, source: { included: false, reason: 'not_requested' }, resource_reconciliation: { status: 'unmatched', reason: 'no_exact_sanitized_url' } }; c.scripts = { total: 1, items: [{ script_key: stableWebScriptKey(item), ...item }] }; return c; }; const before = capture(true), after = capture(false); for (const [name, capture] of [['before', before], ['after', after]]) writeFileSync(`${name}.json`, JSON.stringify({ inspection: capture })); ``` ### Expected behavior A missing producer module classification should remain unknown, rather than prove that the unchanged known script was removed and another added. The [metadata parser contract](https://github.com/morluto/rea/blob/6c4d880f2dd9ca7d42593be916f90ec19b8a09d7/src/browser/CdpCaptureValues.ts#L45-L63) says omitted or invalid declarations remain unknown, and [the normalized schema](https://github.com/morluto/rea/blob/6c4d880f2dd9ca7d42593be916f90ec19b8a09d7/src/domain/cdpObservationSchemas.ts#L23-L44) retains a nullable module classification. The comparison's [changed-status contract](https://github.com/morluto/rea/blob/6c4d880f2dd9ca7d42593be916f90ec19b8a09d7/src/domain/webCaptureDiff.ts#L43-L48) requires an observed difference. A known `false` to known `true` change remains a distinct, comparable input. ### Impact and scope One published-runtime CLI comparison produced two definite script inventory changes from a missing metadata fact. Its production-boundary same-input control reported `scripts.status: "unchanged"`. The overall result was also changed. This can mislead before/after analysis when producer metadata completeness differs; the rate of such omissions in real browsers was not measured. No script source content was read or changed. ### REA version Published npm package `rea-agents` 6.7.0; relevant current-source paths were also inspected at `6c4d880f2dd9ca7d42593be916f90ec19b8a09d7`. ### Runtime and platform Node.js v26.12.1; Linux x64. No Hopper, Ghidra, browser, network service, or device was used. ### Target and artifact context Two synthetic normalized passive captures containing one script each. The production `scriptMetadataValues` and `stableWebScriptKey` functions constructed the nullable metadata and actual keys, rather than manually assigning mismatched identities. ### Interface CLI ### Earliest failure stage Native analysis or reconstruction ### Relevant output or evidence Observed public CLI result, obtained using only local JSON files: ```json { "status": "changed", "total_changes": 2, "changes": [ { "identity": "scr_083fc7d928c0e40da068f1967e19fa688f8e4a6a9f1bc4b7d10926a03002ef14", "change": "added" }, { "identity": "scr_9683f3b651072c0624fa740d8630107f5c62eb3f8ffc625f4ccc6d7c3ae5db33", "change": "removed" } ], "reason": null } ``` The script facts were identical except for `is_module: false` versus `is_module: null` and the resulting key. The same-input production-boundary control returned zero script changes and unchanged status. No post-draft rerun was performed. ### Analysis or suspected cause Source-derived: [stable script keys include the nullable module flag](https://github.com/morluto/rea/blob/6c4d880f2dd9ca7d42593be916f90ec19b8a09d7/src/domain/webInventory.ts#L113-L125), and [comparison treats these keys as complete membership identities](https://github.com/morluto/rea/blob/6c4d880f2dd9ca7d42593be916f90ec19b8a09d7/src/domain/webCaptureDiff.ts#L185-L196). Thus loss of the classification changes the key and is reported as two inventory membership changes, despite the parser's explicit unknown representation. ### Additional context This differs from [#3879](https://github.com/morluto/rea/issues/3879), which concerns unequal retained source digests being ignored. This fixture does not retain source; its failing signal is false membership changes caused solely by unavailable producer metadata. Restoration check (not executed): the supplied known-versus-unknown module pair must not prove removal/addition; a genuinely removed script in complete inventories and a comparable known module-flag change must remain reportable. ## Triage assessment - Impact level: P2 - Assessment status: supported - Impact basis: The offline comparison emits a definite website-change result from incomplete observation facts on the bounded synthetic input; no target state is modified. - Workaround status: unverified - Workaround basis: Recapturing complete matching facts could avoid the affected input, but no live-provider recapture or workaround was executed.
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#3943
No description provided.