fix(agent): pin the tooling lint's import resolver inside the repo - #463
Merged
Merged
Conversation
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The bug
bun run agent:lintfails with ~214Resolve error: node with invalid interface loaded as resolveron a clean checkout. Reported by a first-time user of the stack; reproduced locally onmain.Cause
tools/has nonode_modules, and neither does the repo root.apps/api/eslint.config.jsnever setssettings["import/resolver"](calculateConfigForFilereturnssettingsasundefined), so eslint-plugin-import falls back to its default resolver, named by the bare string"node".eslint-module-utilsresolves it withcreateRequire(sourceFile), wheresourceFileistools/agent/*.ts— Node resolution walks up past the repo entirely:Bun's global install cache. The copy
apps/api/bun.lockpins —0.3.10— is never reached.Why now
No commit here caused it:
tools/eslint.config.mjslanded in feat(agent): add verified workflows, resource tooling and reliable push gates #443 on 2026-09-11 and was never modified since.eslint-plugin-importpin inapps/apiis unchanged since Release 1.0.eslint-import-resolver-node@0.4.0entered the global cache on 2026-09-14 — three days later.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 whenisResolverValidruns'interfaceVersion' in resolver. Merely instrumenting the check withObject.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 unrelatedreact/zodbump inapps/uiflips 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.mjsalready reaches foreslint/lib/api.jsand.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/resolverkey must be an absolute path that exists inside the repo. Deterministic regardless of cache state. It fails closed if thesettingsblock is dropped entirely, and the test also probes thatimport/no-useless-path-segmentsgenuinely 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 keeptools/without its own dependency root — a second root reintroduces exactly the cross-root version drift the@typescript-eslint/utilsnote inapps/api/package.jsonexists to prevent.Verification
bun run agent:quality— green (typecheck, lint, format)bun test tools/agent— 88 pass, 0 failbun run check— green, with the pre-existing OpenAPI-drift skip that needs a running API (unrelated, matches 0691b40)node:name