Skip to content

fix(sync): an import links an added file whose name a popular import shares - #2666

Merged
colbymchenry merged 3 commits into
mainfrom
claude/friendly-pare-eb37c3
Oct 19, 2026
Merged

colbymchenry merged 3 commits into
mainfrom
claude/friendly-pare-eb37c3

Conversation

@colbymchenry

@colbymchenry colbymchenry commented Oct 19, 2026 •

Copy link
Copy Markdown
Owner

Problem

Sync retries a failed import by what an added file can be imported as (its name, its stem, its folder), and skips a key that more than 500 failed imports share, as noise one file cannot answer (#2392). A path import is parked under its path's stem, so a key holds a project's own imports and every package import that ends the same way:

  • folo: utils is the stem of jotai/utils, @electron-toolkit/utils, folo's own @follow/utils[/utils] workspace specifiers (which codegraph can't follow: it ignores a workspace package's exports map) and of ./utils / ~/modules/settings/utils. A fresh index on current main already parks 485 failed imports there, and hooks holds 451. Delete and restore apps/desktop/layer/renderer/src/modules/settings/utils.ts and its 21 imports push utils past 500, so the retry skips the key and none of them relink. Hold out every 7th source file instead, and it's hooks that tips over (539): 88 imports of ~/components/ui/modal/stacked/hooks and its neighbours stay unlinked.
  • bitwarden: in the same hold-out scenario, 2,352 lines of the synced graph differ from a fresh index, most of them libs/common/src/auth/abstractions/account.service.ts's 724 importers and every binding and reference that hangs off them.
  • vscode: assert holds 5,415 failed imports by tail (the assert package's module references and every binding of import * as assert from 'assert'). Delete and restore src/vs/base/common/assert.ts and its importers stay unlinked.

Fix

A path key past the ceiling is now looked through for the imports that could name an added file, and those are retried (getRetryableFailedImports' new couldName, built by ReferenceResolver.couldNameAnyOf(addedFiles)):

  • A JavaScript/TypeScript import (incl. Vue, Svelte, Astro) is accepted when its module resolves to an added file — the path branch of resolveViaImport, now shared with it as jsModuleFile so the two can't drift — when a framework's alias resolves it into one, or when its last segment is an added file's name (the file-name strategy).
  • What it accepts is no noise, so it is held to the module ceiling (5000), as main's resolvedModules lookup is. bitwarden needs this: 724 files import libs/common/src/auth/abstractions/account.service.ts, all of them through specifiers that resolve to it.
  • An import written in any other language is not accepted, so a crowded key there is skipped, exactly as before.
  • A key within the ceiling is retried whole, as before. The names (namespace) and resolvedModules lookups are untouched.

Since a crowded key used to be skipped entirely, this can only add retries: a row the test rejects was not retried before either.

Why not park package imports elsewhere (the other idea in the brief)

I measured that split on current main's fresh indexes (package = isOutOfRepoImport), and it doesn't reach most of the noise:

  • folo utils (485): 40 rows are package imports, and 445 are folo's own @follow/utils and @follow/utils/utils workspace specifiers, so the key would sit at 445 and pass 500 again with a slightly different change set.
  • folo hooks (451): not one row is a package import. This is the key that actually tipped over in the hold-out scenario.
  • vscode assert (5,415): 2,692 rows are package module references, but 2,723 are bindings, which would keep it crowded.

Re-keying would also change name_tail in every fresh index (the dead-code report reads it as "maybe referenced"). And a package specifier with a file-like last segment (chart.js) is linked by file name in a fresh index, so moving it would be a convergence risk. The lookup-time test needs no schema change and works on existing indexes.

Validation

Arms: main = 0d33925, fix = this branch; both with the same native kernel built from main's source. Kit: D:\cgwork\cgceil.

Re-run on a6cfb52 (after merging #2664/#2665 into the branch): the folo and bitwarden fresh controls are still byte-identical, and the folo/bitwarden hold-out and folo delete-and-restore numbers below are reproduced exactly, line for line.

Regression test — __tests__/sync-import-retry-crowded-key.test.ts, three cases, each comparing the synced graph's file-to-file imports with a fresh index of the final tree in a second folder: a project's utils.ts among 520 kitN/utils package imports (folo's shape), assert.ts among 520 import * as assert from 'assert' module refs and bindings (vscode's shape), and a utils.ts that 510 files import (bitwarden's shape). All three fail on main and pass here. sync-import-retry.test.ts gains a getRetryableFailedImports unit test for the crowded-key pass, and its query-plan test now also covers the new per-key read (it stays on idx_unresolved_failed_import_tail/_name).

Fresh indexes — controls, byte-identical (scripts/dump-graph.mjs, cmp): folo, vben, milo-ui, ghostfolio, infrahub, excalidraw, bitwarden, gin (Go), django-realworld (Python) — 9/9 identical. The fix only acts in sync, and the jsModuleFile extraction from resolveViaImport changes nothing.

Late scenario (every 7th source file held out, the rest indexed, the files restored, codegraph sync; each arm's synced dump against its own fresh dump, which are identical across arms):

repo fresh-only lines, main → fix recovered lost
folo 91 → 3 88 (~/components/ui/modal/stacked/hooks ×69, ./hooks, …: the hooks key reached 539) 0
bitwarden 2,352 → 65 2,287 (@bitwarden/common/auth/abstractions/account.service ×679 and the 1,600 binding/reference edges that follow it, @bitwarden/auth/common ×101, …) 0
milo-ui, ghostfolio, infrahub, gin 0 → 0 — 0
excalidraw 5 → 5 — 0
vben 179 → 179 — 0

Delete and restore the file from the brief, folo's apps/desktop/layer/renderer/src/modules/settings/utils.ts, then compare with the fresh index. On current main, utils holds 485 failed imports in a fresh index, and the file's 21 imports push it past 500, so main leaves all 21 unlinked: 14 through ~/modules/settings/utils, 7 through ./utils or ../utils. The fix leaves none.

vscode, delete and restore src/vs/base/common/assert.ts (68 files import it): fresh dumps byte-identical across arms. Main's restore sync retries 28 refs and leaves 393 fresh-only lines. The fix retries 84 and leaves 317, recovering 76 lines and losing 0: 56 module imports of …/base/common/assert.js, plus 9 assert bindings and 9 assert(…) calls that follow them. Every remaining imports line (127) was re-pointed while the file was gone, not left failed, so no retry sees it, on either arm:

  • The file-name strategy took …/base/common/assert.js to test/unit/assert.js, the only other assert.js.
  • Exact-match took assert to another assert function.
  • The 190 calls are the same, or sit under the symbol retry's own ceiling.

That's an existing re-point gap (an import a sync should move to the file that outranks its current target), not this change's.

Cost and plans (vscode, the restored assert.ts; assert holds 5,415 failed imports by tail and 5,405 by name, the two crowded lookups):

  • Without the test, the lookup for the file's keys takes 0.7 ms, because it skips assert.
  • With it, each crowded key's rows are read (16-23 ms per 5.4K rows) and tested. A whole cold lookup took at most 220 ms, most of it the resolver's one-time workspace and alias load, which a sync has already done by the time it retries. Warm, the median is 26 ms.
  • The test's order is chosen for cost. It runs the file-name check, then the memoized path resolution, which a package never passes. Only then comes the module-reference check that reads a file's imports, and the framework resolvers, which read each importing file. Those run only for path-like specifiers, since an alias is a path and a binding never is. A first version that asked those first took about 0.9 s cold on the same key.
  • EXPLAIN QUERY PLAN for the per-key read: SEARCH unresolved_refs USING INDEX idx_unresolved_failed_import_tail (reference_kind=? AND name_tail=?), and idx_unresolved_failed_import_name for the by-name lookup, the same partial indexes as the existing count queries. The query-plan test in sync-import-retry.test.ts now covers it.

Overlap

None open. Builds on #2392 (the import retry), #2450 (module: keys) and the resolvedModules/folder-import lookups on main.

🤖 Generated with Claude Code

colbymchenry and others added 3 commits October 19, 2026 11:12
…shares

Sync retries a failed import by what an added file can be imported as (its
name, its stem, its folder) and skips a key more than 500 failed imports
share, as noise one file cannot answer. A path import is parked under its
path's stem, though, and a package's imports share that stem with every file
of the name: `utils` is the stem of every `jotai/utils` and `@scope/pkg/utils`
as much as of a project's own `./utils`, and `assert` the name of every binding
of `import * as assert from 'assert'`. In a large project those crowded the key
past the ceiling, so a project's own `utils.ts` added after its importers were
indexed, or restored after a delete, never linked them: folo's
`modules/settings/utils.ts` and `components/ui/modal/stacked/hooks.ts`, and
vscode's `base/common/assert.ts`.

A key past the ceiling is now looked through for the imports that could name
an added file, and those are retried. A JavaScript or TypeScript import names
a file by its module (the path branch of resolveViaImport, shared with it as
jsModuleFile), by a framework alias, or by the file's own name. What that
accepts is no noise, so it is held to the module ceiling, as the specifiers
that resolve to an added file already are: bitwarden imports its
account.service from 724 files. An import in another language is not
accepted, so a crowded key there is skipped as before; a key within the
ceiling is retried whole, as it was.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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