[GH-ISSUE #6612] [Bug] Reference-source import resolves createRequire dependencies against the wrong directory #3997

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

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

Observed through the production importReferenceSource application boundary: an ES module creates a CommonJS loader with createRequire(new URL('./sub/entry.cjs', import.meta.url)), then calls require('./dep.cjs'). With both root-level dep.cjs and sub/dep.cjs inventoried, REA records a parsed internal dependency to root-level dep.cjs. Node's resolve-only control selects sub/dep.cjs for that same loader base. A separate createRequire(import.meta.url) control correctly selects root-level dep.cjs in both REA and Node.

All fixture files were inventoried, with no parse failures, exclusions, or entry limitations. The graph's overall inventory_state is partial because it retains the general pathname-race limitation; this report does not claim a complete graph. CLI/MCP transport and downstream comparison behavior were not executed.

Reproduction

The following probe ran once with both the failing input and control. An earlier exploratory import independently showed the same wrong edge before this combined probe. The imported files are inert UTF-8 fixtures; they were never executed. The only Node loader operation is createRequire(...).resolve(...), which resolves the filename without loading its module. sub/entry.cjs need not exist for Node's createRequire API to use that filename as its base.

With the source checkout's locked dependencies installed, save this as probe.mts outside the checkout:

import { mkdir, writeFile } from 'node:fs/promises';
import { createRequire } from 'node:module';
import { join, relative, resolve } from 'node:path';
import { pathToFileURL } from 'node:url';
const repo = resolve(process.argv[2]);
const root = resolve(process.argv[3]);
await mkdir(join(root, 'sub'), { recursive: true });
const files = {
  'main.mjs': `import { createRequire } from 'node:module';
const require = createRequire(new URL('./sub/entry.cjs', import.meta.url));
require('./dep.cjs');
`,
  'control.mjs': `import { createRequire } from 'node:module';
const require = createRequire(import.meta.url);
require('./dep.cjs');
`,
  'dep.cjs': 'module.exports = "root";\n',
  'sub/dep.cjs': 'module.exports = "sub";\n',
};
for (const [path, text] of Object.entries(files)) await writeFile(join(root, path), text);
const load = (path) => import(pathToFileURL(join(repo, 'src', path + '.ts')).href);
const { importReferenceSource } = await load('application/ReferenceSourceImport');
const { ArtifactResourceScope } = await load('artifacts/ArtifactResourceScope');
const resources = new ArtifactResourceScope();
try {
  const result = await importReferenceSource({ root, caller: 'research-probe', policy: { secretPatterns: [] } }, resources);
  if (!result.ok) throw new Error(JSON.stringify(result.error));
  const graph = result.value;
  console.log(JSON.stringify({
    inventory_state: graph.inventory_state,
    entries: graph.entries.map(({ path, limitations }) => ({ path, limitations })),
    relationships: graph.relationships.filter(({ kind }) => kind === 'requires'),
    parse_failures: graph.parse_failures, exclusions: graph.exclusions,
    limitations: graph.limitations,
  }));
  for (const [source, base] of [['main.mjs', './sub/entry.cjs'], ['control.mjs', './control.mjs']]) {
    const loader = createRequire(new URL(base, pathToFileURL(join(root, source))));
    console.log(JSON.stringify({ source, node_resolve: relative(root, loader.resolve('./dep.cjs')) }));
  }
} finally { await resources.close(); }

Run with fresh fixture and configuration directories:

repo="$PWD"
work="$(mktemp -d)"
mkdir -p "$work/home" "$work/config" "$work/cache" "$work/tmp"
HOME="$work/home" XDG_CONFIG_HOME="$work/config" XDG_CACHE_HOME="$work/cache" TMPDIR="$work/tmp" \
  node --import "$repo/node_modules/tsx/dist/loader.mjs" /path/to/probe.mts "$repo" "$work/fixtures"

This invokes the same production importer used by the public command, with caller: "research-probe" and an empty caller exclusion policy. It does not substitute parser, reader, or resolver implementations. No probe was run after this draft was saved.

Expected behavior

The importer should preserve a statically recoverable createRequire base when resolving that loader's relative dependency, or explicitly retain unresolved uncertainty if it cannot recover the base. It should not mark a different existing file as a parsed internal dependency. The executed Node resolution establishes sub/dep.cjs for the failing fixture, while the current-directory control establishes dep.cjs.

REA already recognizes create-require loaders and emits their calls as runtime-require relationships. The historical-source documentation describes the committed reference graph as input for comparison with the current artifact. This expectation does not require evaluating arbitrary dynamic loader bases or executing imported code.

Impact and scope

The bounded valid fixture records the wrong dependency despite containing both candidate files. This changes the historical graph supplied to subsequent analysis. No downstream comparison was tested, no source mutation occurred, and real-world frequency is unknown. A result-preserving workaround within REA was not established; changing the fixture's loader base would change the program being analyzed.

REA version

REA 6.7.0, source baseline, with locked dependencies. The direct importer call leaves its optional importer_version metadata unset; the baseline identifies the code actually imported.

Runtime and platform

Node.js v26.12.1, Linux x64, using the repository's installed tsx loader. No executable target, source fixture module, or external analysis provider was started.

Target and artifact context

Four authored files: main.mjs, control.mjs, root-level dep.cjs, and sub/dep.cjs. Both dependency files contain distinct string exports. The source tree has no Git repository, ignore file, package manifest, symlink, or excluded entry. The two loader bases are the only behavioral difference between the failing module and control.

Interface

Other

Earliest failure stage

Native analysis or reconstruction

Relevant output or evidence

The executed production import returned these requires relationships:

[
  {"from_path":"control.mjs","to":"dep.cjs","kind":"requires","resolution":"internal","parse_state":"parsed"},
  {"from_path":"main.mjs","to":"dep.cjs","kind":"requires","resolution":"internal","parse_state":"parsed"}
]

The same probe's resolve-only controls printed:

{"source":"main.mjs","node_resolve":"sub/dep.cjs"}
{"source":"control.mjs","node_resolve":"dep.cjs"}

The import returned parse_failures: [], exclusions: [], and empty limitations for every file/directory entry. Its one graph limitation concerned portable pathname traversal, not unknown loader resolution. Evidence provenance: observed production-boundary reproduction. Local verification: reproduced. Reproduction completeness: complete for this boundary. Source inspection below explains the output; no public command execution is claimed.

Typed outcome or recovery expected

For the unchanged fixture, preserve a dependency from main.mjs to sub/dep.cjs, or mark that relationship unresolved with the loader-base uncertainty. Preserve the control relationship to dep.cjs, the original file inventory, and truthful parse/coverage facts. Deleting the relationship without recording its unavailable interpretation would not preserve the same evidence contract.

Analysis or suspected cause

Source-derived: loader recognition accepts create-require; relationship extraction retains the importing source path and runtime-require strategy but no loader base. The application resolver then joins the specifier against dirname(fromPath), selecting the root-level sibling. No parser failure or excluded file is needed to trigger this wrong association.

Additional context

The supplied open-issue snapshot contained no report for this loader-base case. Related #4189 concerns package-main precedence with trailing-slash CommonJS specifiers; #4269 concerns percent-escaped ESM pathnames. This fixture has explicit .cjs filenames, no package metadata, and no escaped specifiers. Open and closed issues were searched before submission. No last-known-good version was established. An independent read-only source/evidence review found no blocking semantic objection and did not rerun the probe.

Triage assessment

  • Impact level: P2
  • Assessment status: supported
  • Impact basis: A valid bounded source tree produces an observed wrong parsed internal dependency; the demonstrated consequence is incorrect historical graph evidence, with no demonstrated severe downstream harm.
  • Workaround status: unverified
  • Workaround basis: Node's resolve-only control identifies the correct fixture target, but no general result-preserving REA workflow was tested for affected source trees. Editing their loader bases would change the analyzed program.
Originally created by @coygeek on GitHub (Oct 23, 2026). Original GitHub issue: https://github.com/morluto/rea/issues/6612 ## 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 Observed through the production `importReferenceSource` application boundary: an ES module creates a CommonJS loader with `createRequire(new URL('./sub/entry.cjs', import.meta.url))`, then calls `require('./dep.cjs')`. With both root-level `dep.cjs` and `sub/dep.cjs` inventoried, REA records a parsed internal dependency to root-level `dep.cjs`. Node's resolve-only control selects `sub/dep.cjs` for that same loader base. A separate `createRequire(import.meta.url)` control correctly selects root-level `dep.cjs` in both REA and Node. All fixture files were inventoried, with no parse failures, exclusions, or entry limitations. The graph's overall `inventory_state` is `partial` because it retains the general pathname-race limitation; this report does not claim a complete graph. CLI/MCP transport and downstream comparison behavior were not executed. ## Reproduction The following probe ran once with both the failing input and control. An earlier exploratory import independently showed the same wrong edge before this combined probe. The imported files are inert UTF-8 fixtures; they were never executed. The only Node loader operation is `createRequire(...).resolve(...)`, which resolves the filename without loading its module. `sub/entry.cjs` need not exist for Node's `createRequire` API to use that filename as its base. With the source checkout's locked dependencies installed, save this as `probe.mts` outside the checkout: ```typescript import { mkdir, writeFile } from 'node:fs/promises'; import { createRequire } from 'node:module'; import { join, relative, resolve } from 'node:path'; import { pathToFileURL } from 'node:url'; const repo = resolve(process.argv[2]); const root = resolve(process.argv[3]); await mkdir(join(root, 'sub'), { recursive: true }); const files = { 'main.mjs': `import { createRequire } from 'node:module'; const require = createRequire(new URL('./sub/entry.cjs', import.meta.url)); require('./dep.cjs'); `, 'control.mjs': `import { createRequire } from 'node:module'; const require = createRequire(import.meta.url); require('./dep.cjs'); `, 'dep.cjs': 'module.exports = "root";\n', 'sub/dep.cjs': 'module.exports = "sub";\n', }; for (const [path, text] of Object.entries(files)) await writeFile(join(root, path), text); const load = (path) => import(pathToFileURL(join(repo, 'src', path + '.ts')).href); const { importReferenceSource } = await load('application/ReferenceSourceImport'); const { ArtifactResourceScope } = await load('artifacts/ArtifactResourceScope'); const resources = new ArtifactResourceScope(); try { const result = await importReferenceSource({ root, caller: 'research-probe', policy: { secretPatterns: [] } }, resources); if (!result.ok) throw new Error(JSON.stringify(result.error)); const graph = result.value; console.log(JSON.stringify({ inventory_state: graph.inventory_state, entries: graph.entries.map(({ path, limitations }) => ({ path, limitations })), relationships: graph.relationships.filter(({ kind }) => kind === 'requires'), parse_failures: graph.parse_failures, exclusions: graph.exclusions, limitations: graph.limitations, })); for (const [source, base] of [['main.mjs', './sub/entry.cjs'], ['control.mjs', './control.mjs']]) { const loader = createRequire(new URL(base, pathToFileURL(join(root, source)))); console.log(JSON.stringify({ source, node_resolve: relative(root, loader.resolve('./dep.cjs')) })); } } finally { await resources.close(); } ``` Run with fresh fixture and configuration directories: ```sh repo="$PWD" work="$(mktemp -d)" mkdir -p "$work/home" "$work/config" "$work/cache" "$work/tmp" HOME="$work/home" XDG_CONFIG_HOME="$work/config" XDG_CACHE_HOME="$work/cache" TMPDIR="$work/tmp" \ node --import "$repo/node_modules/tsx/dist/loader.mjs" /path/to/probe.mts "$repo" "$work/fixtures" ``` This invokes the same production importer used by the public command, with `caller: "research-probe"` and an empty caller exclusion policy. It does not substitute parser, reader, or resolver implementations. No probe was run after this draft was saved. ## Expected behavior The importer should preserve a statically recoverable `createRequire` base when resolving that loader's relative dependency, or explicitly retain unresolved uncertainty if it cannot recover the base. It should not mark a different existing file as a parsed internal dependency. The executed Node resolution establishes `sub/dep.cjs` for the failing fixture, while the current-directory control establishes `dep.cjs`. REA already [recognizes `create-require` loaders](https://github.com/morluto/rea/blob/21b836a32710a575357715a798beadd0f3976b8a/src/domain/referenceSourceRequireBindings.ts#L42) and emits their calls as runtime-require relationships. The [historical-source documentation](https://github.com/morluto/rea/blob/21b836a32710a575357715a798beadd0f3976b8a/docs/cli.md#L341) describes the committed reference graph as input for comparison with the current artifact. This expectation does not require evaluating arbitrary dynamic loader bases or executing imported code. ## Impact and scope The bounded valid fixture records the wrong dependency despite containing both candidate files. This changes the historical graph supplied to subsequent analysis. No downstream comparison was tested, no source mutation occurred, and real-world frequency is unknown. A result-preserving workaround within REA was not established; changing the fixture's loader base would change the program being analyzed. ## REA version REA 6.7.0, source [baseline](https://github.com/morluto/rea/tree/21b836a32710a575357715a798beadd0f3976b8a), with locked dependencies. The direct importer call leaves its optional `importer_version` metadata unset; the baseline identifies the code actually imported. ## Runtime and platform Node.js v26.12.1, Linux x64, using the repository's installed `tsx` loader. No executable target, source fixture module, or external analysis provider was started. ## Target and artifact context Four authored files: `main.mjs`, `control.mjs`, root-level `dep.cjs`, and `sub/dep.cjs`. Both dependency files contain distinct string exports. The source tree has no Git repository, ignore file, package manifest, symlink, or excluded entry. The two loader bases are the only behavioral difference between the failing module and control. ## Interface Other ## Earliest failure stage Native analysis or reconstruction ## Relevant output or evidence The executed production import returned these requires relationships: ```json [ {"from_path":"control.mjs","to":"dep.cjs","kind":"requires","resolution":"internal","parse_state":"parsed"}, {"from_path":"main.mjs","to":"dep.cjs","kind":"requires","resolution":"internal","parse_state":"parsed"} ] ``` The same probe's resolve-only controls printed: ```json {"source":"main.mjs","node_resolve":"sub/dep.cjs"} {"source":"control.mjs","node_resolve":"dep.cjs"} ``` The import returned `parse_failures: []`, `exclusions: []`, and empty limitations for every file/directory entry. Its one graph limitation concerned portable pathname traversal, not unknown loader resolution. Evidence provenance: observed production-boundary reproduction. Local verification: reproduced. Reproduction completeness: complete for this boundary. Source inspection below explains the output; no public command execution is claimed. ## Typed outcome or recovery expected For the unchanged fixture, preserve a dependency from `main.mjs` to `sub/dep.cjs`, or mark that relationship unresolved with the loader-base uncertainty. Preserve the control relationship to `dep.cjs`, the original file inventory, and truthful parse/coverage facts. Deleting the relationship without recording its unavailable interpretation would not preserve the same evidence contract. ## Analysis or suspected cause Source-derived: [loader recognition](https://github.com/morluto/rea/blob/21b836a32710a575357715a798beadd0f3976b8a/src/domain/referenceSourceRequireBindings.ts#L49) accepts `create-require`; [relationship extraction](https://github.com/morluto/rea/blob/21b836a32710a575357715a798beadd0f3976b8a/src/domain/referenceSourceImportParsing.ts#L265) retains the importing source path and runtime-require strategy but no loader base. The [application resolver](https://github.com/morluto/rea/blob/21b836a32710a575357715a798beadd0f3976b8a/src/application/ReferenceSourceImportEntries.ts#L277) then joins the specifier against `dirname(fromPath)`, selecting the root-level sibling. No parser failure or excluded file is needed to trigger this wrong association. ## Additional context The supplied open-issue snapshot contained no report for this loader-base case. Related [#4189](https://github.com/morluto/rea/issues/4189) concerns package-main precedence with trailing-slash CommonJS specifiers; [#4269](https://github.com/morluto/rea/issues/4269) concerns percent-escaped ESM pathnames. This fixture has explicit `.cjs` filenames, no package metadata, and no escaped specifiers. Open and closed issues were searched before submission. No last-known-good version was established. An independent read-only source/evidence review found no blocking semantic objection and did not rerun the probe. ## Triage assessment - Impact level: P2 - Assessment status: supported - Impact basis: A valid bounded source tree produces an observed wrong parsed internal dependency; the demonstrated consequence is incorrect historical graph evidence, with no demonstrated severe downstream harm. - Workaround status: unverified - Workaround basis: Node's resolve-only control identifies the correct fixture target, but no general result-preserving REA workflow was tested for affected source trees. Editing their loader bases would change the analyzed program.
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#3997
No description provided.