Skip to content

fix: track barrel re-export renames (export { X as Y } from ...)#1968

Merged
carlos-alm merged 73 commits into
mainfrom
fix/issue-1823-barrel-re-export-with-rename-export-x-as-y
Jul 9, 2026
Merged

fix: track barrel re-export renames (export { X as Y } from ...)#1968
carlos-alm merged 73 commits into
mainfrom
fix/issue-1823-barrel-re-export-with-rename-export-x-as-y

Conversation

@carlos-alm

Copy link
Copy Markdown
Contributor

Summary

While fixing #1730 (renamed import specifiers not resolving to the correct call edge), a distinct-but-related gap was found in barrel/re-export handling: a barrel file that re-exports a symbol under a new name (export { X as Y } from './foo') doesn't track the rename at all, so resolution through that barrel silently breaks for consumers of the renamed name.

Root cause

extractImportNames in src/extractors/javascript.ts (and the mirrored scan_import_names_depth in crates/codegraph-core/src/extractors/javascript.rs) handled export_specifier nodes by always reading the tree-sitter name field (the local declaration being re-exported, i.e. X), and never the alias field (the external name it's re-exported as, i.e. Y).

resolveBarrelExport in src/domain/graph/builder/stages/resolve-imports.ts (and resolve_barrel_export in crates/codegraph-core/src/domain/graph/builder/barrel_resolution.rs) look up a barrel's re-export list by the name a consumer of the barrel would import (Y), then check whether the underlying module actually defines that name. Since the reexport list only ever contained X, both the name-list check and the "does the target define this" check failed whenever a consumer imported the barrel's renamed external name.

Fix

  • Extended Import.renamedImports (added for codegraph exports does not credit consumers that import via a rename (import { X as Y }) #1730) to also populate for export_specifier renames: { local: Y, imported: X }.
  • resolveBarrelExport/resolve_barrel_export now return { file, name } instead of just filename is the declared name to search for in the target file, which may differ from the originally-requested name when a barrel hop renamed it.
  • Threaded the new return shape through every caller in both engines: import-edge emission, barrel-through edges, call-edge resolution (CallNodeLookup.resolveBarrel), and the native orchestrator's symbol lookup.
  • Updated a JS-parser test that had locked in the old (buggy) no-rename-tracking behavior for export_specifier as intentional — per repo policy, a bug is never documented as expected behavior.

Repro (now fixed)

// underlying.js
export function realName() {}

// barrel.js
export { realName as friendlyName } from './underlying.js';

// consumer.js
import { friendlyName } from './barrel.js';
friendlyName();

friendlyName() in consumer.js now correctly resolves to realName in underlying.js on both engines, and codegraph exports underlying.js correctly credits the consumer.

Scope note

codegraph watch's separate DB-only single-file rebuild path (src/domain/graph/builder/incremental.ts, used only by watcher.ts) cannot recover a barrel's rename table when the barrel itself isn't part of the current watch batch — that data only exists in the barrel's freshly-parsed AST, not persisted to the DB. Standard codegraph build (including hash-based incremental skip) is unaffected — confirmed via manual CLI testing on both a from-scratch build and an incremental rebuild that only touches the consumer file. Filed as #1967 for the narrower watch-mode gap.

Test plan

  • Added tests/integration/issue-1823-barrel-rename-reexport.test.ts (parameterized over both engines) plus fixture files under tests/fixtures/barrel-rename-reexport/.
  • Added Rust unit tests in barrel_resolution.rs and build_edges.rs mirroring the new TS behavior.
  • npm test — full suite green (3745 passed, 30 skipped, 2 todo)
  • npm run lint — clean
  • cargo test --lib — 529 passed
  • Native addon rebuilt, codesigned, verified directly
  • codegraph diff-impact --staged -T reviewed — blast radius is exactly the barrel/reexport/import-resolution machinery touched by this fix

Closes #1823

carlos-alm added 30 commits July 5, 2026 11:24
BATCH_COMMANDS entries with sig: 'dbOnly' (currently only complexity)
always wrote their target into opts.target. complexityData treats
opts.target as a symbol-name filter and opts.file as the file-path
filter, so batch complexity <file> never matched anything and the
function list silently fell back to empty with a whole-repo summary
regardless of which file was requested.

Add an optional targetKey on BatchCommandEntry so each dbOnly command
can declare which opts key its targets map to, and set it to 'file'
for complexity. batchData and multiBatchData now route the target
through the declared key instead of assuming opts.target.

Fixes #1721

Impact: 4 functions changed, 6 affected
…ssification

codegraph roles --role dead flagged every function parameter as dead-leaf and
every interface/type member as dead-unresolved, regardless of actual usage.

Root cause: the role classifier's fan-in-based "no callers = dead" heuristic
is meaningless for these two symbol categories, since neither can ever have
inbound call edges by construction:

- Parameters: `kind = 'parameter'` nodes were unconditionally force-assigned
  dead-leaf via a fast-path bypass in classifyNodeRolesFull/Incremental (TS)
  and do_classify_full/do_classify_incremental (native), before any fan-in
  was even computed. A parameter's liveness is a local dataflow question (is
  it referenced within its own function body), not a call-graph reachability
  question, so this produced ~90% noise in --role dead output.

- Interface/type members: every language extractor qualifies interface/type
  members as `Owner.member` top-level definitions (mirroring class method
  qualification), and they never receive inbound call edges (they're
  consumed via type annotations, not calls). Lacking any recognition of
  this, they fell through the normal fan-in/fan-out path and landed in
  dead-unresolved.

Fix:
- Parameters are now fully excluded from role classification (role stays
  NULL), the same treatment already given to file/directory nodes.
- Interface/type members are now recognized in the classifier by resolving
  the Owner.member prefix against same-file TYPE_DEF_KINDS declarations
  (interface/type/struct/enum/trait/record) and classified `leaf`
  unconditionally. Class methods use the identical Owner.member naming
  convention but are unaffected since `class` is not in TYPE_DEF_KINDS, so
  real dead methods/functions remain detected.

Applied to both the TS/WASM classifier (graph/classifiers/roles.ts,
features/structure.ts) and the native Rust classifier
(graph/classifiers/roles.rs) per the dual-engine parity requirement; verified
both engines produce identical results end-to-end against this repo's own
graph.

Fixes #1723

Impact: 6 functions changed, 5 affected
codegraph exports <file> --json (and the --unused dead-export filter it
feeds) reported zero consumers for interfaces/types that are demonstrably
imported and used elsewhere via `import type { X }`, even though
`codegraph deps <file>` correctly showed the importing file in
`importedBy`. Example: ChaContext in src/domain/graph/builder/cha.ts is
imported (type-only) by build-edges.ts and native-orchestrator.ts, but
`codegraph exports` showed consumerCount: 0.

Root cause: exportsFileImpl's per-symbol consumers query
(domain/analysis/exports.ts) only looked at kind = 'calls' edges. The
builder already emits a symbol-level `imports-type` edge for `import
type { X }` statements (source = importing file node, target = the
specific imported symbol -- see emitTypeOnlySymbolEdges in
build-edges.ts/incremental.ts), which `codegraph deps` reads from, but
the exports consumer query never looked at this edge kind. Role
classification (features/structure.ts) already includes 'imports-type'
in its fan-in formula, so `codegraph roles --role dead` was unaffected --
this was purely a gap in exports's independent consumer list.

Fix: widen the consumers query to `kind IN ('calls', 'imports-type')`,
matching the edge-kind set already used everywhere else in the codebase
for cross-file usage credit (structure.ts, graph-enrichment.ts,
boundaries.ts, dependencies.ts). No native Rust changes needed --
domain/analysis/exports.ts is pure query-layer code that reads the
already-built edges table and has no engine-specific mirror.

Deliberately does NOT add `extends`/`implements`: investigation found
those edges are resolved by symbol name only, with no file/import
scoping (buildClassHierarchyEdges in both build-edges.ts/incremental.ts
and the native emit_hierarchy_edges), so they link same-named
declarations across unrelated files (verified: this repo's own graph has
false `implements`/`extends` edges between unrelated fixture classes
across languages and a `Repository` interface in src/types.ts). Filed
as #1812. Also filed #1813 for a related but distinct gap: `import {
type X }` inline per-specifier modifiers aren't tracked as type-only in
either engine's extractor, so such X still gets no credit even after
this fix.

Added tests/integration/exports.test.ts coverage: an interface consumed
only via a symbol-level `imports-type` edge gets consumerCount >= 1 and
is excluded from --unused, while a genuinely unreferenced interface
still shows 0 consumers.

Verified against this repo's own graph: `codegraph exports
src/domain/graph/builder/cha.ts -T --json` now credits ChaContext with
2 consumers (build-edges.ts, native-orchestrator.ts). Full test suite
(201 files, 3359 tests) and lint pass clean.

docs check acknowledged: internal bug fix to exports consumer
computation, no new feature/language/CLI surface/architecture change --
README.md, CLAUDE.md, and ROADMAP.md are unaffected.

Fixes #1724

Impact: 1 functions changed, 3 affected
The CLI's `-f/--file` option is a repeatable Commander accumulator
(collectFile) that always produces a string[], even on first use. The
native composite bindings backing fn-impact (findNodesWithFanIn) and
query (fnDeps) forwarded that array straight into napi-rs functions
whose Rust signatures only accepted a single String, so any use of
-f/--file crashed with "Failed to convert JavaScript value ... into
rust type `String`" regardless of engine defaults. context worked
only because its code path never touches the native repository for
symbol lookup.

Widen the native Rust signatures (find_nodes_with_fan_in, fn_deps) to
accept Vec<String> and build an OR-of-LIKE clause for multiple files,
mirroring buildFileConditionSQL/NodeQuery.fileFilter on the JS side.
Normalize the file option before calling into the native binding on
the TS side, and thread the widened string | string[] type through
QueryOpts, fnDeps's opts, and the call chain down to
findMatchingNodes. This also makes repeated -f/--file genuinely
multi-file end to end for fn-impact/query, matching context's
existing behavior, instead of crashing on any use.

findNodesByScope shares QueryOpts but has no CLI caller and its
native binding still only accepts one file; it now takes the first
value defensively rather than crash or fail to type-check.

Fixes #1726

docs check acknowledged: pure bug fix, no new CLI options/languages/
architecture — the -f/--file "repeatable" docs are now accurate
rather than needing correction.

Impact: 14 functions changed, 34 affected
… exports

Two compounding root causes made `where --file`'s `exported` array (and
`codegraph exports`) unreliable for entire classes of exports:

1. The JS/TS export-statement handler (WASM query path, WASM walk path, and
   the mirrored native Rust extractor) only recognized `export function`,
   `export class`, `export interface`, and `export type` declarations. It
   never matched `lexical_declaration`/`variable_declaration`, so
   `export const/let/var …` was never added to the extractor's export list —
   regardless of the initializer's shape — leaving the `exported=1` DB column
   permanently unset for every exported constant in the codebase (including
   ones that happened to look "correct", like a `new Set(...)`-initialized
   constant referenced as a call argument elsewhere).

2. `where --file`'s `exported` list ignored the `exported` DB column entirely
   and computed membership from `findCrossFileCallTargets` (symbols targeted
   by a cross-file `calls` edge) — a heuristic that only "worked" by
   coincidence for symbols that happened to be passed as call arguments
   elsewhere. `codegraph exports` already had the right idea (prefer the
   `exported` column, fall back to the heuristic only for pre-migration DBs)
   but duplicated that logic locally instead of sharing it.

Fixes:
- src/extractors/javascript.ts: extract the function/class/interface/type
  kind map plus a new lexical/variable-declaration branch into a shared
  `collectExportedDeclarations`, used by both `handleExportCapture` (query
  path) and `handleExportStmt` (walk path) so they can't drift apart again.
  Declarator values are classified with the same predicate already used to
  build the matching Definition (function-valued -> kind 'function',
  literal/array/object/new-expression-valued `const` -> kind 'constant').
- crates/codegraph-core/src/extractors/javascript.rs: mirror the same fix in
  `handle_export_declaration` via a new `collect_exported_var_declarations`.
- src/db/repository/nodes.ts: add `findExportedNodesByFile`, the single
  shared implementation of "prefer exported=1, fall back to cross-file calls
  for pre-migration DBs" — re-exported through db/repository/index.ts and
  db/index.ts.
- src/domain/analysis/exports.ts: use the shared helper instead of a locally
  duplicated hasExportedCol/exportedNodesStmt implementation.
- src/domain/analysis/symbol-lookup.ts: `whereFileImpl` now uses the shared
  helper instead of `findCrossFileCallTargets` directly, so `where --file`
  and `exports` agree on what "exported" means.

Tests:
- tests/parsers/javascript.test.ts: unit coverage for bare-literal, new
  Set(...), object-literal-with-methods, and arrow-function exported consts,
  plus a non-exported-const negative case and a function/class regression
  guard for the shared-helper refactor.
- tests/engines/query-walk-parity.test.ts, tests/engines/parity.test.ts:
  cross-path/cross-engine regression cases for the same shapes.
- tests/integration/queries.test.ts: new fixture node with exported=1 but
  zero incoming edges of any kind, proving `where --file`'s exported list
  no longer depends on cross-file call presence.

No user-facing command/language/architecture changes, so README/CLAUDE.md/
ROADMAP.md are unaffected — docs check acknowledged.

Fixes #1728

Impact: 8 functions changed, 17 affected
tree-sitter-typescript's predefined_type production (string, number,
boolean, ... primitive type keywords) lexes its keyword as an anonymous
grammar token whose type string is identical to the named `string` node
type used for real string literals. Both engines matched by node type
alone, so `codegraph ast --kind string` spuriously matched bare `string`
type annotations (interface fields, parameter types, return types) as if
they were string literals.

Add an isNamed/is_named() guard scoped to the JS/TS/TSX family on both
engines: WASM via astRequiresNamedNode() in ast-analysis/rules/index.ts
feeding the shared resolveAstKind() in ast-store-visitor.ts, native via a
match guard in javascript.rs's walk_ast_nodes_depth. Genuine string,
template, and string-literal-type nodes are always named and unaffected.

No user-facing command/language/architecture changes, so README/CLAUDE.md/
ROADMAP.md are unaffected — docs check acknowledged.

Fixes #1729

Impact: 8 functions changed, 13 affected
codegraph exports <file> reported zero consumers for an exported
function/symbol that is genuinely imported and used elsewhere, whenever
the importing file renamed the binding at the import site
(import { X as Y } from '...'). Example: collectFiles in
src/domain/graph/builder/helpers.ts is imported (as collectFilesUtil)
and called by collect-files.ts and native-orchestrator.ts, but
`codegraph exports helpers.ts -T --json` showed consumerCount: 0.

Root cause (extractor, both engines): extractImportNames (javascript.ts)
and its mirrored scan_import_names_depth (javascript.rs) handled
import_specifier nodes with `node.childForFieldName('name') ||
childForFieldName('alias')`. Per the tree-sitter grammar, `name` is
*always* present on import_specifier (the name as declared by the source
module); `alias` only exists for `X as Y` and holds the *local* binding
actually referenced by call sites. Preferring `name` unconditionally
meant the local alias was silently dropped for every renamed import —
`imp.names` recorded "collectFiles" instead of "collectFilesUtil", so
`importedNames` (keyed by call-site text) never had a matching entry and
the call fell through unresolved.

Fixing only the extractor was not sufficient: once `imp.names` correctly
holds the local alias, `resolveCallTargets` (call-resolver.ts) /
resolve_call_targets (build_edges.rs) still searched the *target file*
for a symbol literally named "collectFilesUtil" — which doesn't exist
there (only "collectFiles" does). This required threading a second
piece of information end-to-end: for each renamed specifier, the local
alias's *original* exported name, so target-file/barrel lookups search
by the right name while importedNames stays keyed by the call-site text.

Changes (both engines, full-build + incremental/native-orchestrator +
native-hybrid paths):
- types.ts / types.rs: new `Import.renamedImports` /
  `Import.renamed_imports` field — { local, imported } pairs, populated
  only for specifiers that actually rename a binding.
- extractors/javascript.ts, .rs: extractImportNames /
  extract_import_names_with_renames now prefer `alias` (local binding)
  for import_specifier and record the rename pair. export_specifier is
  deliberately left unchanged (see scope notes below). Fixed in both the
  walk path (handleImportStmt) and the query/WASM-worker path
  (handleImportCapture) on the TS side.
- build-edges.ts, build_edges.rs, import_edges.rs, pipeline.rs,
  call-resolver.ts, incremental.ts: buildImportedNamesMap /
  collect_imported_names_for_file now also produce a local-alias ->
  original-name map; resolveCallTargets / resolve_call_targets consult
  it to search the target file by the original name. Applied
  consistently to the primary call-resolution path, the
  Object.defineProperty post-pass, points-to alias resolution, barrel
  edge / import-type edge emission, and cross-file return-type
  propagation — every place that previously assumed a call site's local
  name equals the target file's declared name.

Native Rust: touched and verified. Rebuilt locally via
`npx napi build --platform --release` (crates/codegraph-core), codesigned
(`codesign --sign - --force`), and installed over the loaded
node_modules/@optave/codegraph-darwin-arm64/codegraph-core.node so the
fix was exercised by the actual native engine, not the prebuilt npm
binary. `cargo test --lib`: 427 passed.

Tests added:
- tests/parsers/javascript.test.ts: extractor-level coverage for the
  local-name/rename-pair extraction, a mixed renamed+non-renamed
  specifier list, and confirmation export_specifier is unaffected.
- crates/codegraph-core/src/extractors/javascript.rs: matching Rust unit
  tests.
- tests/integration/issue-1730-renamed-import-consumer.test.ts: new
  end-to-end test building real files through buildGraph() on both wasm
  and native engines, asserting the calls edge exists, no edge is
  created against a nonexistent "collectFilesUtil" symbol, and
  `codegraph exports` credits the consumer — on both engines.
  tests/integration/exports.test.ts was not extended: that file
  hand-inserts DB rows and only exercises the query layer, which is
  unaffected by this bug (the missing piece was edge creation, not the
  consumer query added for #1724).

Verified against this repo's own graph (native engine, rebuilt):
`codegraph exports src/domain/graph/builder/helpers.ts -T --json` now
credits collectFiles with 2 consumers (collect-files.ts,
native-orchestrator.ts); WASM engine agrees exactly. Without -T, also
picks up tests/unit/builder.test.ts via the (unrenamed) builder.ts
barrel re-export, which already worked pre-fix and was only hidden by
-T in the original repro.

Scope notes — filed as separate issues rather than expanding this fix:
- #1823: export { X as Y } from '...' (barrel re-export with rename) is
  not tracked — resolveBarrelExport/resolve_barrel_export key off the
  original name, and export_specifier extraction was deliberately left
  unchanged here since barrel/reexport tracing is a distinct mechanism.
- #1824: dynamic import destructuring rename
  (const { a: b } = await import(...)) has the same class of bug in
  extractDynamicImportNames, a different extraction function.
- #1825: a renamed import used as a call receiver
  (import { X as Y }; Y.method()) still fails to resolve — confirmed
  empirically (no calls edge created) — a different resolution strategy
  (resolveByReceiver/resolveViaDirectQualifiedMethod in
  resolver/strategy.ts) than the one this fix addresses.

Full test suite (202 files, 3398 tests) and lint pass clean.

docs check acknowledged: internal resolver/extractor bug fix, no new
feature/language/CLI surface/architecture change -- README.md,
CLAUDE.md, and ROADMAP.md are unaffected.

Fixes #1730

Impact: 29 functions changed, 47 affected
… builds

insertNodes committed file_hashes for changed files in the same
transaction as node insertion, before resolveImports/buildEdges rebuilt
those files' edges (a separate, later stage/transaction) — in both the
JS/WASM pipeline and the native Rust orchestrator. Any exception, crash,
or interruption between the two left a hash that claimed a file was
"up to date" while its edges still reflected an older revision. Since
change-detection trusts file_hashes exclusively, that divergence was
never self-healed by later builds.

Defer the file_hashes commit so it only runs once edges have been
rebuilt to match:
- insert-nodes.ts: insertNodes no longer writes file_hashes for changed
  files; new commitFileHashes() does it, called from pipeline.ts after
  buildEdges.
- insert_nodes.rs / pipeline.rs / connection.rs: do_insert_nodes no
  longer upserts file_hashes (only removed-file cleanup, which has no
  coupling risk); new commit_file_hashes() runs after Stage 7 (edges).

Watch-mode's rebuildFile never wrote file_hashes at all, leaving it
permanently stale after every edit — also fixed by writing/deleting the
hash once a file's edges have been rebuilt or the file is deleted.

Adds tests/integration/issue-1731-hash-edge-coupling.test.ts: a
fault-injection test that throws inside buildEdges mid-incremental-build
and asserts the hash does not advance until edges genuinely match (and
that a retry self-heals), plus coverage for rebuildFile keeping
file_hashes in sync with its edge rebuilds.

Fixes #1731

Impact: 8 functions changed, 13 affected
checkNoSignatureChanges compared def.line (post-change, new-file
coordinates, from the rebuilt graph) against diff.oldRanges (pre-change,
old-file coordinates). Any diff that shifts line counts before a given
point in a file — virtually all deletions/insertions — could make an
untouched symbol's new line number coincidentally fall inside the old
hunk's range, producing a false "signature change" violation.

checkMaxBlastRadius already used diff.changedRanges (new-file positions)
against the same db; checkNoSignatureChanges now does the same. The
parameter is renamed from oldRanges to changedRanges to make the expected
coordinate space explicit. diff.oldRanges remains computed by
parseDiffOutput and is still exercised directly by its own tests.

No README/CLAUDE.md/ROADMAP updates needed — internal predicate logic fix,
no new commands, languages, or architecture changes (docs check acknowledged).

Fixes #1732
Fixes #1737

Impact: 2 functions changed, 5 affected
…g WASM grammars

Every git worktree gets its own untracked node_modules/ and grammars/, so a
worktree set up before a host Node upgrade (or where npm install was
interrupted) can end up with a better-sqlite3 binary compiled for the wrong
Node ABI, or an incomplete grammars/ directory — both fail deep inside a
build or test run with confusing, unrelated-looking errors rather than a
clear diagnosis.

Adds src/infrastructure/doctor.ts with two checks whose decision logic is
pure and unit-tested in isolation from the I/O:
- native ABI compatibility, via a real require() attempt
- grammar completeness against the full LANGUAGE_REGISTRY, split by the
  registry's own required flag: a missing required (JS/TS/TSX) grammar fails
  the check, but a missing optional grammar only warns. Non-required parsers
  are designed to fail gracefully at runtime (per this repo's own CLAUDE.md),
  so a worktree missing one rarely-used language's grammar must stay able to
  run npm test, not get hard-blocked before a single test starts.

scripts/doctor.ts is the CLI entry point (npm run doctor, optionally with
--fix for a scoped, worktree-local repair covering both blocking and
non-blocking findings) and is also wired as pretest so npm test fails fast
on a genuinely blocking problem instead of a wall of unrelated failures.

Fixes #1733

docs check acknowledged: CLAUDE.md already covers the new npm run doctor
command and infrastructure/doctor.ts (added in this same change); README.md
does not document npm-run dev scripts (only the shipped codegraph CLI), and
ROADMAP.md has no phase this bug-fix-sized change affects.

Impact: 6 functions changed, 3 affected
`codegraph communities --drift` produced different modularity and
community assignments across separate full rebuilds of byte-identical
source. Two independent, compounding causes in the native Rust engine:

1. The build pipeline collected parsed file symbols into a
   `std::collections::HashMap` (pipeline.rs), whose iteration order is
   randomized per-process. That order drove node/edge insertion order
   into SQLite, so the same file could get a different autoincrement
   `id` (and therefore a different position in the in-memory graph) on
   every rebuild. Fixed by switching `file_symbols` to `BTreeMap`
   throughout the pipeline, import-edge, and structure-metrics stages,
   so insertion order is always sorted by file path.

2. The native Louvain local-move phase (louvain.rs) accumulated
   per-candidate-community weights in a `HashMap`. A genuine tie in
   modularity gain between candidate communities was broken by hashmap
   bucket order instead of a reproducible rule -- non-deterministic even
   with a fixed random seed, since the seed only controls visitation
   order, not this tie-break. Fixed by switching `cur_edges`/`comm_w`
   to `BTreeMap`.

Also added `ORDER BY` to the node/edge read queries used to build the
community-detection graph (both native `graph_read.rs` and the JS/WASM
`graph-read.ts` mirror), as defense in depth: SQLite's row order for a
bare `WHERE` scan is otherwise unspecified.

Verified via 10+ full rebuilds of this repo's own ~900-file graph
producing byte-identical `communities --drift --json` output, versus
differing modularity/community counts on every rebuild beforehand.

This is an internal determinism fix with no new features, commands, or
language support changes, so no README/CLAUDE.md/ROADMAP.md updates are
needed (docs check acknowledged).

Fixes #1734

Impact: 4 functions changed, 17 affected
The PostToolUse hook's hardcoded extension case-statement was a hand-copied
subset of EXTENSIONS (src/shared/constants.ts) that had drifted out of
sync — .mjs/.cjs were missing, so editing those files silently skipped the
incremental rebuild and left .codegraph/graph.db stale.

The hook now prefers dist/hook-extensions.txt, a plain-text snapshot of
EXTENSIONS generated by scripts/gen-hook-extensions.mjs as part of
`npm run build`, checked with a native `grep -qxF` (no extra Node startup
per edit). A synced hardcoded case list remains as a fallback for before
the first build. tests/unit/hook-extensions.test.ts fails if that fallback
ever drifts behind EXTENSIONS again.

This only touches internal dev-tooling (a Claude Code hook and its
build-time codegen script) — no language support, CLI feature, or
architecture surface changed, so README/CLAUDE.md/ROADMAP do not need
updates. docs check acknowledged.

Fixes #1736
…n incremental rebuild

codegraph structure --depth 2 --json reported stale fileCount/symbolCount/
fanIn/cohesion/density for a directory after an incremental rebuild added
or removed a file in it. Only a full rebuild (--no-incremental) produced
correct numbers.

Root cause: the small-incremental fast path (updateChangedFileMetrics in
domain/graph/builder/stages/build-structure.ts, mirrored by
update_changed_file_metrics in crates/codegraph-core/src/features/
structure.rs) only ever updated per-FILE node_metrics rows. It never
touched directories at all -- no directory-metrics recompute, no
`contains` edge for the new file, and no directory node for a brand-new
directory. This path triggers whenever an incremental build touches at
most smallFilesThreshold (5) files and the repo already has more than 20
files -- i.e. almost every normal edit-and-rebuild cycle on a non-trivial
repo, including a pure-removal build (0 parsed files, which trivially
satisfies the "<=5" gate). The full (non-fast-path) incremental branch
was already correct in both engines -- it always recomputes every current
directory's metrics unconditionally.

Fix: added refreshAffectedDirectoryMetrics (build-structure.ts) and its
mirror refresh_affected_directory_metrics (structure.rs), which run
alongside the existing per-file fast path whenever it's taken. They
recompute metrics for the ancestor directories of every file touched by
the build (added, removed, or modified), plus any directory reachable
from them via a live cross-directory import edge (a changed file
gaining/losing an import into a sibling package shifts that package's
fan-in/fan-out too, even though none of its own files changed -- mirrors
the one-hop neighbour-expansion classifyNodeRolesIncremental already does
for role classification). Directory/edge bookkeeping (node creation,
`contains` edges) is wired up idempotently, so a file landing in a
brand-new (possibly multi-level) directory is handled too. All of this
uses indexed point queries against the live DB state, bounded by (changed
files x path depth) rather than repo size, so it stays cheap enough to run
unconditionally on the fast path. getAncestorDirs was promoted from a
features/structure.ts-private helper to shared/constants.ts so both the
fast path and the full path use the same implementation.

Filed #1839 for a narrower residual gap this does not cover: a directory
whose only link to the touched file set was an edge to/from a file that
was itself just removed can't be discovered here, since that edge's
evidence is already deleted by the purge step that runs earlier in the
pipeline.

Verified against the real incremental pipeline (not just unit-level): a
throwaway repro script confirmed the stale fileCount/symbolCount and a
missing `contains` edge for the new file before the fix, and correct,
full-rebuild-matching output after, on both engines (native rebuilt via
napi build + codesign for local verification). Added
tests/integration/issue-1738-structure-metrics-incremental.test.ts, which
diffs incremental-rebuild output against a from-scratch full build of the
identical final file set across add/remove/new-nested-directory/
cross-directory-neighbour scenarios, run against both WASM and native.

npm test: 207 files / 3444 tests passed, 0 failed.
npm run lint: clean.
cargo check / cargo test -p codegraph-core: clean.

This is an internal bug fix to incremental-build correctness -- no new
language support, CLI commands, or architecture surface changed, so
README/CLAUDE.md/ROADMAP.md do not need updates (docs check acknowledged).

Fixes #1738

Impact: 3 functions changed, 8 affected
…n DEFAULTS

Moves the remaining inline maxBuffer magic numbers for git subprocess
calls into DEFAULTS, following the same pattern already applied to
resolveSecrets's apiKeyCommand execFileSync options:

- DEFAULTS.coChange.execMaxBufferBytes (50 MB) — git log in cochange.ts
- DEFAULTS.check.execMaxBufferBytes (10 MB) — git diff in check.ts and
  diff-impact.ts (same operation, shared constant)
- DEFAULTS.build.execMaxBufferBytes (100 MB) — git check-ignore in
  native-orchestrator.ts

Call sites that already had a resolved config in scope (check.ts,
diff-impact.ts, native-orchestrator.ts) now read the value off it so
.codegraphrc.json overrides apply; cochange.ts reads DEFAULTS directly,
matching its existing convention for other coChange fields. No
behavioral change — numeric values are unchanged.

docs check acknowledged: no README/CLAUDE.md/ROADMAP.md updates needed —
this is a config-registration fix with no language/architecture/roadmap
impact; docs/guides/configuration.md was updated with the three new keys.

Fixes #1739

Impact: 10 functions changed, 16 affected
…ng fan-in

checkMaxBlastRadius previously failed any staged change touching a function
whose absolute transitive-caller count exceeded the threshold, regardless of
whether the diff actually changed anything risky. A function reachable only
through a near-universally-called "spine" function (e.g. resolveSecrets via
loadConfig) would fail on any edit at all, including fully behavior-preserving
ones like replacing an inline literal with a named constant.

checkMaxBlastRadius now exempts a touched function from the threshold unless
the diff changed its call graph shape: its own declaration line was touched
(signature/name risk), or the set of paren-preceded tokens referenced in its
body changed (a callee was added, removed, or swapped). This is a mechanical,
diff-text heuristic rather than a full pre/post call-graph reconstruction —
parseDiffOutput now pairs each added-line run with whatever removed text it
replaced (scoped to a single hunk, never crossing hunk boundaries) so the
comparison needs no re-parsing and stays fully synchronous. Missing edit data
(e.g. hand-built ranges) conservatively falls back to the old always-gate
behavior, so this is purely opt-in via real diff data.

Known limitation: paren-less call syntax (Ruby's `foo x, y`, Lua's `foo
"arg"`) is invisible to the token-set comparison, so a newly introduced
paren-less call could be missed and its function wrongly exempted. This is a
deliberate, documented trade-off for a non-parsing check.

Also updates titan-gate SKILL.md Step 8 (both the internal and docs/examples
copies) to defer to Step 1's now-authoritative, shape-aware blast-radius
predicate instead of re-deriving a pass/fail from diff-impact's raw absolute
counts.

No user-facing feature, command, or language changed -- README/ROADMAP left
as-is; docs check acknowledged.

Fixes #1740

Impact: 17 functions changed, 10 affected
…g callees

extractCallbackReferenceCalls (and its native mirror) emitted a dynamic call
edge for every bare identifier argument passed to any call expression, with
no gating on the callee — unlike member_expression args, which were already
gated by CALLBACK_ACCEPTING_CALLEES/HTTP_VERB_CALLEES (#974/#1191). When an
identifier's name collided with an unrelated exported function elsewhere in
the repo, the resolver's global-fallback confidence scoring bound the two
together, fabricating a call edge and, in codegraph's own graph, a phantom
cycle between src/features/communities.ts and src/presentation/communities.ts
(analyzeDrift/communitiesData both call analyzeDrift(communities, ...), where
`communities` is a plain parameter — not a call to the unrelated `communities`
CLI command).

Apply the same allowlist gate to identifier args that member_expression args
already use, in both the TS/WASM extractor (extractCallbackReferenceCalls,
shared by the walk and query extraction paths) and the native Rust mirror
(extract_callback_reference_calls). Legitimate callback-by-reference patterns
(e.g. arr.forEach(myCallback), router.use(handleToken)) are preserved since
their callees are already in the allowlist.

On codegraph's own repo this eliminates the two fabricated communities.ts
edges and the phantom 3-node cycle, and reduces the broader
dynamic=1/confidence=0.5/kind=calls edge signature from 342 to 14 (native)
across the whole graph.

Recalibrates the jelly-micro `classes` recall floor (6/31 -> 5/31): the one
lost edge (f4 -> f1, from `function f4(x) { return x(f1); }`) was only ever
matched because the removed heuristic fabricated it — `x` is an unresolvable
parameter call, syntactically identical to the false-positive shape this fix
removes, and only Jelly's points-to analysis can resolve it precisely.

Fixes #1741

Internal extractor/resolver bug fix — no new language support, CLI surface,
architecture, or roadmap changes. docs check acknowledged.

Impact: 1 functions changed, 6 affected
Follow-up refinement to #1741's identifier-argument gating. A peer audit of
the resolution-benchmark fixtures found two edges the naive name-only gate
would break:

- tests/benchmarks/resolution/fixtures/pts-javascript/array-from.js:
  `Array.from(arr, mapCallback)` — a legitimate, well-known stdlib callback
  pattern, not a name-collision false positive. `Array.from`'s callee name
  is `from`, which wasn't in CALLBACK_ACCEPTING_CALLEES at all, so the
  callback arg (mapCallback) was dropped.
- tests/benchmarks/resolution/fixtures/typescript/callbacks.ts:
  `processEach(users, logUser)` — logUser passed to a project-defined
  higher-order function. No name allowlist can enumerate arbitrary user
  code, so this class of edge is a genuine, harder gap (see below).

Fix Array.from properly rather than just adding 'from' to the general
allowlist: `Array.from(arrayLike, mapFn, thisArg)` puts the callback at
argument index 1, not 0 — naively allowlisting 'from' the same way as
'.map'/'.forEach' would treat `arrayLike` (plain data at index 0) as a
callback candidate too, reintroducing the exact name-collision false-positive
class #1741 fixes. Added POSITIONAL_CALLBACK_ARG_INDEX (TS) /
positional_callback_arg_index (Rust) so a callee can restrict eligibility to
one specific argument index instead of "any position" (the existing
behavior, still needed for variadic Express/Router middleware chains like
`app.get(path, mw1, mw2, handler)`). Applies uniformly to every TypedArray
constructor's .from (Uint8Array.from, Int32Array.from, etc.) since they share
the same signature convention.

This restores pts-javascript to 13/13 (100% recall/precision, matching its
pre-#1741 baseline exactly).

The processEach/filterThen case (arbitrary user-defined higher-order
functions) is NOT fixed here — recognizing it needs the callee's own
parameter type (function-shaped?), which is a resolver-level, cross-engine
feature, not a name/position extension of this gate. Left the 3 affected
expected-edges.json entries in typescript/expected-edges.json unchanged
(they're real, decidable facts, not fabricated edges) and documented the
gap in resolution-benchmark.test.ts's THRESHOLDS.typescript comment: recall
is now 44/47 (93.6%), still well above the 0.72 floor. Tracked as a
follow-up in issue #1845 (not fixed by this commit).

Verified via tests/benchmarks/resolution/resolution-benchmark.test.ts
(--reporter=verbose): pts-javascript 100%/100% (was 92.3% recall with the
naive gate), typescript 95.7%/93.6% (unchanged by this commit, gap tracked).
Cross-engine parity (native rebuilt + codesigned) and query/walk-path parity
both re-verified with new dedicated test cases.

Internal extractor/resolver refinement — no new language support, CLI
surface, architecture, or roadmap changes. docs check acknowledged.

Impact: 1 functions changed, 6 affected
`codegraph exports <file>` treated any file-level `reexports` edge as
proof that every export of the target file was re-exported, so a single
`export { X } from 'Y'` caused `reexportedSymbols` to dump all of Y's
exports (including symbols never mentioned in any reexport clause, and
symbols only ever imported as a type). The file-level edge only proves a
reexport *relationship* exists with a target file — it never carried the
specific symbol name(s).

Both engines now also emit a symbol-level `reexports` edge straight to the
specifically-named symbol for `export { X }` / `export { X as Z }`
clauses, mirroring the existing `imports-type` symbol-level edge from
#1724 (JS/WASM: build-edges.ts + incremental.ts watch-mode path; native:
import_edges.rs primary pipeline + build_edges.rs FFI fallback). The query
layer prefers these precise edges when present and only falls back to a
target's full export list when none exist — i.e. a genuine
`export * from 'Y'` wildcard, which really does re-export everything.

Fixes #1742

docs check acknowledged — internal query/edge-emission bug fix, no new
commands, languages, or architecture to document.

Impact: 14 functions changed, 14 affected
… complexity

storeCfgResults / storeNativeCfgResults in ast-analysis/engine.ts (and a
duplicate copy in domain/wasm-worker-entry.ts) overwrote the correctly
computed cyclomatic complexity with a CFG-derived value (edges - blocks + 2).
That formula doesn't model short-circuit logical operators (&&, ||, ??),
optional chaining (?.), or nested function/closure bodies, all of which the
AST-based cyclomatic walk correctly counts (matching cognitive complexity's
treatment of closures). The override ran on every WASM build and on native
builds whenever the Rust orchestrator was bypassed (e.g. an engine switch or
schema/version change triggers forceFullRebuild), silently corrupting
cyclomatic for any function using those constructs while leaving cognitive,
Halstead, and LOC untouched.

The native engine never had this override, so it was already correct.
Cyclomatic is now always the single AST-derived value on both engines.

Fixes #1743

Impact: 5 functions changed, 13 affected
Full builds always tag directly-resolved calls edges technique='ts-native'
(applied by both engines, not just native), either inline or via a
post-insert backfill. Two incremental-rebuild paths left it NULL instead:

- codegraph watch (rebuildFile/emitIncrementalCallEdges in
  builder/incremental.ts) never set technique at all when inserting a calls
  edge. Fixed with a scoped post-rebuild backfill, mirroring
  applyEdgeTechniquesAfterNativeInsert.
- codegraph build's native-orchestrator incremental path
  (backfillEdgeTechniquesAfterNativeOrchestrator) scoped its backfill to only
  the directly-changed files reported by Rust, missing one-hop reverse
  dependents whose outgoing edges into a changed file are reconnected by
  Rust's own reverse-dep cascade and so also carry a fresh, untagged
  technique. Fixed by expanding the scope to include them.

Rust itself never writes the technique column on any edge-insertion path
(confirmed across crates/codegraph-core) — it has always been the JS-side
backfill's job, so no native crate changes are needed.

No README/CLAUDE.md/ROADMAP.md updates needed — bug fix only, no new
architecture, commands, or language support (docs check acknowledged).

Fixes #1744

Impact: 4 functions changed, 9 affected
response.json() in embedRemote sat outside the try/catch guarding the
fetch call, so a 200 OK response with a malformed/non-JSON body (e.g.
an HTML error page from a misconfigured proxy) threw a raw uncaught
SyntaxError instead of the descriptive EngineError used by every other
failure mode in this function (timeout, network failure, non-2xx
status, bad response shape, dimension mismatch).

Fixes #1745

Impact: 1 functions changed, 9 affected
getDefaultUserConfigPath() and resolveUserConfigPath() each implemented
the same XDG_CONFIG_HOME / APPDATA / ~/.config three-way branch, so a
future change to the priority order or path shape had to be applied in
two places. Extract the branch into computePlatformDefaultConfigPath(),
called by both; resolveUserConfigPath() still layers its own existsSync
checks on top. Pure dedup — no behavior change.

Fixes #1746

Impact: 3 functions changed, 38 affected
…onfig.ts

Extract-method decomposition of the 5 functions in src/infrastructure/config.ts
flagged for excessive Halstead effort (>15000) while staying within warn-only
range on cognitive/cyclomatic/maxNesting/mi: loadConfig, loadConfigWithProvenance,
resolveConsent, promptForConsentIfNeeded, and resolveSecrets.

Each function's natural sub-steps (merge layers, consent-resolution stages,
provenance-tracking stages, prompt gating/IO, secret-command validation/exec)
are pulled into named private helpers, with the original function reduced to a
thin orchestrator. No control flow, priority ordering, or error handling was
changed — verified via the full test suite and manual sanity checks of config
loading, global-config consent resolution, and apiKeyCommand secret resolution.

Fixes #1747

Impact: 20 functions changed, 136 affected
… check acknowledged)

Extract-method refactor only, no behavior change. Both functions exceeded
the halstead.effort FAIL threshold (>15000) and were flagged WARN on
cognitive/cyclomatic complexity.

findDbPath: extracted resolveCustomDbPath (--db directory/`.codegraph`
handling), resolveDbSearchCeiling and resolveDbSearchStartDir (realpathSync
normalization), and walkUpForDbPath (the parent-directory walk with git
ceiling/no-ceiling stop conditions).

openRepo: extracted wrapInjectedRepo (opts.repo validation), tryOpenRepoNative
(native rusqlite attempt with busy/locked re-throw and fallback-on-failure),
and openRepoSqliteFallback (better-sqlite3 fallback construction).

halstead.effort: findDbPath 30574.33 -> 1710.82, openRepo 15118.12 -> 2055.6.
No newly extracted helper exceeds any threshold. Internal refactor only —
no public API, CLI, or language-support changes, so README/CLAUDE.md/ROADMAP
do not need updates.

Fixes #1748

Impact: 11 functions changed, 159 affected
Extracts the /\b(busy|locked|SQLITE_BUSY|SQLITE_LOCKED)\b/i regex check
into a single isBusyOrLockedError(msg) helper, replacing the identical
inline check duplicated between openRepo's native path (tryOpenRepoNative)
and openReadonlyWithNative's native path. No change to matching behavior
or the busy_timeout value.

DEFAULTS.db.busyTimeoutMs (this issue's other flagged cleanup) was
already added and wired into openDb/openReadonlyOrFail by an earlier
commit in this stack (8f23020); this commit covers the remaining
regex duplication only.

docs check acknowledged: internal dedup-only refactor, no CLI surface,
language support, or documented architecture/design decision changed.

Fixes #1749

Impact: 3 functions changed, 27 affected
…ng them

The catch block in resolveCustomDbPath (part of the findDbPath flow) caught
all fs.statSync errors — including unexpected ones like EACCES or symlink
loops — indistinguishably from the expected "path doesn't exist yet" case,
leaving no diagnostic trail. Add a debug() call matching this file's
existing convention (18 other catch blocks already do this).

Fixes #1750

Impact: 1 functions changed, 68 affected
…oseDbDeferred

closeDbPair, closeDbPairDeferred, and closeDbDeferred in src/db/connection.ts
had zero direct test coverage despite being core resource-lifecycle
primitives in a fanIn-55 file — the same category of gap where the
openReadonlyWithNative leak went undetected (see
openReadonlyWithNative-leak.test.ts).

Adds three describe blocks to tests/unit/db.test.ts, alongside the existing
openDb/closeDb coverage:
- closeDbPair: native handle closes before the better-sqlite3 handle; a
  native close failure doesn't prevent the better-sqlite3 close; works with
  no native handle present.
- closeDbPairDeferred: native closes synchronously within the call, while
  the better-sqlite3 close is deferred via closeDbDeferred (including when
  the native close throws).
- closeDbDeferred: the advisory lock releases synchronously (verified via a
  real lock file), the handle itself closes on the next tick, and
  flushDeferredClose() closes it synchronously when called first — with the
  originally scheduled callback correctly skipping a second close.

Fixes #1751
…arations on incremental rebuild

Root cause: reconnectReverseDepEdges (build-edges.ts, WASM/JS engine) and
its native mirror reconnect_reverse_dep_edges (crates/codegraph-core's
detect_changes.rs) re-attach a reverse-dependency caller's edge to its
purged-and-reinserted target using only (name, kind, file) plus "nearest
to the old line" as a tiebreak. When a file contains multiple distinct
symbols sharing the same name and kind -- e.g. several object-literal
close() {} methods returned from different functions in the same file, a
pattern this repo's own src/db/connection.ts uses four times -- nearest-
line is not a reliable way to tell them apart: once unrelated code
shifts the whole same-named group, an old reference line can end up
numerically closer to a different sibling's new line than to its own,
silently re-attaching the edge to the wrong symbol (and collapsing
distinct edges together via INSERT OR IGNORE while leaving another
candidate untargeted). A full rebuild is immune because it re-resolves
every call from scratch using real call-site information, not line
proximity.

Root-caused by replaying this repo's own last 35 real commits as a
sequence of incremental builds and diffing the result against a full
rebuild of the identical final source: node tables came out byte-
identical, but 5 reverse-dep callers ended up wired to close@line 433
instead of the correct close@line 580.

Fixed by recording each target's 1-based ordinal rank (by line) among
its same-(name,kind) siblings at save time, and using that ordinal --
not line proximity -- to re-select the correct candidate after
purge+reinsert, falling back to nearest-line only when the sibling
count itself changed since save (a genuinely ambiguous case).

Changes:
- src/domain/graph/builder/context.ts: extend savedReverseDepEdges with
  tgtOrdinal/tgtSiblingCount.
- src/domain/graph/builder/stages/detect-changes.ts: compute each
  target's ordinal/sibling-count before purge (computeNodeOrdinals).
- src/domain/graph/builder/stages/build-edges.ts: reconnect using the
  saved ordinal instead of nearest-line (pickReconnectTarget).
- crates/codegraph-core/.../detect_changes.rs: identical fix on the
  native engine (compute_ordinals, pick_reconnect_target), plus Rust
  unit/integration tests covering the ordinal match, the sibling-count-
  changed fallback, and a full save/purge/reconnect round trip.

Native engine: fixed in source and manually verified by full read-
through (borrow-checker/type correctness), but NOT compiled or run via
cargo test in this session -- the environment's disk ran critically low
(shared machine, other concurrent sessions) and a from-scratch napi/
cargo build for this crate was not safe to attempt. The TS-side fix is
complete, tested (full suite green), and independently verified via a
controlled A/B swap against the pre-fix code reproducing the exact
divergence this fix resolves.

A separate, pre-existing bug was found and filed independently
(#1863): resolveByGlobal's receiver-less call
resolution matches every same-named candidate clearing a loose
directory-proximity confidence threshold and creates an edge to each of
them, rather than picking the best match. That bug reproduces on a
from-scratch full build too (not incremental-specific) and is out of
scope for this fix.

docs check acknowledged: internal correctness fix to incremental-build
edge reconnection; no CLI surface, language support, or documented
architecture/design decision changed.

Fixes #1752

Impact: 5 functions changed, 12 affected
…sis.pointsToMaxIterations

MAX_SOLVER_ITERATIONS was a hardcoded 50 in both the WASM points-to solver
(points-to.ts) and the native Rust solver (build_edges.rs), duplicating but
never reading DEFAULTS.analysis.pointsToMaxIterations. Threads a maxIterations
parameter from the pipeline's resolved config through buildPointsToMap ->
buildPointsToMapForFile -> buildCallEdgesJS/buildCallEdgesNative on the TS
side, and through build_call_edges -> process_file -> build_file_context ->
build_pts_map_for_file -> build_points_to_map on the Rust side, sourced from
a new BuildConfig.analysis.points_to_max_iterations field deserialized from
the JSON config payload already passed to the native engine. Default value
(50) is unchanged when no override is configured.

docs check acknowledged: no README/CLAUDE.md/ROADMAP.md updates needed —
purely internal plumbing for an already-documented, already-accepted config
key with no new CLI flags or user-facing surface.

Fixes #1753

Impact: 7 functions changed, 6 affected
generator.ts's embedding-progress messages now go through logger.info()
(unconditionally visible, matches the info()-based "Reusing previously-
stored embedding model" message already used in cli/commands/embed.ts)
instead of console.log/stdout. semantic.ts's dimension-mismatch message
is a genuine warning (same severity class as the file's existing
warnOnSimilarQueries), so both of its console.log lines are merged into
a single warn() call, following the fix already applied to prepare.ts.

cli-formatter.ts is the actual data-output layer for `codegraph search`
(including the --json contract), directly analogous to
presentation/queries-cli/ — CLI display wrappers for query functions
that already live in presentation/ and call console.log directly. No
other domain/ file has precedent for presentation code living there, so
it moves to presentation/search.ts rather than being kept as a
domain-layer exception. Only two import sites needed updating
(cli/commands/search.ts, the domain/search/index.ts barrel) plus one
test import.

docs check acknowledged: internal logging-layer refactor + file move,
no new feature/language/CLI/architecture surface — README.md, CLAUDE.md,
and ROADMAP.md do not need updates.

Fixes #1754

Impact: 2 functions changed, 9 affected
carlos-alm added 20 commits July 6, 2026 13:41
…ls (docs check acknowledged)

codegraph roles --role entry classified any exported, zero-fan-in node as
`entry` regardless of kind, so non-exported interfaces/constants (e.g.
ParsedUserConfig, WorkspaceEntry, _configCache in config.ts) were also
sweeping into the entry bucket via a separate bug: the barrel-reexport-chain
"exported" inference (#837) marks every symbol in a re-exported file as
exported, including symbols that were never independently exportable
(class/interface methods) and symbols without the `export` keyword.

Two fixes, mirrored in the TS classifier and the Rust native engine:

- classifyNodeRole / classify_node: the exported+zero-fan-in branch now
  requires kind IN ('function', 'method') to return `entry`. Every other
  exported kind (interface/type/constant/class) is classified `leaf` instead
  — a live, intentional part of the public surface, but not something
  invoked from outside the codebase. This also refines two #1583 cases
  (exported interface with same-file-only type usage) from `entry` to the
  more accurate `leaf`, while keeping their "never dead" guarantee intact.

- classifyNodeRolesFull / classifyNodeRolesIncremental (structure.ts) and
  do_classify_full / do_classify_incremental (roles.rs): exclude `method`
  from the barrel-reexport-chain exported-inference query. A `reexports`
  edge only ever concerns top-level module bindings, so a class method can
  never be an independently re-exportable binding — without this exclusion,
  abstract base-class methods (e.g. Repository.findNodeById) were promoted
  to `entry` merely because a sibling symbol in the same file was
  re-exported through a barrel.

Verified real entry points are unaffected: CLI command execute/validate
methods, MCP tool handlers, ESM loader hooks, and event:/route:/command:
framework-prefixed handlers all still classify as `entry`.

Symptom 2 from the same issue (dead-unresolved for LanguageRules interface
members in visitor-utils.ts) was already fully fixed by #1723's
isTypeDeclarationMember exemption — verified via fresh build, no changes
needed.

Fixes #1780

Impact: 3 functions changed, 4 affected
const { X, Y } = (await import('./mod.js')) as {...} — a dynamic
import() whose awaited result is wrapped in redundant parentheses
and/or a TypeScript `as` type assertion before being destructured —
never resolved to a `calls` edge. extractDynamicImportNames (TS) and
extract_dynamic_import_names (Rust) only skipped a single optional
await_expression before requiring the immediate parent to be a
variable_declarator, so the extra parenthesized_expression /
as_expression layers broke the walk-up and silently returned an empty
names list. Without names, importedNames never gained an entry for
the destructured bindings, so calls through them never resolved,
and `codegraph exports` reported the target functions as zero-consumer
dead exports despite being called from production code (e.g.
buildDataflowVerticesFromMap from native-orchestrator.ts).

Generalize the walk-up in both engines to skip any combination or
nesting of await_expression, parenthesized_expression, and
as_expression wrappers before checking for the enclosing
variable_declarator. Scoped to static string-literal import
specifiers, the only case that is statically resolvable at all;
computed/variable specifiers remain unresolvable by design.

Verified on both engines against the exact repro (codegraph exports
src/features/dataflow.ts -T --json now credits
buildDataflowVerticesFromMap with its real consumer) and against the
resolution-benchmark suite (no change: 206/206 tests, 63.8% aggregate
recall before and after).

No language/architecture/command surface changed (docs check acknowledged).

Fixes #1781

Impact: 1 functions changed, 8 affected
…docs check acknowledged)

codegraph complexity --file <f> --health -T --json returned functions:[]
for every Lua file even though symbol extraction (codegraph where) already
worked correctly. Root cause: Lua had zero entries in COMPLEXITY_RULES /
HALSTEAD_RULES (src/ast-analysis/rules/index.ts) and the native mirror
(lang_rules / halstead_rules in crates/codegraph-core/src/ast_analysis/
complexity.rs) — not a wrong node-type mapping, but a completely missing
per-language configuration on both engines, so the complexity visitor never
ran for Lua at all.

Adds complexityLua/halsteadLua (src/ast-analysis/rules/b3.ts, wired into
COMPLEXITY_RULES/HALSTEAD_RULES in rules/index.ts) and the matching
LUA_RULES/LUA_HALSTEAD (crates/codegraph-core/src/ast_analysis/
complexity.rs, wired into lang_rules/halstead_rules), derived from the real
tree-sitter-lua grammar shape (verified by parsing probe snippets with both
the WASM grammar and the native tree-sitter-lua crate): elseif_statement/
else_statement are flat siblings of if_statement via repeated `alternative:`
fields (same shape as Python's elif_clause/else_clause), and Lua's single
generic binary_expression node covers arithmetic/comparison/logical
operators alike, filtered by operator token. function_declaration matches
the same node type the existing extractor and dataflowLua rules already use,
so complexity results attach to the same definitions `where` reports. Also
adds a Lua entry to the LOC comment-prefix maps (`--`) on both engines,
which were defaulting to C-style prefixes and undercounting comment lines.

Verified against all 5 Lua files in the repo (tracer + 4 fixtures) via both
engines. Filed #1922 (WASM-only parity gap for files where every function
has a dot-qualified name, e.g. Lua's `M.foo` module pattern) and #1923
(the same missing-complexity-config gap affects 24 other languages) as
separate follow-ups rather than expanding this fix.

Fixes #1782
…s (docs check acknowledged)

The global-by-name call-resolution fallback (resolveByGlobal/resolveByReceiver
in strategy.ts, resolveReceiverEdge in call-resolver.ts, and their Rust
mirrors) filtered candidates purely by proximity-based confidence
(computeConfidence), with no language-consistency check at all. A bare-name
call with no import/receiver match could therefore resolve against a
same-named symbol in a completely unrelated language, as long as the two
files were proximate enough (e.g. same directory).

Concretely: ruby-tracer.rb's builtin `Kernel#load` call was falsely
attributed as a consumer of loader-hooks.mjs's unrelated `load` export,
because both files live in the same directory and computeConfidence scores
same-directory pairs at 0.7 — well above the resolver's 0.5 threshold —
with no check that a Ruby caller and a JS target aren't even in the same
language.

Adds isSameLanguageFamily()/is_same_language_family() to resolve.ts/resolve.rs,
derived from LANGUAGE_REGISTRY (TS) / LanguageKind::from_extension (Rust),
with JavaScript/TypeScript/TSX collapsed into one family since those
routinely call into each other within the same project. computeConfidence
now rejects cross-family candidates before scoring proximity, which
transitively hardens every fallback tier gated on it (resolveByGlobal's
exact-name lookup, resolveByReceiver's typed/prototype/direct-qualified/
composite-pts lookups, the same-class-sibling and accessor-this-dispatch
fallbacks, and the CHA/property-propagation post-passes). resolveReceiverEdge's
global fallback had no confidence gate at all, so it gets an explicit
isSameLanguageFamily filter instead.

Verified via the resolution-benchmark suite: precision/recall are unchanged
across all 40 single-language fixtures (expected, since none mix languages
within one directory); the fix is validated by the exact repro plus new
targeted unit tests covering both cross-language rejection and same-language
regression safety in both engines.

Fixes #1783

Impact: 4 functions changed, 30 affected
codegraph exports did not credit `instanceof ClassName` checks (or other
bare-reference, no-call-site usages) as consumers. CodegraphError showed
consumerCount:0 via `codegraph exports src/shared/errors.ts --json` despite
two real production usages (src/cli.ts, src/mcp/server.ts) that only ever
reference it via `err instanceof CodegraphError` — never `new
CodegraphError(...)`. Subclasses consumed via constructor calls were
correctly credited (ConfigError, consumerCount:23); only bare-reference
usages like instanceof were invisible to the consumer counter, so any
base/parent class whose primary cross-file use is instanceof narrowing
falsely presented as dead/unused.

Root cause: no edge at all was created for the right-hand operand of an
`instanceof` binary expression — the same "no edge for a bare-identifier
value reference" gap as #1771 (object-literal property values) and #1776
(Lua builtin reassignment), just at a different syntactic position.

Fix: both engines now extract a dynamic `calls` edge (dynamicKind/
dynamic_kind = 'value-ref') when instanceof's right operand is a bare
identifier, reusing the #1771/#1776 taxonomy entry rather than introducing
a new DynamicKind — ADR-002 explicitly anticipated this as "syntax-position-
agnostic" and invited a third extraction site to reuse it. Unlike the
function/method-only #1771/#1776 sites, the resolver-side kind filter for
value-ref now also accepts class-kind targets, since instanceof's operand is
always a class/constructor (never a plain data reference) — it still accepts
function-kind too, correctly covering the pre-ES6 "constructor function"
instanceof idiom (`x instanceof SomeFunction`).

Applied to both engines:
- WASM/TS: src/extractors/javascript.ts (collectInstanceofValueRefCall,
  wired into the shared runCollectorWalk's binary_expression case),
  src/domain/graph/builder/stages/build-edges.ts (resolveFallbackTargets
  kind filter extended to accept 'class'), src/domain/graph/builder/
  incremental.ts (mirrored filter for the incremental/watch-mode path).
- Native: crates/codegraph-core/src/extractors/javascript.rs
  (handle_instanceof_value_ref, wired into match_js_node's binary_expression
  arm — dispatches for JavaScript/TypeScript/Tsx alike), crates/
  codegraph-core/src/domain/graph/builder/stages/build_edges.rs (matching
  kind filter in process_file).
- src/types.ts / docs/architecture/decisions/002-dynamic-call-resolution.md:
  documented instanceof as the third value-ref extraction site and the
  class-kind target-set extension.

Verified against the exact repro on both engines: CodegraphError now shows
consumerCount:2 (src/cli.ts, createCallToolHandler in src/mcp/server.ts),
identical between native and WASM. codegraph roles --role dead does not (and
did not) flag the CodegraphError class itself — exported classes are never
classified dead by role, independent of this fix; the fix's measurable
effect is the consumer-count/fan-in correction. Total dead-symbol count
across the repo's own graph dropped from 816 to 810 as a side effect of
crediting other instanceof-only-referenced symbols codebase-wide.

Resolution-benchmark: no fixture exercises instanceof, so precision/recall
is byte-identical before and after (javascript 100/100, typescript
95.7/93.6, aggregate 63.8% recall) — confirmed empirically, not assumed.

Fixes #1784

docs check acknowledged — internal edge-emission/resolver bug fix, no new
commands, languages, or architecture to document.

Impact: 5 functions changed, 17 affected
…on inputs

anthropics/claude-code-action v1 (pinned at 558b1d6cab4085c7753fe402c10bef0fbb92ac7a)
no longer accepts `model` or `direct_prompt` as inputs, so both jobs' overrides
were being silently ignored on every run (warning annotations only, no CI failure).

- `direct_prompt` -> `prompt` (documented drop-in rename)
- `model` -> `claude_args: --model <id>` (model selection moved to the Claude CLI
  arguments; there is no dedicated top-level input anymore)

Fixes #1802
…l sites

outputResult took data: Record<string, any>, whose any-typed index signature
already made TypeScript skip the "index signature is missing" check for
plain interfaces -- so callers never actually needed the defensive
`as unknown as Record<string, unknown>` cast; they just carried it forward
as noise. Widen the parameter to `object` (outputResult only serializes/
formats data and never returns or re-exposes its shape, so a generic isn't
needed) and move the one necessary cast to Record<string, unknown> inside
outputResult itself, where it's actually justified by printNdjson/printCsv/
printAutoTable needing key access for NDJSON field plucking and CSV/table
flattening. Removes the cast from all 22 current call sites across
audit.ts, triage.ts, owners.ts, and queries-cli/{impact,path,exports,
inspect,overview}.ts.

docs check acknowledged: internal type-signature cleanup only, no new
features/languages/architecture/CLI surface -- README.md, CLAUDE.md, and
ROADMAP.md do not need updates.

Fixes #1803

Impact: 23 functions changed, 79 affected
…detection parity

`codegraph communities`/`--drift` ran genuinely different algorithms
depending on which engine was active: the native path ran classic Louvain
(crates/codegraph-core/src/graph/algorithms/louvain.rs, undirected
modularity optimization), while the JS/WASM fallback ran Leiden
(src/graph/algorithms/leiden/*, hardened by #1734/#1755/#1770 earlier in
this batch). Louvain and Leiden are related but distinct algorithms with
different guarantees (Leiden avoids Louvain's disconnected-community
defect), so results diverged purely based on whether the native addon
loaded, independent of any change to the analyzed codebase.

Ported Leiden to Rust (crates/codegraph-core/src/graph/algorithms/leiden.rs,
~950 lines) as a faithful line-by-line translation of the TS reference:
graph adapter with undirected symmetrization (averaging reciprocal edge
weights, not summing them -- a second, independent divergence the old
Louvain implementation had), partition with incremental aggregate deltas,
modularity quality/diff functions, greedy local-move phase, true Leiden
refinement (singleton start, singleton guard, Boltzmann probabilistic
selection), post-refinement disconnected-community splitting, and
multi-level coarsening. Also ported a mulberry32 PRNG matching the TS
reference's exact bit-pattern arithmetic (u32 wrapping ops), since the
refinement phase's random draws must stay in lockstep with the JS engine
bit-for-bit.

Scope: covers exactly the option surface `louvainCommunities`/
`LouvainOptions` (src/graph/algorithms/louvain.ts) ever exercises --
undirected, modularity-only, "neighbors" candidate strategy, refine always
on, uniform edge weight/node size. The TS reference's directed mode, CPM
quality, alternate candidate strategies, allowNewCommunity, fixedNodes, and
preserveLabels are not reachable from this binding and are not ported; see
leiden.rs's module doc and follow-up issue #1936.

Determinism (per #1734's exact failure mode): every place the TS reference
relies on Map/Array *insertion order* for tie-breaking, this port uses an
insertion-order-preserving Vec + HashMap-as-lookup-only pattern rather than
a BTreeMap -- a BTreeMap would be deterministic but *sorted*, which does
not match the JS engine's insertion order and would silently reintroduce
cross-engine divergence.

Retired the old classic-Louvain louvain.rs entirely (confirmed via
repo-wide grep that its only consumer was the mod.rs re-export feeding this
one napi binding) and renamed the native binding louvainCommunities ->
leidenCommunities (types.ts's NativeAddon.leidenCommunities is marked
optional so older published native packages that predate this rename
correctly fall back to the JS engine instead of silently running the old,
now-removed algorithm). Updated the misleading "native: Louvain, JS:
Leiden" doc comments in louvain.ts, config.ts, and the communities CLI/MCP
descriptions that documented the mismatch as intentional.

Verification:
- Before fix: this repo's own dependency graph via native vs JS differed
  in both community count and modularity at file level (322 vs 335
  communities, Q=0.566 vs 0.536) and function level (5540 vs 6072
  communities, Q=0.850 vs 0.655), confirming the bug was real.
- First native Leiden port attempt still diverged from TS beyond level 0
  of the multi-level pipeline; root-caused to the coarse-graph builder
  reusing raw first-seen-edge order instead of replicating the TS
  reference's two-stage process (building a synthetic ascending-id
  CodeGraph, then re-reading it through the undirected dedup generator,
  which reorders each community's adjacency list). Fixed by making
  build_coarse_graph replicate both stages exactly.
- After fix: native and JS produce byte-identical assignments and
  modularity (including exact community-id labels, not just equivalent
  groupings) across 40 seed/resolution/knob combinations on this repo's
  own file- and function-level graphs, 72 combinations across 8 other
  fixture projects (multiple languages, this repo's own Rust crate at
  6072/13062 nodes/edges), and a suite of hand-built/synthetic graphs
  specifically covering reciprocal edges, self-loops, and forced
  multi-level coarsening.
- Determinism re-verified for the new implementation: 12 separate process
  invocations plus 2 full clean rebuilds (3 independently compiled
  binaries total) all produce byte-identical (SHA-256-matching) output on
  this repo's own graph.
- npm test: 223 files, 3665 passed, 0 failed, 30 skipped, 2 todo.
- npm run lint: clean.
- cargo test: 513 passed, 0 failed. cargo clippy: leiden.rs clean.
- Resolution-benchmark suite: 206 passed (community detection isn't part
  of it, but confirms nothing else regressed).

Fixes #1804

Impact: 2 functions changed, 12 affected
…ignatureChanges

checkNoSignatureChanges could never catch a file deleted in its entirety:
parseDiffOutput discarded a deleted file's `+++ /dev/null` hunk entirely
(no changedRanges entry to compare against), and the graph builder purges
the file's nodes rows on the very next rebuild, so the DB-driven lookup
always returned zero rows for it. Deleting a file whose exports still
have external callers elsewhere in the codebase silently passed the
signature-change gate.

parseDiffOutput now tracks fully deleted files (via the `--- a/<file>` /
`+++ /dev/null` header pair) in a new `deletedFiles` set. A new sibling
predicate, checkNoDeletedExportsInUse, queries the current DB for each
deleted file's exported function/method/class declarations and flags any
that still have a real external (cross-file) consumer — callers that are
themselves among the files this same diff deletes don't count, mirroring
checkNoSignatureChanges's own exported-only rationale. Unlike
checkNoSignatureChanges (which flags any touched exported declaration
regardless of caller count), this predicate only flags when an actual
external consumer exists, so deleting genuinely dead files is unaffected.

Both predicates report under the same 'signatures' name/flag, so existing
consumers (the pre-commit hook, `codegraph check --json`, titan-gate) pick
up the new violations with no wiring changes. checkData's early-return
guard now also accounts for delete-only diffs, which never populate
changedRanges.

Scope: this closes the gap for the common case where `codegraph check`
runs before any rebuild has purged the deleted file's rows (e.g. this
repo's own pre-commit hook, which checks staged changes without
rebuilding first). Once some other `codegraph build` invocation purges
the rows, this predicate has nothing left to find — same as before this
predicate existed, not a regression. Follow-up #1938 tracks a durable,
purge-order-independent fix.

Fixes #1806

docs check acknowledged: no new CLI flag/command was added (the
`--signatures` flag and 'signatures' predicate already existed and are
not individually documented in README/CLAUDE.md today), no new language
support, no architecture-table-level change.

Impact: 19 functions changed, 7 affected
computeComplexitySummary previously queried all function_complexity
rows filtered only by noTests, ignoring the file/target/kind WHERE
clause used to scope the `functions` list. This made `codegraph
complexity --file X` report whole-repo summary stats alongside a
correctly-filtered functions table.

Thread the same where/params built by buildComplexityWhere into the
summary query so it reflects the same scope as `functions`. The
above-threshold HAVING clause is intentionally excluded from the
summary query so stats like aboveWarn stay meaningful against the
full in-scope population rather than collapsing to a tautology.

Impact: 3 functions changed, 5 affected
…ind 'property'

extractInterfaceMethods (JS/TS, shared by both the walk and query extraction
paths) and its Rust mirror extract_interface_methods unconditionally emitted
kind: 'method' for every interface/type-literal member, even plain data
properties with no parameters or body. Branch on the tree-sitter node type
instead: method_signature -> 'method', property_signature -> 'property'.

Fixing the mislabeling exposed a latent, mirrored bug in both engines' role
classifiers: classifyNodeRolesFull/Incremental (TS) and do_classify_full/
do_classify_incremental (Rust) fast-path every kind = 'property' node straight
to 'dead-leaf', bypassing the isTypeDeclarationMember/is_type_declaration_member
check that correctly recognizes interface/type members as never-dead 'leaf'
nodes (#1723). That fast path was only ever safe because property-signature
interface members never actually had kind 'property' in practice. Both engines
now partition property rows into genuine dead-leaf fields vs interface/type
members before classifying, so property-kind interface members land on 'leaf'
just like method-kind ones always have.

Impact: 7 functions changed, 16 affected
…assification

`codegraph roles --role dead` force-assigned every `kind: 'property'` node
(real class/struct/object fields) to `dead-leaf` unconditionally, regardless
of whether the field was read/written elsewhere in its owning class. A
property never produces inbound `calls` edges by construction, so this
heuristic guaranteed every field looked dead whether or not it was used --
the same category error #1723 fixed for `parameter`-kind nodes.

Codegraph has no property-access/write edge tracking to answer the real
question (is this field read/written anywhere in its class), so a field's
liveness carries zero dead-code signal from call-graph reachability alone.
Genuine (non-interface) properties now get no role at all -- the same
treatment `parameter` already receives -- while interface/type
property-signature members (#1809) continue to classify as `leaf`.

Applied to both the TS/WASM classifier (features/structure.ts) and the
native Rust classifier (graph/classifiers/roles.rs) per the dual-engine
parity requirement; verified both engines produce identical role
distributions end-to-end against this repo's own graph (216 properties ->
null, 3765 interface members -> leaf, on both engines).

Fixes #1810

Impact: 4 functions changed, 7 affected
…guage

extends/implements heritage-clause resolution matched a bare type name
against every node in the graph with that name, filtered only by kind —
with no file or language scoping. Common type names (Repository, User,
etc.) produced false cross-file, even cross-language, hierarchy edges
whenever an unrelated declaration happened to share the name.

Add resolveHierarchyTargets (shared by build-edges.ts and incremental.ts,
mirrored in the native build_edges.rs emit_hierarchy_edges), following the
same priority order already used for receiver-edge resolution:
same-file declaration, then the file's actually-resolved import, and only
as a last resort a same-language-family global-by-name match (never
cross-language, matching the #1783 precedent).

Fixes #1812

Impact: 19 functions changed, 12 affected
`import { value, type Foo } from 'mod'` marks only Foo as type-only via
an inline modifier, distinct from a whole-statement `import type { X }`.
Both engines derived `typeOnly` per import statement, so a mixed
statement's type-only names got no symbol-level `imports-type` edge and
were undercounted as export consumers.

Extends `Import` with a sparse `typeOnlyNames` field (mirrors the
`renamedImports` convention) populated from the per-specifier `type`/
`typeof` token in the tree-sitter-typescript grammar. Propagates through
the WASM/TS extractor (both query-dispatch and walk paths), the native
Rust extractor, and both edge-building paths (JS full-build/incremental
and native FFI/pure-native pipelines) so `emitNamedSymbolEdges` credits
exactly the flagged names instead of gating on whole-statement typeOnly.

Impact: 19 functions changed, 38 affected
triage, and the unwired findNodesByScope/findNodeByQualifiedName/
listFunctionNodes/iterateFunctionNodes native bindings, forwarded the
CLI's repeatable -f/--file array straight into rusqlite bindings typed
for a single Rust String. triage silently swallowed the resulting
napi-rs conversion error into an empty result at exit code 0; the
others would crash the same way once wired up.

Widen the five NativeDatabase bindings (Rust + TS declarations) to
Option<Vec<String>>, reusing the push_file_filter() helper already
shared by find_nodes_with_fan_in/fn_deps, and forward the full
normalized array from NativeRepository instead of truncating to the
first value. Also narrow triageData's catch to only swallow the
validated-input ConfigError case so a genuine internal failure
propagates instead of masquerading as "no symbols match".

Impact: 24 functions changed, 46 affected
The JS/TS extractor's get_first_call_arg helper never used its source
parameter — it only needs node structure (child kinds) to locate the
first non-punctuation call argument, matching the WASM/TS mirror
(getFirstCallArg in src/extractors/javascript.ts), which never took a
source parameter at all. Removed it and updated the three call sites
(eval() key_expr capture, Reflect.apply, Reflect.construct), silencing
the dead-parameter compiler warning.

Added native unit tests covering all three call sites so a future
change to this helper's signature or behavior is caught by cargo test.
…al methods

For `const`/`let`/`var obj = { method() {} }`, both engines emitted a bare
`method` (kind: method) and a qualified `obj.method` (kind: function)
definition, but from different, independently-timed extraction passes — one
inline with the object's own declaration, the other from a separate
generic/deferred pass. That made native and WASM disagree on the relative
position of the two entries in the `definitions` array whenever other
top-level statements sat between the declaration and end of file.

Consolidate both entries into a single call site on each engine
(extractObjectLiteralFunctions / extract_object_literal_functions), emitted
inline at the object literal's own declaration, and skip the generic
method_definition handlers for exactly these nodes. Native's now-redundant
deferred `match_js_objlit_qualified_method_defs` pass is removed entirely,
folding its responsibilities (including previously let/var-only pair
handling) into the const branch's existing inline call plus a new let/var
branch in handle_var_decl. A class-static-block nested edge case (where the
generic handler legitimately produces a class-qualified name instead of a
bare one) is preserved unchanged via an enclosing-class check.
…itializer shape

Both handleVariableDeclarator (WASM/TS walk path) and extractConstDeclarators
(WASM/TS query path), plus their Rust mirror handle_var_decl, only recognized
a fixed whitelist of "literal-shaped" initializers (number/string/array/
object/new-expression/etc) for top-level `const` declarations. Any other
shape -- call expressions, member expressions, parenthesized expressions,
plain identifier aliases -- silently produced no Definition at all, making
the symbol invisible to every downstream query (where --file, exports,
dead-code audits, codegraph query).

Broaden the recognition to match how function declarations are already
captured regardless of body complexity: any non-function-valued top-level
`const` (or `const [a, b] = ...` array-pattern) now becomes a "constant"
Definition, independent of initializer shape. collectExportedDeclarations
and its Rust mirror collect_exported_var_declarations get the identical
change so `export const x = <anything>` is marked exported consistently
with the new Definition (the two must stay in sync, per #1728).

The Rust walk previously conflated Definition creation with pts
fn_ref_binding collection (const alias = handler) in a single mutually
exclusive if/else chain, so widening the constant branch would have
silently dropped fn_ref_binding tracking for const aliases. Decoupled the
two concerns to mirror the WASM/TS extractor, which already runs
fnRefBindings collection independently of Definition creation.

Fixes #1819
tree-sitter-php's primitive_type production (scalar type-hints like
`string $x` and `: string` return types) and cast_type production
(`(string) $x`) both lex the `string` keyword as an anonymous grammar
token whose type string is identical to the named `string` literal
node type -- the same collision as TypeScript's predefined_type (#1729).
Without a named-node guard, `codegraph ast --kind string` misclassified
these keyword occurrences as string literals.

Fixes both engines:
- Native: add `requires_named_node` to `LangAstConfig`, checked in
  `classify_ast_node` via `node.is_named()`; set true only for PHP.
- WASM/TS: extend `astRequiresNamedNode()` (already scoped to JS/TS/TSX
  from #1729) to also cover `php`, reusing the existing
  `resolveAstKind()` guard threaded through all three production call
  sites.

Verified non-tautological: reverting each side's guard and rebuilding
reproduces the false positives (7 rows instead of 3) that the new tests
catch, on both the WASM and native paths.

Impact: 1 functions changed, 9 affected
extractImportNames's export_specifier branch only ever recorded the
declaration name (X), never the external alias (Y) a consumer of the
barrel actually imports. resolveBarrelExport/resolve_barrel_export
therefore had no way to translate a consumer's requested name (Y) back
to X, so calls resolved through a renamed barrel re-export were
silently dropped.

Extends Import.renamedImports (added for #1730) to export_specifier
nodes and threads the rename table through barrel resolution in both
engines: resolveBarrelExport/resolve_barrel_export now return the
declared name alongside the resolved file, and every caller (import
edges, call-edge resolution, native orchestrator) uses that name
instead of the originally-requested one.

codegraph watch's separate DB-only incremental rebuild path can't
recover a barrel's rename table when the barrel itself isn't part of
the current watch batch -- filed as #1967, out of scope here since
standard codegraph build (including hash-based incremental skip) is
unaffected.

Closes #1823

Impact: 28 functions changed, 59 affected
@greptile-apps

greptile-apps Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes a gap in barrel file re-export handling where export { X as Y } from '...' renames were never tracked, causing resolution to silently fail for any consumer that imported the barrel's external alias Y. The fix is consistently applied across both the TypeScript (WASM) and Rust (native) engines.

  • Extractor fix: extractImportNames / scan_import_names_depth now records { local: Y, imported: X } rename pairs for export_specifier nodes, populating Import.renamedImports on barrel re-export entries.
  • Resolver fix: resolveBarrelExport / resolve_barrel_export return { file, name } instead of a bare file path; a new translateThroughRename helper maps a consumer's external alias back to the declared name before matching re.names or checking definitions.
  • Callers updated across build-edges.ts, build_edges.rs, import_edges.rs, pipeline.rs, incremental.ts, call-resolver.ts, and strategy.ts; the codegraph watch limitation is documented and tracked in codegraph watch does not resolve barrel re-export renames (export { X as Y } from ...) on incremental rebuild #1967.

Confidence Score: 5/5

Safe to merge — the fix is correctly mirrored across both engines, all callers are updated consistently, and multi-hop rename chains work by natural recursion.

The change is a well-scoped correctness fix touching only the barrel/reexport resolution machinery. Both the extraction (recording rename pairs) and the resolution (translating consumer names before matching) sides are updated symmetrically in TS and Rust. Existing passing tests confirm no regressions, and new parameterized integration tests validate the fixed behavior end-to-end on both engines.

No files require special attention — the changes are internally consistent across all 18 touched files.

Important Files Changed

Filename Overview
crates/codegraph-core/src/domain/graph/builder/barrel_resolution.rs Introduces BarrelResolution {file, name} return type and translate_through_rename helper; multi-hop rename chains verified correct in all cases with comprehensive new Rust unit test.
src/domain/graph/builder/stages/resolve-imports.ts TS mirror of the Rust fix — adds BarrelExportResolution interface, translateThroughRename, and threads renames from Import.renamedImports into the reexport map; cache key correctly uses the external (consumer-facing) name.
src/extractors/javascript.ts extractImportNames gains optional renamedOut param; export_specifier branch now records {local:alias, imported:name} rename pairs while keeping names[] on the original declared name — both handleExportCapture and handleExportStmt callers updated.
crates/codegraph-core/src/extractors/javascript.rs Rust mirror of JS extractor fix — handle_reexport calls extract_import_names_with_renames and populates renamed_imports on the Import; export_specifier branch captures alias/name pairs correctly.
src/domain/graph/builder/stages/build-edges.ts All emitNamedSymbolEdges / buildBarrelEdges / buildImportedNamesMap / buildImportedNamesForNative callers updated to use resolved.name for symbol lookup and resolved.file for file routing.
crates/codegraph-core/src/domain/graph/builder/stages/build_edges.rs symbol_node_map changed from &'a str to owned String keys to accommodate freshly-resolved barrel-rename names; all emit_ callers updated to use BarrelResolution.
src/domain/graph/builder/incremental.ts resolveBarrelTarget updated to return {file, name} shape matching CallNodeLookup contract; known watch-mode limitation clearly documented referencing #1967.
crates/codegraph-core/src/domain/graph/builder/pipeline.rs collect_imported_names_for_file and compute_edge_relevant_files updated; ImportedName.imported now correctly set to target_name rather than original.
tests/integration/issue-1823-barrel-rename-reexport.test.ts Parameterized over both engines; verifies correct call edge, absence of spurious friendlyName edge, and consumer credit in exports query.
tests/parsers/javascript.test.ts Old no-rename-tracking test replaced with two new tests covering renamed and non-renamed export_specifier cases.

Reviews (3): Last reviewed commit: "fix: resolve merge conflicts with main (..." | Re-trigger Greptile

Comment on lines 1081 to 1085
const traceBarrel = (resolvedPath: string, cleanName: string): string => {
if (!isBarrelFile(ctx, resolvedPath)) return resolvedPath;
const actual = resolveBarrelExportCached(ctx, resolvedPath, cleanName);
return actual ?? resolvedPath;
const resolved = resolveBarrelExportCached(ctx, resolvedPath, cleanName);
return resolved?.file ?? resolvedPath;
};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 CJS barrel rename: translated name is silently dropped

buildImportArtifactNames resolves through barrel chains but only keeps resolved.file; the translated declared name (resolved.name, e.g. realName) is discarded. For a CJS require through a renamed barrel (export { realName as friendlyName } from …), combined ends up with friendlyName → underlying.ts. Downstream call-edge resolution then looks for a node named friendlyName in underlying.ts, which doesn't exist, so the edge is silently dropped. importedOriginalNames for CJS bindings has no corresponding rename entry either, so the fallback targetName = call.name = "friendlyName" path also fails.

The function comment notes it doesn't affect call resolution or DB edge creation (#1661), limiting the impact to importArtifactNames consumers, but it is a divergence from the ESM path's handling that is now fixed for the same scenario.

Fix in Claude Code

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch on the asymmetry — traceBarrel here does drop resolved.name while the ESM path's own traceBarrel (buildImportedNamesMap, same file) correctly threads it through as part of this PR. I traced the actual impact before fixing though: the sole consumer of this map is resolveReceiverEdge (call-resolver.ts), called from build-edges.ts:1909 and incremental.ts:765, and it only calls importedNames?.has(effectiveReceiver) — a key-presence check that never reads the map's value. The map's key (the CJS-destructured local binding, e.g. friendlyName) is set correctly regardless of this bug, so no edge is actually silently dropped today — the local-definition-vs-import-artifact classification this map exists for is unaffected. There's also no native-engine equivalent of this function to diverge from.

Given the value is provably unread today, and a proper fix cascades into resolveReceiverEdge's shared ReadonlyMap<string, string> parameter type (also used by the ESM-only importedNames map, which stays plain file strings) — fixing it fully means either a second parallel map or a tuple-typed one shared by both callers, more invasive than warranted for a value nothing currently reads. Filed as #2071 to track properly rather than leaving it unrecorded, since it would silently misbehave the moment this map's value gains a real consumer.

@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Codegraph Impact Analysis

31 functions changed58 callers affected across 9 files

  • CallNodeLookup.resolveBarrel in src/domain/graph/builder/call-resolver.ts:36 (10 transitive callers)
  • resolveCallTargets in src/domain/graph/builder/call-resolver.ts:264 (13 transitive callers)
  • PipelineContext in src/domain/graph/builder/context.ts:22 (4 transitive callers)
  • resolveBarrelTarget in src/domain/graph/builder/incremental.ts:325 (13 transitive callers)
  • resolveBarrelImportEdges in src/domain/graph/builder/incremental.ts:357 (7 transitive callers)
  • emitNamedSymbolEdges in src/domain/graph/builder/incremental.ts:395 (4 transitive callers)
  • buildImportedNamesMap in src/domain/graph/builder/incremental.ts:497 (5 transitive callers)
  • emitNamedSymbolEdges in src/domain/graph/builder/stages/build-edges.ts:196 (3 transitive callers)
  • buildBarrelEdges in src/domain/graph/builder/stages/build-edges.ts:283 (3 transitive callers)
  • buildImportedNamesForNative in src/domain/graph/builder/stages/build-edges.ts:859 (3 transitive callers)
  • addImports in src/domain/graph/builder/stages/build-edges.ts:869 (3 transitive callers)
  • buildImportedNamesMap in src/domain/graph/builder/stages/build-edges.ts:1009 (7 transitive callers)
  • traceBarrel in src/domain/graph/builder/stages/build-edges.ts:1020 (7 transitive callers)
  • addImportNames in src/domain/graph/builder/stages/build-edges.ts:1028 (6 transitive callers)
  • buildImportArtifactNames in src/domain/graph/builder/stages/build-edges.ts:1059 (3 transitive callers)
  • traceBarrel in src/domain/graph/builder/stages/build-edges.ts:1068 (7 transitive callers)
  • ReexportEntry.renames in src/domain/graph/builder/stages/resolve-imports.ts:21 (0 transitive callers)
  • buildReexportMap in src/domain/graph/builder/stages/resolve-imports.ts:25 (3 transitive callers)
  • reparseBarrelFiles in src/domain/graph/builder/stages/resolve-imports.ts:128 (3 transitive callers)
  • BarrelExportResolution.file in src/domain/graph/builder/stages/resolve-imports.ts:184 (0 transitive callers)

Base automatically changed from fix/issue-1821-codegraph-ast-kind-string-misclassifies-php to main July 9, 2026 05:53
Owned scope confirmed via this PR's own single commit diff-stat (18
files): pipeline.rs, build_edges.rs, import_edges.rs, javascript.rs
(native), call-resolver.ts, context.ts, incremental.ts, build-edges.ts,
resolve-imports.ts, strategy.ts, javascript.ts, types.ts, the barrel
rename fixture files, the new integration test, and javascript.test.ts.
Of these, only 10 had actual conflicts to resolve; the rest (including
barrel_resolution.rs and resolve-imports.ts, this PR's own signature
change for resolve_barrel_export/resolveBarrelExport) merged cleanly.

Two files hit the same silent-duplicate-function pattern seen earlier
this session on incremental.ts and now recurring on
build-edges.ts (TS): this PR's branch still had a local
importNamePairs helper that main had already deleted in favor of a
shared import from import-utils.ts/import-utils.js, and git's merge
kept HEAD's copy with zero conflict markers since the deletion and this
PR's own untouched surrounding doc comments landed in adjacent, not
overlapping, diff regions. Deleted the stale local copies (verified the
shared version is byte-identical, and that no other call site still
referenced the bare original) rather than leaving a redeclaration
error behind.

The remaining conflicts split cleanly into two categories:
- This PR's own resolveBarrelExport/resolve_barrel_export signature
  change (string -> { file, name }) threaded through every caller
  (import edges, call-edge resolution, native orchestrator, both
  engines) -- resolved by taking HEAD's side throughout.
- Unrelated stack noise already merged into main since this branch
  forked: the #1957 importedOriginalNames hierarchy-edge threading,
  the #1849 reexports-wildcard marker, the #1963/#1964
  is_eligible_object_literal_declarator/VAR_DECL_FN_SCOPE_TYPES
  dedup, and the recurring satisfies_expression/CALLBACK_ACCEPTING_CALLEES/
  Array.from stack-noise blocks already resolved in prior PRs this
  session -- resolved by taking main's side throughout.

All ten conflicted files verified byte-exact against this PR's own
diff (matching added-line counts) after resolution; the eight
cleanly-merged files spot-checked the same way. Native
rebuilt+codesigned+checksum-verified, full suite green (226 files,
3765 tests, 0 failures).

Impact: 53 functions changed, 185 affected

Impact: 53 functions changed, 185 affected
@carlos-alm

Copy link
Copy Markdown
Contributor Author

@greptileai

@carlos-alm carlos-alm merged commit 8470a01 into main Jul 9, 2026
48 of 50 checks passed
@carlos-alm carlos-alm deleted the fix/issue-1823-barrel-re-export-with-rename-export-x-as-y branch July 9, 2026 07:05
@github-actions github-actions Bot locked and limited conversation to collaborators Jul 9, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Barrel re-export with rename (export { X as Y } from ...) is not tracked, breaking barrel resolution

1 participant