[extract] Adaptive deeper file stem on collision (opt-in via resolve_stems_for_corpus) - #710
[extract] Adaptive deeper file stem on collision (opt-in via resolve_stems_for_corpus)#710ibrahimyuecel wants to merge 1 commit into
Conversation
Refactor _file_stem() into a public/private split:
- _initial_stem(path) — legacy 1-parent stem
- _stem_at_depth(path, depth) — variable-depth stem
- resolve_stems_for_corpus(paths) — opt-in pre-pass that adaptively
deepens stems for files in collision groups, populating _STEM_CACHE
- _file_stem(path) — unchanged signature; returns cached deepened stem
if pre-resolved, else legacy 1-parent stem (full backward compat)
Strategy: 1-parent stems work for ~%93 of files. The remaining ~%7
collide because feature-based architectures repeat filenames like
index.ts, routes.py, schema.go across many directories.
resolve_stems_for_corpus() walks the colliding files up to max_depth=5
parents until each gets a unique stem, falling back to a deterministic
hash suffix only if all paths share the same N-parent tail.
Backward compat:
- No behaviour change unless callers invoke resolve_stems_for_corpus()
explicitly. Existing graphs round-trip identically.
- Opt-in upgrade: skill code can add a single line at the top of the
extraction phase to enable deeper stems for the corpus.
Measured on a 3,800-file TypeScript monorepo: 271 files affected by
collisions reduce to 6 truly unresolvable (~%97 reduction). Top
collision groups today: 13 files in 'dtos.index', 11 in 'queries.index',
11 in 'hooks.index', 9 in 'constants.index'.
Smoke test verified backward compat (no resolve_stems_for_corpus call =
legacy behaviour) and adaptive deepening (3 sibling index.py files
get unique feature_a.index / feature_b.index / feature_c.index stems).
|
Hit collision-driven node confusion on a 1,873-file SvelteKit codebase that this PR would address. Adding our cases for evidence. Symptom on our side: 33 self-loops in the graph after extraction. Sampling them, several patterns trace back to
Repo-wide: ~30 of 33 self-loops are likely this bug class (3 are valid SQL FK self-references on This PR's deepening strategy (auto-walk to 2+ parents on collision) would resolve all of them. The deepening is a strict superset of fixes — once stems are unique, the canonical-ID collisions go away and the spurious self-loops with them. One observation worth pinning down: the (Same methodology note as #709 — wipe |
|
Considering we're on v7, doing breaking changes in a v8 at any time for a stable long term solution seems preferable over more config knobs and flags |
[extract] Auto-deepen file stem on parent collision
Summary
_file_stem(path)returnsf"{parent_dir}.{file_stem}"— only one parentsegment. In feature-based architectures (NestJS, Next.js, Angular, Django apps,
Go cmd/X, Rust workspaces) the same parent name (
index.ts,routes.py,schema.go,mod.rs) appears in many directories.This causes node ID collisions: dozens of files share the same stem, and
the
_make_id(stem, name)IDs collapse them. God nodes report showsinflated degrees, communities mix unrelated features, and queries return
wrong-feature matches.
This PR auto-deepens the stem (walks up additional parents) when a collision
is detected during initial extraction, with no API change to consumers.
Why this matters
Tested on a SharedModel monorepo (~3,800 TypeScript files):
Real impact:
dtos/index.tsfiles all map to stemdtos.indexqueries/index.tscollapse togetherhooks/index.ts, 10components/index.ts, 9constants/index.ts...When a query asks for
studyRoutes(defined instudy/constants/routes.ts,re-exported through
study/constants/index.ts), the barrel resolver finds(constants.index, studyRoutes)— but 9 different barrels claim the samestem, so the edge attribution is non-deterministic.
Proposed change
Strategy A: Adaptive deepening (recommended)
In
extract.py, two-pass:more parent until they're unique (or hit a hard cap of 5 levels).
Strategy B: Always deepen to N (simpler, breaking)
Just change
_file_stem()to always include 2 or 3 parents. Backward-compatbreaking — old graphs become unrecognizable, all node IDs shift.
Strategy C: Opt-in flag (
--deep-stem)Add CLI flag to enable adaptive deepening. Default behaviour unchanged. Most
conservative.
Recommended: Strategy A (zero config, backward-compat for non-colliding
stems, only colliding files get longer IDs).
Backward compatibility
with no collisions are byte-identical.
Test fixture
Expected (Strategy A):
feature_a/routes.ts→ stemfeature_a.routes(deepened, wasfeature_a.routesalready — coincidence)index.tsfiles →feature_a.index,feature_b.index,feature_c.indexWithout this PR:
routes.tscollapse to stemfeature_a.routes,feature_b.routes,feature_c.routes(these don't collide today by luck of unique parents)index.tscollapse to sameindexstem — the bugOut of scope
changes group members' stems when collision set changes — annoying for
incremental update if someone deletes a file)
Tested against
SharedModel monorepo: