fix(resolution): stop "Resolving refs" wedge on theme-vendoring repos; add exclude config + index watchdogs (#999) (#1009)

Three fixes for a repo that commits a large JS/TS theme/SDK (Metronic under
static/, ~1,600 tracked files):

1. A SECOND "Resolving refs" quadratic that #915 didn't cover. #915 capped
   import-name collisions; this caps method-name collisions (init/update/render
   re-declared on every widget), which flow through matchMethodCall Strategy 3
   and findBestMatch instead. New AMBIGUOUS_NAME_CEILING (default 500, env
   CODEGRAPH_AMBIGUOUS_NAME_CEILING): above it the fuzzy strategies decline
   rather than score K candidates — no proximity score can pick the one true
   target among thousands anyway. Resolving drops from O(K^2) to linear in refs
   (e.g. 900-file synthetic: 28.7s -> 3.4s), edge counts unchanged, and the cap
   never fires on normal repos (max real method-collision ~40).

2. A new `exclude` array in codegraph.json keeps git-TRACKED paths out of the
   index, which .gitignore can't do (enumeration is `git ls-files`). Mirrors the
   existing includeIgnored plumbing across the git, sync, and non-git-walk
   paths.

3. `index`/`init` now install the #850 liveness + #277 ppid watchdogs (which
   were serve-only), so a wedged or orphaned indexer self-terminates instead of
   pinning a core. The --liftoff-only relaunch's spawnSync can't forward
   signals, so killing the parent shim used to orphan the worker.

Tests: ubiquitous-name ceiling, exclude (incl. tracked-file exclusion on git +
non-git), orphan self-termination (POSIX), and ppid-parser units. Shared the
ppid parsers out of mcp/index.ts into mcp/ppid-watchdog.ts.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Colby Mchenry
2026-06-26 20:25:47 -05:00
committed by GitHub
co-authored by Claude Opus 4.8
parent d3179f5004
commit 45d3293c6a
12 changed files with 805 additions and 100 deletions
+45
View File
@@ -7,6 +7,33 @@
import { Node } from '../types';
import { UnresolvedRef, ResolvedRef, ResolutionContext } from './types';
/**
* Ceiling on how many same-named definitions a FUZZY name-match strategy will
* score. A name defined more times than this is "ubiquitous" — a method/symbol
* re-declared across a vendored theme or SDK (e.g. `init`/`update`/`render` on
* every widget of a committed Metronic theme — #999). No directory-proximity or
* receiver-word-overlap score can reliably pick THE one true target among
* thousands, so the fuzzy strategies (matchByExactName's findBestMatch, and
* matchMethodCall Strategy 3) decline above the ceiling instead of emitting a
* low-confidence, almost-certainly-wrong edge. This also caps their per-ref cost
* at O(ceiling): without it, K same-named refs each scored K candidates — the
* O(K²) blow-up that pinned a core for 15-28 min at "Resolving refs … 94%" on a
* repo vendoring a large JS/TS theme (#999). The PRECISE strategies are
* unaffected: qualified-name, import-based, and class-name (Strategy 1/2)
* resolution all still run and resolve a ubiquitous name when the context names
* its exact target. Real repos top out near ~40 same-named methods, so a normal
* codebase never reaches this; only bulk-vendored code does. Tune via
* `CODEGRAPH_AMBIGUOUS_NAME_CEILING`.
*/
const DEFAULT_AMBIGUOUS_NAME_CEILING = 500;
function resolveAmbiguousNameCeiling(): number {
const raw = process.env.CODEGRAPH_AMBIGUOUS_NAME_CEILING;
if (!raw) return DEFAULT_AMBIGUOUS_NAME_CEILING;
const parsed = Number.parseInt(raw, 10);
return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_AMBIGUOUS_NAME_CEILING;
}
const AMBIGUOUS_NAME_CEILING = resolveAmbiguousNameCeiling();
/**
* Try to resolve a path-like reference (e.g., "snippets/drawer-menu.liquid")
* by matching the filename against file nodes.
@@ -344,6 +371,15 @@ export function matchByExactName(
};
}
// Ubiquitous-name ceiling (#999): above it, picking one target among K
// same-named defs by directory proximity is unreliable AND O(K) per ref — the
// quadratic behind the "Resolving refs" wedge on theme/SDK-vendoring repos.
// Decline; the precise strategies (qualified-name, import, class-name) already
// ran. Falls through to fuzzy, which itself only resolves a UNIQUE candidate.
if (candidates.length > AMBIGUOUS_NAME_CEILING) {
return null;
}
// Multiple matches - try to narrow down
const bestMatch = findBestMatch(ref, candidates, context);
if (bestMatch) {
@@ -1067,6 +1103,15 @@ export function matchMethodCall(
// names like permissionEngine → PermissionRuleEngine.
if (methodName) {
const methodCandidates = context.getNodesByName(methodName!);
// Ubiquitous-method ceiling (#999): a method name re-declared across a
// vendored theme/SDK (Metronic's `init`/`update`/… on every widget) yields
// K candidates that receiver-word overlap can't reliably disambiguate —
// and filtering + scoring all K per call is the O(K²) cost that wedged
// "Resolving refs" for 15-28 min. Bail before the O(K) work; Strategy 1/2
// (class-name match) already had their precise shot above.
if (methodCandidates.length > AMBIGUOUS_NAME_CEILING) {
return null;
}
const methods = methodCandidates.filter(
(n) => n.kind === 'method' && n.name === methodName
);