[GH-ISSUE #3932] [Bug] Type-only import-equals creates a false source-map dependency #1540

Closed
opened 2026-10-16 21:06:53 +00:00 by bsakaguchi · 2 comments
Owner

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

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

analyze_web_bundle and analyze-web-bundle --fetch-source-maps return a require dependency for the erased TypeScript declaration import type X = require("m") in a source map's original source. This adds a false module dependency to otherwise usable source-map Evidence. The behavior was reproduced through both adapters using the repository's fake CDP browser at the production protocol boundary.

Reproduction

Use the baseline under Runtime or environment with its dependencies installed and runtime already built. The following focused test exercises the registered MCP tool and the built CLI. It uses only the repository's loopback fake CDP browser, requires no native provider or real browser, and does not execute the original TypeScript source.

  1. Save the following as tests/boundary/mcp/importEqualsReproduction.test.ts:
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
import { expect, it } from "vitest";
import { createTestBinarySession } from "../../fixtures/binarySession.js";
import { CdpBrowserProvider } from "../../../src/browser/CdpBrowserProvider.js";
import { createServer } from "../../../src/server/createServer.js";
import { startFakeCdpBrowser } from "../../fixtures/fakeCdpBrowser.js";
import { runCliJson } from "../../fixtures/cliJsonProcess.js";

it("checks type-only import-equals through MCP and CLI", async () => {
  const text = 'import type X = require("m");\nimport Y = require("n");\nimport type { T } from "ordinary-types";\nexport type { T } from "export-types";\n';
  const browser = await startFakeCdpBrowser({ sourceMapBody: JSON.stringify({
    version: 3, sources: ["orig.ts"], sourcesContent: [text], names: [], mappings: "AAAA",
  }) });
  const session = createTestBinarySession(() => { throw new Error("no binary provider"); });
  const server = createServer({ kind: "session", session }, { browserObservation: new CdpBrowserProvider() });
  const client = new Client({ name: "type-only-import-verifier", version: "1" });
  try {
    const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair();
    await server.connect(serverTransport);
    await client.connect(clientTransport);
    expect((await client.listTools()).tools.map(({ name }) => name)).toContain("analyze_web_bundle");
    const response = await client.callTool({ name: "analyze_web_bundle", arguments: {
      cdp_endpoint: browser.endpoint, target_id: "allowed-page", allowed_origins: [browser.allowedOrigin], observation_ms: 0, fetch_source_maps: true,
    } });
    expect(response.isError, JSON.stringify(response)).not.toBe(true);
    const cli = await runCliJson(["analyze-web-bundle", browser.endpoint, "allowed-page", "--allowed-origins", browser.allowedOrigin, "--observation-ms", "0", "--fetch-source-maps", "--json"]);
    for (const [adapter, result] of [["MCP", response.structuredContent], ["CLI", cli]] as const) {
      const map = (result as any).normalized_result.observations.source_maps.items[0];
      console.log(JSON.stringify({ adapter, map_status: map.status, original_module_edges: map.original_module_edges, source_retained: map.original_sources[0].artifact.text === text }));
      expect(map.status).toBe("included");
      expect(map.original_module_edges.map(({ kind, specifier }: any) => ({ kind, specifier }))).toEqual([
        { kind: "require", specifier: "m" }, { kind: "require", specifier: "n" },
      ]);
      expect(map.original_sources[0].artifact.text).toBe(text);
    }
  } finally {
    await client.close();
    await server.close();
    await session.close();
    await browser.close();
  }
});
  1. Run the focused test from the repository root with the pinned Node.js and npm on PATH, isolating configuration under a disposable home:
reproduction_home=$(mktemp -d)
HOME="$reproduction_home" npx vitest run tests/boundary/mcp/importEqualsReproduction.test.ts --maxWorkers=1
rm tests/boundary/mcp/importEqualsReproduction.test.ts
rm -rf "$reproduction_home"
  1. Inspect the logged original_module_edges for each adapter. The diagnostic test passes when the current incorrect output is present. It is not a regression test that asserts the desired behavior.

The source map contains version: 3, sources: ["orig.ts"], names: [], mappings: "AAAA", and the source shown in the test. The MCP request explicitly sets fetch_source_maps: true, observation_ms: 0, and the fake target's allowed origin. The CLI uses the corresponding flags.

Expected behavior

A whole-declaration import type is an erased source fact and must not produce a runtime dependency candidate. The original-source edge list must omit m. It should retain the value import-equals dependency candidate for n and continue to exclude the ordinary whole-statement type-only import and export controls. Retaining n is a static candidate claim, not proof that a compiler emitted it or that a loader executed it.

The source-map subsystem already requires value import-equals dependencies and excludes ordinary whole-statement type-only imports and exports. The documented TypeScript rule agrees with those subsystem tests.

tests/boundary/browser/webSourceMapFetcher.test.ts:1100-1118 covers value import-equals and ordinary whole-statement type-only exclusions. docs/javascript-application-workflows.md:26-31 states the explicit type-only rule primarily for application analysis. src/contracts/browserToolContracts.ts:174-182 establishes public bundle/source-map analysis.

Impact and scope

P2. Both public adapters return incorrect dependency evidence for this bounded TypeScript form. Source bytes and the map remain usable; no crash, data loss, or downstream failure was observed. The preserved original source allows an analyst to identify and disregard this concrete false edge. A general automatic filter was not tested.

REA version

rea 6.5.0 at source commit 4692bb3330

Runtime and platform

Node 24.18.0; macOS arm64

Target and artifact context

Version-3 source map containing embedded orig.ts, served by the loopback HTTP/WebSocket fake CDP browser. Registered MCP and built CLI both exercised; no target code or real browser executed.

Interface

MCP

Earliest failure stage

Native analysis or reconstruction

Relevant output or evidence

Observed in one completed integration-test run, both MCP and the built CLI returned an included map with these edge kind/specifier pairs:

[
  { "kind": "require", "specifier": "m" },
  { "kind": "require", "specifier": "n" }
]

Both edges also carried the original orig.ts source URL and resolved_source: null. The exact original source text was retained. Neither the ordinary type-only import nor export generated an edge. The test completed successfully because its assertions describe this observed defect.

Source inspection shows that external import-equals declarations receive a require edge without a declaration-level type-only check. A separate parser probe confirmed importKind: "type" on the erased declaration and importKind: "value" on its control. Real Chromium capture and downstream graph effects were not tested.

Typed outcome or recovery expected

Run the same supported calls with the same source map. Each returned original_module_edges list must contain only the require candidate for n; m must be absent. The original source artifact and valid mapping must remain available, and the ordinary type-only controls must remain excluded. The diagnostic assertions will need to express that expected result when used as regression coverage. Choose the implementation and regression-test structure independently.

Analysis or suspected cause

Source-derived: src/browser/WebSourceMapFetcher.ts:1106-1113 emits require for external TSImportEqualsDeclaration nodes without checking declaration-level importKind. Original-source traversal at lines 738-800 reaches that branch. A separate Babel parser probe returned importKind: "type" for m and importKind: "value" for the n control.

Additional context

The cited failing code is unchanged on current origin/main at 22ec8bdb2f7d4626587f23b1eed4a207df600386.

Related closed #2598 corrected ordinary type-only imports/exports in source-map originals, but the independently reproduced import-equals form remains. Closed #2058 and open PRs #2081 and #2060 concern application-analysis module/type handling; their file changes do not include src/browser/WebSourceMapFetcher.ts.

Originally created by @coygeek on GitHub (Oct 15, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/3932 ### 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 `analyze_web_bundle` and `analyze-web-bundle --fetch-source-maps` return a `require` dependency for the erased TypeScript declaration `import type X = require("m")` in a source map's original source. This adds a false module dependency to otherwise usable source-map Evidence. The behavior was reproduced through both adapters using the repository's fake CDP browser at the production protocol boundary. ### Reproduction Use the baseline under Runtime or environment with its dependencies installed and runtime already built. The following focused test exercises the registered MCP tool and the built CLI. It uses only the repository's loopback fake CDP browser, requires no native provider or real browser, and does not execute the original TypeScript source. 1. Save the following as `tests/boundary/mcp/importEqualsReproduction.test.ts`: ```typescript import { Client, InMemoryTransport } from "@modelcontextprotocol/client"; import { expect, it } from "vitest"; import { createTestBinarySession } from "../../fixtures/binarySession.js"; import { CdpBrowserProvider } from "../../../src/browser/CdpBrowserProvider.js"; import { createServer } from "../../../src/server/createServer.js"; import { startFakeCdpBrowser } from "../../fixtures/fakeCdpBrowser.js"; import { runCliJson } from "../../fixtures/cliJsonProcess.js"; it("checks type-only import-equals through MCP and CLI", async () => { const text = 'import type X = require("m");\nimport Y = require("n");\nimport type { T } from "ordinary-types";\nexport type { T } from "export-types";\n'; const browser = await startFakeCdpBrowser({ sourceMapBody: JSON.stringify({ version: 3, sources: ["orig.ts"], sourcesContent: [text], names: [], mappings: "AAAA", }) }); const session = createTestBinarySession(() => { throw new Error("no binary provider"); }); const server = createServer({ kind: "session", session }, { browserObservation: new CdpBrowserProvider() }); const client = new Client({ name: "type-only-import-verifier", version: "1" }); try { const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); await server.connect(serverTransport); await client.connect(clientTransport); expect((await client.listTools()).tools.map(({ name }) => name)).toContain("analyze_web_bundle"); const response = await client.callTool({ name: "analyze_web_bundle", arguments: { cdp_endpoint: browser.endpoint, target_id: "allowed-page", allowed_origins: [browser.allowedOrigin], observation_ms: 0, fetch_source_maps: true, } }); expect(response.isError, JSON.stringify(response)).not.toBe(true); const cli = await runCliJson(["analyze-web-bundle", browser.endpoint, "allowed-page", "--allowed-origins", browser.allowedOrigin, "--observation-ms", "0", "--fetch-source-maps", "--json"]); for (const [adapter, result] of [["MCP", response.structuredContent], ["CLI", cli]] as const) { const map = (result as any).normalized_result.observations.source_maps.items[0]; console.log(JSON.stringify({ adapter, map_status: map.status, original_module_edges: map.original_module_edges, source_retained: map.original_sources[0].artifact.text === text })); expect(map.status).toBe("included"); expect(map.original_module_edges.map(({ kind, specifier }: any) => ({ kind, specifier }))).toEqual([ { kind: "require", specifier: "m" }, { kind: "require", specifier: "n" }, ]); expect(map.original_sources[0].artifact.text).toBe(text); } } finally { await client.close(); await server.close(); await session.close(); await browser.close(); } }); ``` 2. Run the focused test from the repository root with the pinned Node.js and npm on `PATH`, isolating configuration under a disposable home: ```sh reproduction_home=$(mktemp -d) HOME="$reproduction_home" npx vitest run tests/boundary/mcp/importEqualsReproduction.test.ts --maxWorkers=1 rm tests/boundary/mcp/importEqualsReproduction.test.ts rm -rf "$reproduction_home" ``` 3. Inspect the logged `original_module_edges` for each adapter. The diagnostic test passes when the current incorrect output is present. It is not a regression test that asserts the desired behavior. The source map contains `version: 3`, `sources: ["orig.ts"]`, `names: []`, `mappings: "AAAA"`, and the source shown in the test. The MCP request explicitly sets `fetch_source_maps: true`, `observation_ms: 0`, and the fake target's allowed origin. The CLI uses the corresponding flags. ### Expected behavior A whole-declaration `import type` is an erased source fact and must not produce a runtime dependency candidate. The original-source edge list must omit `m`. It should retain the value import-equals dependency candidate for `n` and continue to exclude the ordinary whole-statement type-only import and export controls. Retaining `n` is a static candidate claim, not proof that a compiler emitted it or that a loader executed it. The source-map subsystem already requires value import-equals dependencies and excludes ordinary whole-statement type-only imports and exports. The documented TypeScript rule agrees with those subsystem tests. `tests/boundary/browser/webSourceMapFetcher.test.ts:1100-1118` covers value import-equals and ordinary whole-statement type-only exclusions. `docs/javascript-application-workflows.md:26-31` states the explicit type-only rule primarily for application analysis. `src/contracts/browserToolContracts.ts:174-182` establishes public bundle/source-map analysis. ### Impact and scope P2. Both public adapters return incorrect dependency evidence for this bounded TypeScript form. Source bytes and the map remain usable; no crash, data loss, or downstream failure was observed. The preserved original source allows an analyst to identify and disregard this concrete false edge. A general automatic filter was not tested. ### REA version rea 6.5.0 at source commit 4692bb33300c7cb77b095b6cc0c33da861c1b860 ### Runtime and platform Node 24.18.0; macOS arm64 ### Target and artifact context Version-3 source map containing embedded `orig.ts`, served by the loopback HTTP/WebSocket fake CDP browser. Registered MCP and built CLI both exercised; no target code or real browser executed. ### Interface MCP ### Earliest failure stage Native analysis or reconstruction ### Relevant output or evidence Observed in one completed integration-test run, both MCP and the built CLI returned an included map with these edge kind/specifier pairs: ```json [ { "kind": "require", "specifier": "m" }, { "kind": "require", "specifier": "n" } ] ``` Both edges also carried the original `orig.ts` source URL and `resolved_source: null`. The exact original source text was retained. Neither the ordinary type-only import nor export generated an edge. The test completed successfully because its assertions describe this observed defect. Source inspection shows that external import-equals declarations receive a `require` edge without a declaration-level type-only check. A separate parser probe confirmed `importKind: "type"` on the erased declaration and `importKind: "value"` on its control. Real Chromium capture and downstream graph effects were not tested. ### Typed outcome or recovery expected Run the same supported calls with the same source map. Each returned `original_module_edges` list must contain only the `require` candidate for `n`; `m` must be absent. The original source artifact and valid mapping must remain available, and the ordinary type-only controls must remain excluded. The diagnostic assertions will need to express that expected result when used as regression coverage. Choose the implementation and regression-test structure independently. ### Analysis or suspected cause Source-derived: `src/browser/WebSourceMapFetcher.ts:1106-1113` emits `require` for external `TSImportEqualsDeclaration` nodes without checking declaration-level `importKind`. Original-source traversal at lines 738-800 reaches that branch. A separate Babel parser probe returned `importKind: "type"` for `m` and `importKind: "value"` for the `n` control. ### Additional context The cited failing code is unchanged on current `origin/main` at `22ec8bdb2f7d4626587f23b1eed4a207df600386`. Related closed #2598 corrected ordinary type-only imports/exports in source-map originals, but the independently reproduced import-equals form remains. Closed #2058 and open PRs #2081 and #2060 concern application-analysis module/type handling; their file changes do not include `src/browser/WebSourceMapFetcher.ts`.
Author
Owner

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

Confirmed: import type X = require("m") is erased at compile time and creates no runtime dependency, so the source-map Evidence should not carry a require edge for it. This is the type-only counterpart to #2081 — while that fix retains real import-equals module dependencies, the type-only variant must drop the false dependency (and its byte cost) once type erasure has removed it from the original source.

<!-- gh-comment-id:6186259066 --> @XinwangTao commented on GitHub (Oct 15, 2026): Confirmed: import type X = require("m") is erased at compile time and creates no runtime dependency, so the source-map Evidence should not carry a require edge for it. This is the type-only counterpart to #2081 — while that fix retains real import-equals module dependencies, the type-only variant must drop the false dependency (and its byte cost) once type erasure has removed it from the original source.
Author
Owner

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

Working on this. Reproduced with your fake-CDP test through the registered MCP tool and the built CLI: both returned require edges for m and n. The source-map collector now skips an import-equals whose importKind is type, and the reference-source import parser gets the same guard (same erased form, same false requires edge); the value form keeps its edge. After the fix both adapters return only n with the original source retained. Draft PR follows shortly.

<!-- gh-comment-id:6189931016 --> @yankikucuk commented on GitHub (Oct 15, 2026): Working on this. Reproduced with your fake-CDP test through the registered MCP tool and the built CLI: both returned `require` edges for `m` and `n`. The source-map collector now skips an import-equals whose `importKind` is `type`, and the reference-source import parser gets the same guard (same erased form, same false `requires` edge); the value form keeps its edge. After the fix both adapters return only `n` with the original source retained. Draft PR follows shortly.
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#1540
No description provided.