Skip to content

fix(agent): pin the tooling lint's import resolver inside the repo - #463

Merged
agjs merged 1 commit into
mainfrom
fix/tools-eslint-resolver-pin
Sep 20, 2026
Merged

agjs merged 1 commit into
mainfrom
fix/tools-eslint-resolver-pin

Conversation

@agjs

@agjs agjs commented Sep 20, 2026

Copy link
Copy Markdown
Contributor

The bug

bun run agent:lint fails with ~214 Resolve error: node with invalid interface loaded as resolver on a clean checkout. Reported by a first-time user of the stack; reproduced locally on main.

Cause

tools/ has no node_modules, and neither does the repo root. apps/api/eslint.config.js never sets settings["import/resolver"] (calculateConfigForFile returns settings as undefined), so eslint-plugin-import falls back to its default resolver, named by the bare string "node". eslint-module-utils resolves it with createRequire(sourceFile), where sourceFile is tools/agent/*.ts — Node resolution walks up past the repo entirely:

from tools/agent -> ~/.bun/install/cache/eslint-import-resolver-node@0.4.0@@@1/index.js

Bun's global install cache. The copy apps/api/bun.lock pins — 0.3.10 — is never reached.

Why now

No commit here caused it:

The config was always structurally wrong; it became a failure when 0.4.0 shipped upstream. Loaded from the cache it comes back as an ESM namespace ([Object: null prototype]) whose bindings are not materialized when isResolverValid runs 'interfaceVersion' in resolver. Merely instrumenting the check with Object.keys(resolver) made the lint pass, and rewriting the file (invalidating Bun's transpile cache) flipped it back to green — so pass/fail hinges on ambient cache state. That is why it shows up in CI, why it was not reproducible locally, and why an unrelated react/zod bump in apps/ui flips it: the cache key changes and the 0.4.0 entry gets restored or not.

I could not re-trigger the failure once the cache state was disturbed, so the lazy-namespace mechanism is inference from the instrumentation evidence rather than something I could toggle on demand. The resolution target is directly observable and is the actual defect.

The fix

Point the resolver at the installed file by absolute path so the lockfile decides. This matches how tools/eslint.config.mjs already reaches for eslint/lib/api.js and .prettierrc.json — the resolver was the one dependency reached by bare specifier, and that was the whole hole.

The guard

Worth calling out: the first guard I wrote asserted the symptom (no "Resolve error" in lint output). Checked against the unfixed config, it passed anyway, because this machine's cache currently resolves fine. A gate that only fires when the ambient cache happens to be bad gates nothing.

Replaced with a structural assertion: every declared import/resolver key must be an absolute path that exists inside the repo. Deterministic regardless of cache state. It fails closed if the settings block is dropped entirely, and the test also probes that import/no-useless-path-segments genuinely fires, so the rules are resolving rather than silently no-op.

Invariant

tools/ may borrow the API's installation, but only by absolute path, never by bare specifier. I'd keep tools/ without its own dependency root — a second root reintroduces exactly the cross-root version drift the @typescript-eslint/utils note in apps/api/package.json exists to prevent.

Verification

  • bun run agent:quality — green (typecheck, lint, format)
  • bun test tools/agent — 88 pass, 0 fail
  • bun run check — green, with the pre-existing OpenAPI-drift skip that needs a running API (unrelated, matches 0691b40)
  • Guard verified both directions: passes with the fix, fails deterministically when reverted to the bare node: name

tools/ has no node_modules, so eslint-plugin-import resolved its default
resolver by the bare name "node" from the linted file's directory, found
no dependency root, and escaped to Bun's global install cache. The copy
apps/api/bun.lock pins (eslint-import-resolver-node 0.3.10) was never
reached.

That stayed harmless until 0.4.0 was published and landed in the global
cache, at which point the lookup started finding a module whose bindings
are not materialized when eslint-module-utils validates the resolver
interface. Every import rule then reports "Resolve error" instead of
running: 214 errors on a clean checkout. Because the failure depends on
what the ambient cache happens to hold, any unrelated dependency bump
flips the CI cache key and flips the lint with it.

Point the resolver at the installed file by absolute path so the lockfile
decides, matching how this config already reaches for eslint/lib/api.js
and .prettierrc.json.

Guard it structurally rather than by symptom: assert every declared
import/resolver key is an absolute path inside the repo. A test that only
checked lint output for "Resolve error" passed against the unfixed config
on a machine whose cache happened to resolve, so it gated nothing.
@agjs
agjs merged commit 8f602da into main Sep 20, 2026
29 checks passed
@agjs
agjs deleted the fix/tools-eslint-resolver-pin branch September 20, 2026 20:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant