Skip to content

fix(javascript): preserve CommonJS lookup and analysis provenance - #2306

Merged
morluto merged 14 commits into
morluto:mainfrom
rohitg00:fix/commonjs-loader-resolution
Oct 13, 2026
Merged

morluto merged 14 commits into
morluto:mainfrom
rohitg00:fix/commonjs-loader-resolution

Conversation

@rohitg00

@rohitg00 rohitg00 commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Preserve the caller's CommonJS lookup intent and requested resource limits throughout JavaScript application analysis. Static graph edges now agree with Node's loader for directory requests and dependency ancestors, and completed or interrupted analysis retains the requested limits in Evidence.

Problem and expected behavior

With both dep.js and dep/index.js present, require("./dep/.") previously selected the file after path normalization erased directory intent. Node selects dep/index.js. CommonJS analysis could also select a decoy under node_modules/node_modules, an ancestor position Node skips for this loader. ESM has a different search order and must keep it.

Separately, max_heap_mb and analysis_timeout_ms reached the analysis worker but disappeared from the Evidence parameter projection. Retained and exported results must preserve supplied settings, including partial results after timeout, while leaving omitted settings absent.

These are directly reproduced correctness defects; there is no separate tracking issue.

Change and scope

Keep directory intent at the shared path-resolution owner and use a loader-specific ancestor iterator. Apply the directory guard to ordinary file and TypeScript source candidates. Preserve existing package-main, container, confinement, and unresolved-result behavior.

Project both optional resource limits at the shared Evidence owner and forward the validated request directly through the service and partial-result path, removing redundant option reconstruction. The existing Apple fixture correction gives Swift compilation a bounded timeout while retaining all assertions.

Contract and boundary impact

  • Semantic owner and earliest changed stage: JavaScript artifact candidate selection and application Evidence parameter projection.
  • Contribution scope and maintainer decision: ordinary fixes within existing offline analysis; a new scope decision is not applicable.
  • CLI and MCP/tool catalog: input schemas unchanged; both graph projections receive corrected module paths and Evidence retains supplied limits.
  • Provider/platform compatibility: CommonJS and ESM checked independently against Node. Resource defaults and enforcement are unchanged.
  • Evidence/provenance: raw requests, omission semantics, retained parent Evidence, partial results, and authenticated bundle round trips remain preserved. Recorded limits do not independently prove enforcement.
  • Target authority and cleanup: the existing owned static-analysis worker is used; analyzed application source is not executed. No new network, mutation, or cleanup behavior.
  • Generated metadata, package, installation impact: none.

Expected effects:

  • Read existing files or captured data.
  • Launch or attach to a process.

Process launch is limited to the existing analysis worker. Native Node resolution controls and Swift fixture generation are tests against owned fixtures.

Evidence and regression coverage

Four compiled CLI/stdio MCP cases compare both graph projections with native Node resolution. They cover trailing slash/dot/double-dot requests, scoped and nested dependencies, missing directories, nearest dependency precedence, package-main behavior, a TypeScript decoy, and the differing ESM target.

Six public provenance cases cover both limits, either limit, omitted limits despite ambient NODE_OPTIONS, retained parent Evidence, export/import, and an actual one-millisecond timeout followed by a successful MCP ping. Existing memory-pressure and cancellation regressions also pass.

Representative output is resolved_path: "dep/index.js" for ./dep/.. An analysis requested with both controls retains {"format":"auto","integrity_policy":"fail","max_heap_mb":128,"analysis_timeout_ms":30000} in Evidence. The timeout case retains analysis_timeout_ms: 1 in partial Evidence.

The original CommonJS public regressions and requested-limit projection regression failed before their respective fixes.

  • Observed, derived, and inferred claims remain distinguishable.
  • Artifact identity, provenance, and failed attempts remain preserved.
  • Incomplete, unavailable, and uncertain outcomes remain visible.

Validation performed

Pinned Node 24.18.0/npm 11.16.0 on macOS arm64, against upstream fb28ec1:

  • npm run test:focused -- src/application/javascript/JavaScriptArtifactPathResolution.test.ts tests/acceptance/applications/javascriptCommonJsDirectoryLookup.test.ts tests/acceptance/applications/javascriptAnalysisLimits.test.ts tests/boundary/mcp/javascriptAnalysisResources.test.ts tests/boundary/mcp/javascriptAnalysisCancellation.test.ts tests/boundary/filesystem/interfaceBuilderBundle.test.ts: build passed; 190 tests passed across six files, with one explicit Xcode ibtool prerequisite skip.
  • npm run check:ci: all eight tasks and architecture guards passed; duplication 2.98%.
  • npm run docs:check: passed, including two README consistency tests.
  • npm run verify:test-discovery: all 1,058 files discovered exactly once.
  • git diff --check: passed.

Compatibility, safety, and release

No breaking changes, migration, dependencies, or installation changes. Actual compiled CLI, stdio MCP, isolated Node worker, bundle persistence, and Swift Foundation fixtures ran locally. The Xcode-compiled storyboard case was unavailable because ibtool was not configured. Windows and Bun resource behavior were not exercised locally. Local verification does not establish hosted CI success.

Before merge

  • Concrete problem, focused outcome, and issue-link rationale stated.
  • Scope and authority resolved.
  • Tests cover public behavior, omission, partial failure, and persistence.
  • Contracts and generated metadata reviewed; no updates required.
  • Representative output and remaining platform limits described.
  • Final diff reviewed for secrets, unrelated changes, and unsupported claims.

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits.
Repo admins can enable using credits for code reviews in their settings.

@CassieFox23

Copy link
Copy Markdown
Contributor

TL;DR: With this PR, REA picks the same file as Node 22, 24 and 26 for require("./dep/.") and for require("shared") inside node_modules/client. ESM lookup did not change. One gap remains, and it was there before the PR: a package.json main that ends in a slash.

Results

I checked 135 requests against real Node 22, 24 and 26, which all agreed. REA ran on the commit this PR sits on (aeee507) and on the PR head (56a0aee).

Request Node 22, 24, 26 Before the PR With the PR
require("./dep/."), with dep.js and dep/index.js dep/index.js dep.js dep/index.js
require("./dep/") dep/index.js dep/index.js dep/index.js
require("./missing/."), with only missing.js not found missing.js not found
require("shared") from node_modules/client/main.cjs node_modules/shared/index.js node_modules/node_modules/shared/index.js node_modules/shared/index.js
import "shared" from node_modules/client/main.mjs node_modules/node_modules/shared/index.js node_modules/node_modules/shared/index.js node_modules/node_modules/shared/index.js
  • REA matched Node on 101 of 135 requests before the PR and 132 with it. All 31 changes moved toward Node. ESM and import() results did not change.
  • Plain trailing slashes like ./dep/ already worked. The PR fixes /., /.., ., .. and the node_modules skip.
  • The PR's 4 tests pass on Node 22, 24 and 26. On the tree before it, all 4 fail on Node 24. Four related test files, 144 tests, pass on Node 24.
  • The nine fixtures from issues 2020 to 2024 give byte-identical output before and after.

Still different, before and after

If package.json says "main": "./lib/" or "main": "lib/", and both lib.js and lib/index.js exist, Node picks lib.js. REA picks lib/index.js. This is outside the PR's claim.

Not run

Thanks, rohitg00, for the fix, and for tests that check the answers against real Node.

Counts by group
Group Requests Match Node before Match Node with the PR
require, directory forms 57 39 54
require, node_modules ancestors 28 12 28
ESM import 44 44 44
import() inside a .cjs file 6 6 6
Total 135 101 132
All 31 changed results

[at] stands for the at sign in a scoped package name. I left the sign out so GitHub tags nobody.

Source file Request Node 22, 24, 26 Before the PR With the PR
main.cjs ./dep/. dep/index.js dep.js dep/index.js
main.cjs ./parent/child/.. parent/index.js parent.js parent/index.js
main.cjs ./missing/. not found missing.js not found
main.cjs directory/. node_modules/directory/index.js node_modules/directory.js node_modules/directory/index.js
main.cjs package/dep/. node_modules/package/dep/index.js node_modules/package/dep.js node_modules/package/dep/index.js
parent/child/main.cjs . parent/child/index.js parent/child.js parent/child/index.js
parent/child/main.cjs .. parent/index.js parent.js parent/index.js
main.cjs ./dep/./. dep/index.js dep.js dep/index.js
main.cjs ./dep/../dep/. dep/index.js dep.js dep/index.js
main.cjs ./onlyfile/. not found onlyfile.js not found
main.cjs ./dep.js/. not found dep.js not found
parent/child/main.cjs ../child/. parent/child/index.js parent/child.js parent/child/index.js
parent/child/main.cjs ../../dep/. dep/index.js dep.js dep/index.js
main.cjs ./pkgdir/. pkgdir/lib.js pkgdir.js pkgdir/lib.js
main.cjs [at]scope/pkg/sub/. node_modules/[at]scope/pkg/sub/index.js node_modules/[at]scope/pkg/sub.js node_modules/[at]scope/pkg/sub/index.js
node_modules/client/main.cjs shared node_modules/shared/index.js node_modules/node_modules/shared/index.js node_modules/shared/index.js
node_modules/client/main.cjs [at]scope/shared node_modules/[at]scope/shared/index.js node_modules/node_modules/[at]scope/shared/index.js node_modules/[at]scope/shared/index.js
node_modules/[at]scope/client/main.cjs shared node_modules/shared/index.js node_modules/node_modules/shared/index.js node_modules/shared/index.js
node_modules/parent/node_modules/client/main.cjs shared node_modules/parent/node_modules/shared/index.js node_modules/parent/node_modules/node_modules/shared/index.js node_modules/parent/node_modules/shared/index.js
node_modules/client/main.cjs onlydecoy not found node_modules/node_modules/onlydecoy/index.js not found
node_modules/loose.cjs shared node_modules/shared/index.js node_modules/node_modules/shared/index.js node_modules/shared/index.js
node_modules/node_modules/inner/main.cjs shared node_modules/shared/index.js node_modules/node_modules/shared/index.js node_modules/shared/index.js
node_modules/node_modules/inner/main.cjs onlydecoy not found node_modules/node_modules/onlydecoy/index.js not found
node_modules/[at]scope/client/main.cjs [at]scope/shared node_modules/[at]scope/shared/index.js node_modules/node_modules/[at]scope/shared/index.js node_modules/[at]scope/shared/index.js
node_modules/[at]scope/client/node_modules/[at]scope/inner/main.cjs shared node_modules/shared/index.js node_modules/node_modules/shared/index.js node_modules/shared/index.js
node_modules/a/main.cjs l0 node_modules/l0/index.js node_modules/node_modules/l0/index.js node_modules/l0/index.js
node_modules/a/node_modules/b/main.cjs l0 node_modules/l0/index.js node_modules/node_modules/l0/index.js node_modules/l0/index.js
node_modules/a/node_modules/b/main.cjs l1 node_modules/a/node_modules/l1/index.js node_modules/a/node_modules/node_modules/l1/index.js node_modules/a/node_modules/l1/index.js
node_modules/a/node_modules/b/node_modules/c/main.cjs l0 node_modules/l0/index.js node_modules/node_modules/l0/index.js node_modules/l0/index.js
node_modules/a/node_modules/b/node_modules/c/main.cjs l1 node_modules/a/node_modules/l1/index.js node_modules/a/node_modules/node_modules/l1/index.js node_modules/a/node_modules/l1/index.js
node_modules/a/node_modules/b/node_modules/c/main.cjs l2 node_modules/a/node_modules/b/node_modules/l2/index.js node_modules/a/node_modules/b/node_modules/node_modules/l2/index.js node_modules/a/node_modules/b/node_modules/l2/index.js
The 3 results that differ from Node on both trees
Source file Request Node 22, 24, 26 Before the PR With the PR
main.cjs ./maindir2/ maindir2/lib.js maindir2/lib/index.js maindir2/lib/index.js
main.cjs ./maindir4/ maindir4/lib.js maindir4/lib/index.js maindir4/lib/index.js
main.cjs mainslash node_modules/mainslash/lib.js node_modules/mainslash/lib/index.js node_modules/mainslash/lib/index.js
Test setup
Commands

How I got the Node answers. CommonJS requests use createRequire(<source file>).resolve(request). ESM and import() requests use a real import() of a virtual module at the source file's path. A module.registerHooks resolve hook records Node's own answer. The PR's test does a similar check with import.meta.resolve. It agrees with mine for the shared case.

node <tree>/scripts/rea.mjs analyze-javascript-application <fixture> --format json
env -C <head tree> npm run test:focused -- tests/acceptance/applications/javascriptCommonJsDirectoryLookup.test.ts

Both trees came from git archive, then npm ci and npm run build. The head tree also ran npm run test:prepare. Each Node ran the test with its own bin folder first on PATH. I copied the test file into the base tree for the "before" run. Every command exited 0, except the base test run. It exited 1 with 4 failures, as expected.

🤖 Generated with Claude Code (Sonnet 5.5, verified by Opus 5.5) 🦊

@morluto

morluto commented Oct 13, 2026

Copy link
Copy Markdown
Owner

@greptileai review

@greptile-apps

greptile-apps Bot commented Oct 13, 2026

Copy link
Copy Markdown
Contributor

This PR is set for a Plus review. TREX isn't supported with Plus or Apex reviews yet, so Greptile reviews this PR at Base while TREX is on.

@greptile-apps

greptile-apps Bot commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerConfidence Score: 5/5

[Medium impact] The PR appears safe to merge based on the available review evidence.

Summary

This PR corrects CommonJS directory and dependency lookup and preserves requested analysis limits in Evidence.

  • CommonJS graph edges follow directory and package lookup rules.
  • JavaScript analysis Evidence keeps the heap and timeout limits callers request.
  • The Swift archive fixture gets bounded compile and test waits.

Reviews (8) · Last reviewed commit: "chore(ci): retain contributor updates al..." · Reviewed by Greptile

@rohitg00 rohitg00 changed the title fix(javascript): preserve CommonJS directory and ancestor lookup fix(javascript): preserve CommonJS lookup and analysis provenance Oct 13, 2026
@rohitg00

Copy link
Copy Markdown
Contributor Author

@morluto Would like you to consider this for new release as well.

@morluto morluto added bug Something isn't working area: domain Domain logic, errors, and pure functions P2: medium Medium priority issue labels Oct 13, 2026
@morluto
morluto merged commit d005bd5 into morluto:main Oct 13, 2026
34 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: domain Domain logic, errors, and pure functions bug Something isn't working P2: medium Medium priority issue

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants