[GH-ISSUE #7542] [Bug] An incomplete capture with no mapped entities fails to prevent static absence claims #4483

Closed
opened 2026-10-31 00:50:11 +00:00 by bsakaguchi · 0 comments
Owner

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

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

node scripts/rea.mjs reconcile-javascript-runtime INPUT --json returns not-observed-in-capture for an analyzed app.js asset and its containing module when one supplied browser capture explicitly has attach-limited script inventory. It also increments summary.static_not_observed to 2. The result retains coverage.status: "partial" and reports that capture's scripts_complete_within_scope: false.

The input combines a complete capture containing a script inside the declared URL-prefix mapping with an incomplete capture whose only reported script is outside that prefix. The incomplete capture has no mapped entity, but its missing scripts could still include code from the analyzed application. Reversing the capture order produces the same states. Moving its reported script to a mapped, nonexistent file makes the previously absent nodes correctly remain unknown.

Reproduction

  1. Use Node 24.22.1 and the baseline below with its locked dependencies and built runtime. Run from the checkout root. The verification used the existing build; it did not reinstall dependencies or rebuild.
  2. Create a disposable work directory and isolate REA's user state there:
export REPRO_DIR="$(mktemp -d)"
export HOME="$REPRO_DIR/home"
export XDG_CONFIG_HOME="$HOME/config" XDG_CACHE_HOME="$HOME/cache"
export XDG_DATA_HOME="$HOME/data" XDG_STATE_HOME="$HOME/state"
mkdir -p "$HOME"
cat > "$REPRO_DIR/repro.mts" <<'JS'
import { mkdirSync, writeFileSync } from 'node:fs';
import { join } from 'node:path';
import { pathToFileURL } from 'node:url';
import { execFileSync } from 'node:child_process';
const root = process.argv[2];
const load = (path) => import(pathToFileURL(join(process.cwd(), path)).href);
const { createBrowserEvidence } = await load('src/application/BrowserEvidence.ts');
const { inspectWebPageInputSchema } = await load('src/domain/browserObservation.ts');
const { classifyBrowserCompleteness } = await load('src/domain/browserCompleteness.ts');
const { passiveNetworkCapture } = await load('tests/fixtures/passiveNetworkCapture.ts');
const { createWebTextArtifact } = await load('src/domain/webContentArtifact.ts');
const origin = 'https://app.example.test';
const app = join(root, 'app');
mkdirSync(app, { recursive: true });
writeFileSync(join(app, 'app.js'), 'export const app = 1;\n');
writeFileSync(join(app, 'other.js'), 'export const other = 2;\n');
const cli = (...args) => JSON.parse(execFileSync(process.execPath,
  ['scripts/rea.mjs', ...args, '--json'],
  { encoding: 'utf8', maxBuffer: 16000000, stdio: ['ignore', 'pipe', 'pipe'] }));
const analysis = cli('analyze-javascript-application', app);
writeFileSync(join(root, 'analysis.json'), JSON.stringify(analysis));
function capture(path, incomplete, source = null) {
  const result = passiveNetworkCapture([]);
  result.frames = [{ frame_id: 'frame-main', parent_frame_id: null, url: origin, origin }];
  if (incomplete) result.completeness = classifyBrowserCompleteness({
    policyFilteredSections: new Set(), attachLimitedSections: new Set(['scripts']),
    truncatedSections: new Set(), unavailableSections: new Set(['scripts']), excluded: [],
    droppedEvents: { scripts: 0, network_requests: 0, console_events: 0,
      websocket_connections: 0, websocket_frames: 0, webmcp_tools: 0, timeline_events: 0 },
  });
  result.scripts = { total: 1, items: [{
    script_key: `scr_${'0'.repeat(64)}`, frame_id: 'frame-main', url: origin + path, origin,
    cdp_hash: null, length: null, is_module: true, language: 'JavaScript', source_map_url: null,
    resource_reconciliation: { status: 'unmatched', reason: 'no_exact_sanitized_url' },
    source: source === null ? { included: false, reason: 'not-requested' }
      : { included: true, artifact: createWebTextArtifact(source, 'text/javascript') },
  }] };
  const input = inspectWebPageInputSchema.parse({
    cdp_endpoint: 'http://127.0.0.1:9222', target_id: 'page', allowed_origins: [origin],
    observation_ms: 0, include_script_sources: source !== null,
  });
  return createBrowserEvidence('inspect_web_page', input, result, {
    id: 'rea-cdp-browser', name: 'REA Chrome DevTools Protocol observation provider', version: '2',
  });
}
const complete = capture('/assets/other.js', false);
const incomplete = capture('/elsewhere/app.js', true);
const cases = {
  mixed: [complete, incomplete],
  reverse: [incomplete, complete],
  scoped_missing: [complete, capture('/assets/missing.js', true)],
  both_complete: [complete, capture('/elsewhere/app.js', false)],
  unmapped_source: [complete, capture('/elsewhere/app.js', true, 'export const app = 1;\n')],
};
for (const [name, observations] of Object.entries(cases)) {
  const inputPath = join(root, name + '-input.json');
  writeFileSync(inputPath, JSON.stringify({
    static_layers: [{ role: 'application', analysis, runtime_mappings: [{
      kind: 'url-prefix', prefix: origin + '/assets/', artifact_prefix: '',
    }] }], runtime_observations: observations,
  }));
  const evidence = cli('reconcile-javascript-runtime', inputPath);
  writeFileSync(join(root, name + '-output.json'), JSON.stringify(evidence));
  const r = evidence.normalized_result;
  const states = r.static_load_states.filter(s =>
    analysis.normalized_result.graph.nodes.find(n => n.node_id === s.static_node_id)
      ?.observations.some(o => o.label === 'app.js'));
  console.log(JSON.stringify({
    name, static_coverage: analysis.normalized_result.graph.coverage.status,
    coverage: r.coverage.status, static_not_observed: r.summary.static_not_observed,
    scripts_complete: r.runtime_captures.map(c => c.scripts_complete_within_scope),
    app_states: states.map(s => ({ kind: s.kind, status: s.status, reason: s.reason })),
    scripts: r.reconciliations.filter(i => i.entity_kind === 'script')
      .map(i => ({ status: i.status, reason: i.reason, candidates: i.candidate_static_count })),
  }));
}
JS
node_modules/.bin/tsx "$REPRO_DIR/repro.mts" "$REPRO_DIR"
  1. Compare mixed and reverse with scoped_missing. The latter changes only the incomplete script's location to a path inside the exercised prefix, while keeping it absent from the static fixture. both_complete changes the incomplete capture's inventory completeness. unmapped_source instead supplies its exact captured bytes.

The verifier ran the failing input in both orders, the controls, and an additional empty-incomplete-inventory case through the real CLI. Both the static analysis and reconciliation commands ran normally; no process-list workaround or in-process analyzer substitution was used. A replay of the inline script above also reproduced the result.

Expected behavior

A supplied capture with attach-limited script inventory must prevent negative code-presence claims for the potentially affected static nodes, including when that capture happens to contain no entity mapped into the layer. Keep those nodes unknown and exclude them from static_not_observed. Preserve accepted positive matches and the complete-inventory control.

The documented load-state contract requires complete authorized captures for absence, preserves uncertainty from incomplete captures, and explicitly rules out negative code-presence conclusions from attach-limited script coverage. Being unable to map the scripts that were reported does not establish that the missing scripts also fall outside the mapping.

Impact and scope

P2, supported under triage/v1: valid reconciliation input produces an unsupported bounded absence classification and repeats it in the aggregate count. The observed affected scope is one application layer, two browser Evidence records, one ordinary URL-prefix mapping, and incomplete script inventory with no mapped entity. No downstream incident, live-browser frequency, or security boundary crossing was established. Electron, Inspector, and multiple static layers were not exercised.

A practical workaround is to inspect the retained completeness markers and conservatively interpret static absence states as unknown whenever a supplied script inventory is incomplete. The executed result retains scripts_complete_within_scope: false and the original input Evidence, so callers can apply this safeguard without dropping observations or changing the analyzed files.

REA version

Exact node scripts/rea.mjs --version stdout:

6.8.0

Baseline: 65f1c447344a423fa2785847dbcea1e9a9e2454a.

Runtime and platform

Node.js v24.22.1; macOS 27.0.1 arm64. Existing locked dependencies and prebuilt baseline runtime.

Target and artifact context

Two inert local JavaScript files analyzed through the public CLI, plus synthetic browser inspection Evidence built with production constructors and the baseline's normalized passive-capture fixture. No browser or CDP connection was opened. The loopback endpoint is input metadata. The static scripts were parsed, not executed.

Interface

CLI

Earliest failure stage

Transport or result projection

Relevant output or evidence

Selected fields from the executed CLI results:

mixed and reverse:
  static graph coverage: complete
  result coverage: partial
  one capture scripts_complete_within_scope: false
  app.js asset: not-observed-in-capture / not-observed-in-bounded-capture
  app.js containing module: not-observed-in-capture / not-observed-in-bounded-capture
  static_not_observed: 2
  unmapped script: unknown / no-authorized-location-mapping, candidates 0

scoped_missing:
  result coverage: partial
  app.js asset and containing module: unknown / static-or-runtime-coverage-incomplete
  static_not_observed: 0

both_complete:
  result coverage: complete-within-inputs
  app.js asset and containing module: not-observed-in-capture
  static_not_observed: 2

unmapped_source:
  result coverage: partial
  unmapped script: matched / unique-content-match
  app.js asset: loaded
  app.js containing module: resident-in-loaded-asset
  static_not_observed: 0

The other.js asset remains loaded in all these cases. Named-export semantic nodes remain outside runtime scope; the report concerns the asset and containing module carrying the exact app.js label.

Contract locators at the baseline: docs/javascript-runtime-reconciliation.md:134–150. Failure locators: src/domain/javascript/javascriptRuntimeLoadState.ts:66–75 and 98–111. Capture completeness is recognized in src/domain/javascript/javascriptRuntimeReconciliationCaptureParsing.ts:202–205 and 508–525.

Typed outcome or recovery expected

The mixed and reversed inputs should retain unknown load states for app.js and omit those nodes from the definite absence count. Positive correspondences should stay loaded. No particular new reason string or API change is required.

Analysis or suspected cause

Source-derived explanation: the load-state classifier first filters runtime entities to locations that map into the current layer, then checks completeness only on those entities' captures. An incomplete capture with no mapped entity never participates in this gate. A complete mapped capture can therefore permit absence even though another supplied capture has explicitly incomplete code inventory.

The observed unmapped-source control confirms why lack of a location mapping cannot make such a capture irrelevant: captured bytes can still establish application code presence through the global digest fallback in src/domain/javascript/javascriptRuntimeReconciliationMatching.ts:162–184.

Additional context

Searched issues in open and closed states, PRs in all states, and issue comments. This differs from closed #3192, where complete captures had ambiguous static candidates. Here there is no ambiguous candidate; capture-level incompleteness is omitted before static absence classification.

Closed #5884 concerned a missing worker-inventory condition in an exercised capture's completeness predicate. That condition exists at this baseline. Here script incompleteness is already recognized, but the entire capture is skipped by the mapped-entity filter. This is not evidence that either closed fix regressed.

Closed #6061 and merged #6214 concern the unmapped runtime script's own correspondence; that part already correctly returns unknown in this reproduction. #3316 and #5878 concern digest disagreement and encoded URL separators, respectively, neither required here. Open #2068 concerns browser worker discovery/source coverage and does not change the static load-state classifier in its inspected file list.

Originally created by @coygeek on GitHub (Oct 27, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/7542 ### 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 `node scripts/rea.mjs reconcile-javascript-runtime INPUT --json` returns `not-observed-in-capture` for an analyzed `app.js` asset and its containing module when one supplied browser capture explicitly has attach-limited script inventory. It also increments `summary.static_not_observed` to 2. The result retains `coverage.status: "partial"` and reports that capture's `scripts_complete_within_scope: false`. The input combines a complete capture containing a script inside the declared URL-prefix mapping with an incomplete capture whose only reported script is outside that prefix. The incomplete capture has no mapped entity, but its missing scripts could still include code from the analyzed application. Reversing the capture order produces the same states. Moving its reported script to a mapped, nonexistent file makes the previously absent nodes correctly remain unknown. ### Reproduction 1. Use Node 24.22.1 and the baseline below with its locked dependencies and built runtime. Run from the checkout root. The verification used the existing build; it did not reinstall dependencies or rebuild. 2. Create a disposable work directory and isolate REA's user state there: ```sh export REPRO_DIR="$(mktemp -d)" export HOME="$REPRO_DIR/home" export XDG_CONFIG_HOME="$HOME/config" XDG_CACHE_HOME="$HOME/cache" export XDG_DATA_HOME="$HOME/data" XDG_STATE_HOME="$HOME/state" mkdir -p "$HOME" cat > "$REPRO_DIR/repro.mts" <<'JS' import { mkdirSync, writeFileSync } from 'node:fs'; import { join } from 'node:path'; import { pathToFileURL } from 'node:url'; import { execFileSync } from 'node:child_process'; const root = process.argv[2]; const load = (path) => import(pathToFileURL(join(process.cwd(), path)).href); const { createBrowserEvidence } = await load('src/application/BrowserEvidence.ts'); const { inspectWebPageInputSchema } = await load('src/domain/browserObservation.ts'); const { classifyBrowserCompleteness } = await load('src/domain/browserCompleteness.ts'); const { passiveNetworkCapture } = await load('tests/fixtures/passiveNetworkCapture.ts'); const { createWebTextArtifact } = await load('src/domain/webContentArtifact.ts'); const origin = 'https://app.example.test'; const app = join(root, 'app'); mkdirSync(app, { recursive: true }); writeFileSync(join(app, 'app.js'), 'export const app = 1;\n'); writeFileSync(join(app, 'other.js'), 'export const other = 2;\n'); const cli = (...args) => JSON.parse(execFileSync(process.execPath, ['scripts/rea.mjs', ...args, '--json'], { encoding: 'utf8', maxBuffer: 16000000, stdio: ['ignore', 'pipe', 'pipe'] })); const analysis = cli('analyze-javascript-application', app); writeFileSync(join(root, 'analysis.json'), JSON.stringify(analysis)); function capture(path, incomplete, source = null) { const result = passiveNetworkCapture([]); result.frames = [{ frame_id: 'frame-main', parent_frame_id: null, url: origin, origin }]; if (incomplete) result.completeness = classifyBrowserCompleteness({ policyFilteredSections: new Set(), attachLimitedSections: new Set(['scripts']), truncatedSections: new Set(), unavailableSections: new Set(['scripts']), excluded: [], droppedEvents: { scripts: 0, network_requests: 0, console_events: 0, websocket_connections: 0, websocket_frames: 0, webmcp_tools: 0, timeline_events: 0 }, }); result.scripts = { total: 1, items: [{ script_key: `scr_${'0'.repeat(64)}`, frame_id: 'frame-main', url: origin + path, origin, cdp_hash: null, length: null, is_module: true, language: 'JavaScript', source_map_url: null, resource_reconciliation: { status: 'unmatched', reason: 'no_exact_sanitized_url' }, source: source === null ? { included: false, reason: 'not-requested' } : { included: true, artifact: createWebTextArtifact(source, 'text/javascript') }, }] }; const input = inspectWebPageInputSchema.parse({ cdp_endpoint: 'http://127.0.0.1:9222', target_id: 'page', allowed_origins: [origin], observation_ms: 0, include_script_sources: source !== null, }); return createBrowserEvidence('inspect_web_page', input, result, { id: 'rea-cdp-browser', name: 'REA Chrome DevTools Protocol observation provider', version: '2', }); } const complete = capture('/assets/other.js', false); const incomplete = capture('/elsewhere/app.js', true); const cases = { mixed: [complete, incomplete], reverse: [incomplete, complete], scoped_missing: [complete, capture('/assets/missing.js', true)], both_complete: [complete, capture('/elsewhere/app.js', false)], unmapped_source: [complete, capture('/elsewhere/app.js', true, 'export const app = 1;\n')], }; for (const [name, observations] of Object.entries(cases)) { const inputPath = join(root, name + '-input.json'); writeFileSync(inputPath, JSON.stringify({ static_layers: [{ role: 'application', analysis, runtime_mappings: [{ kind: 'url-prefix', prefix: origin + '/assets/', artifact_prefix: '', }] }], runtime_observations: observations, })); const evidence = cli('reconcile-javascript-runtime', inputPath); writeFileSync(join(root, name + '-output.json'), JSON.stringify(evidence)); const r = evidence.normalized_result; const states = r.static_load_states.filter(s => analysis.normalized_result.graph.nodes.find(n => n.node_id === s.static_node_id) ?.observations.some(o => o.label === 'app.js')); console.log(JSON.stringify({ name, static_coverage: analysis.normalized_result.graph.coverage.status, coverage: r.coverage.status, static_not_observed: r.summary.static_not_observed, scripts_complete: r.runtime_captures.map(c => c.scripts_complete_within_scope), app_states: states.map(s => ({ kind: s.kind, status: s.status, reason: s.reason })), scripts: r.reconciliations.filter(i => i.entity_kind === 'script') .map(i => ({ status: i.status, reason: i.reason, candidates: i.candidate_static_count })), })); } JS node_modules/.bin/tsx "$REPRO_DIR/repro.mts" "$REPRO_DIR" ``` 3. Compare `mixed` and `reverse` with `scoped_missing`. The latter changes only the incomplete script's location to a path inside the exercised prefix, while keeping it absent from the static fixture. `both_complete` changes the incomplete capture's inventory completeness. `unmapped_source` instead supplies its exact captured bytes. The verifier ran the failing input in both orders, the controls, and an additional empty-incomplete-inventory case through the real CLI. Both the static analysis and reconciliation commands ran normally; no process-list workaround or in-process analyzer substitution was used. A replay of the inline script above also reproduced the result. ### Expected behavior A supplied capture with attach-limited script inventory must prevent negative code-presence claims for the potentially affected static nodes, including when that capture happens to contain no entity mapped into the layer. Keep those nodes unknown and exclude them from `static_not_observed`. Preserve accepted positive matches and the complete-inventory control. The documented load-state contract requires complete authorized captures for absence, preserves uncertainty from incomplete captures, and explicitly rules out negative code-presence conclusions from attach-limited script coverage. Being unable to map the scripts that were reported does not establish that the missing scripts also fall outside the mapping. ### Impact and scope P2, supported under triage/v1: valid reconciliation input produces an unsupported bounded absence classification and repeats it in the aggregate count. The observed affected scope is one application layer, two browser Evidence records, one ordinary URL-prefix mapping, and incomplete script inventory with no mapped entity. No downstream incident, live-browser frequency, or security boundary crossing was established. Electron, Inspector, and multiple static layers were not exercised. A practical workaround is to inspect the retained completeness markers and conservatively interpret static absence states as unknown whenever a supplied script inventory is incomplete. The executed result retains `scripts_complete_within_scope: false` and the original input Evidence, so callers can apply this safeguard without dropping observations or changing the analyzed files. ### REA version Exact `node scripts/rea.mjs --version` stdout: ```text 6.8.0 ``` Baseline: `65f1c447344a423fa2785847dbcea1e9a9e2454a`. ### Runtime and platform Node.js v24.22.1; macOS 27.0.1 arm64. Existing locked dependencies and prebuilt baseline runtime. ### Target and artifact context Two inert local JavaScript files analyzed through the public CLI, plus synthetic browser inspection Evidence built with production constructors and the baseline's normalized passive-capture fixture. No browser or CDP connection was opened. The loopback endpoint is input metadata. The static scripts were parsed, not executed. ### Interface CLI ### Earliest failure stage Transport or result projection ### Relevant output or evidence Selected fields from the executed CLI results: ```text mixed and reverse: static graph coverage: complete result coverage: partial one capture scripts_complete_within_scope: false app.js asset: not-observed-in-capture / not-observed-in-bounded-capture app.js containing module: not-observed-in-capture / not-observed-in-bounded-capture static_not_observed: 2 unmapped script: unknown / no-authorized-location-mapping, candidates 0 scoped_missing: result coverage: partial app.js asset and containing module: unknown / static-or-runtime-coverage-incomplete static_not_observed: 0 both_complete: result coverage: complete-within-inputs app.js asset and containing module: not-observed-in-capture static_not_observed: 2 unmapped_source: result coverage: partial unmapped script: matched / unique-content-match app.js asset: loaded app.js containing module: resident-in-loaded-asset static_not_observed: 0 ``` The `other.js` asset remains loaded in all these cases. Named-export semantic nodes remain outside runtime scope; the report concerns the asset and containing module carrying the exact `app.js` label. Contract locators at the baseline: `docs/javascript-runtime-reconciliation.md:134–150`. Failure locators: `src/domain/javascript/javascriptRuntimeLoadState.ts:66–75` and `98–111`. Capture completeness is recognized in `src/domain/javascript/javascriptRuntimeReconciliationCaptureParsing.ts:202–205` and `508–525`. ### Typed outcome or recovery expected The mixed and reversed inputs should retain unknown load states for `app.js` and omit those nodes from the definite absence count. Positive correspondences should stay loaded. No particular new reason string or API change is required. ### Analysis or suspected cause Source-derived explanation: the load-state classifier first filters runtime entities to locations that map into the current layer, then checks completeness only on those entities' captures. An incomplete capture with no mapped entity never participates in this gate. A complete mapped capture can therefore permit absence even though another supplied capture has explicitly incomplete code inventory. The observed unmapped-source control confirms why lack of a location mapping cannot make such a capture irrelevant: captured bytes can still establish application code presence through the global digest fallback in `src/domain/javascript/javascriptRuntimeReconciliationMatching.ts:162–184`. ### Additional context Searched issues in open and closed states, PRs in all states, and issue comments. This differs from closed [#3192](https://github.com/morluto/rea/issues/3192), where complete captures had ambiguous static candidates. Here there is no ambiguous candidate; capture-level incompleteness is omitted before static absence classification. Closed [#5884](https://github.com/morluto/rea/issues/5884) concerned a missing worker-inventory condition in an exercised capture's completeness predicate. That condition exists at this baseline. Here script incompleteness is already recognized, but the entire capture is skipped by the mapped-entity filter. This is not evidence that either closed fix regressed. Closed [#6061](https://github.com/morluto/rea/issues/6061) and merged [#6214](https://github.com/morluto/rea/pull/6214) concern the unmapped runtime script's own correspondence; that part already correctly returns unknown in this reproduction. [#3316](https://github.com/morluto/rea/issues/3316) and [#5878](https://github.com/morluto/rea/issues/5878) concern digest disagreement and encoded URL separators, respectively, neither required here. Open [#2068](https://github.com/morluto/rea/pull/2068) concerns browser worker discovery/source coverage and does not change the static load-state classifier in its inspected file list.
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#4483
No description provided.