fix: track inline per-specifier type-only import modifier#1958
fix: track inline per-specifier type-only import modifier#1958carlos-alm wants to merge 67 commits into
Conversation
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
…Insert chunk loop The technique/confidence backfill loop in applyEdgeTechniquesAfterNativeInsert re-prepared its two UPDATE statements on every chunk iteration instead of caching by chunk size. Apply the shared getOrCreatePerDbChunkStmt primitive (src/shared/chunked-stmt-cache.ts) with a persistent per-db WeakMap cache, matching the batchInsertEdges/batchInsertNodes caches in builder/helpers.ts — this function can run twice within a single buildEdges() call against the same db (once from insertNativeBulkEdges, once from reconnectReverseDepEdges), so a fresh per-call cache would still miss across that second invocation. Pure performance refactor: SQL text and behavior are unchanged. Fixes #1768 Impact: 1 functions changed, 4 affected
…tance (docs check acknowledged) computeConfidence (and its Rust mirror compute_confidence) scored call-edge resolution confidence using a fixed-depth check: same-directory, or dirname(dirname(caller)) === dirname(dirname(target)) for a "sibling directory" tier. That equality only holds when both files sit at the same depth, so a file in a subdirectory calling a method on a class declared in its direct parent directory (e.g. graph/algorithms/bfs.ts calling a method on the CodeGraph class in graph/model.ts) was scored as maximally distant (0.3) even though the receiver's type had already been correctly resolved via typeMap (from the parameter's `graph: CodeGraph` type annotation). That score fell below the 0.5 threshold used by resolveViaTypedMethod, silently dropping the call edge. Replaced both fixed-depth checks with a symmetric, depth-independent directory-tree distance (hops to the nearest common ancestor), preserving the existing same-directory (0.7) and sibling (0.5) tiers exactly and adding a new tier for direct parent/child directory nesting (0.6). Verified via the resolution-benchmark suite before/after the fix: all 42 language fixtures produce byte-identical precision/recall/TP/FP/FN numbers (aggregate recall 63.8%, 355/556 edges) — no regression. Fixes #1769 Impact: 3 functions changed, 26 affected
…thods optimiser.ts's computeQualityGain (the only caller of CPM/modularity delta computation in the Leiden hot loop) calls the standalone diffCPM/diffModularity functions directly — it never calls partition.deltaCPM(...), partition.deltaModularityDirected(...), or partition.deltaModularityUndirected(...). Confirmed via repo-wide grep that these three Partition interface methods, plus getCandidateCommunityCount, have zero callers in src/, tests/, or scripts, and that Partition/makePartition are not part of the public API (src/index.ts). Verified dead empirically, not just statically: replaced each of the four functions' bodies with a throw and ran the full test suite (3549 tests) — all passed, proving no code path anywhere reaches them. This also revealed that the "directed modularity delta — exact regression" tests added by #1755 (pinning computeDeltaModularityDirected's assumed output) actually exercise a different function — diffModularityDirected in modularity.ts, reached via computeQualityGain — since detectClusters never calls the Partition interface method. Corrected that test's comment accordingly instead of leaving it pointing at a deleted function. Removed: - The four dead Partition interface methods (deltaCPM, deltaModularityDirected, deltaModularityUndirected, getCandidateCommunityCount) and their backing implementations (computeDeltaCPM, computeDeltaModularityDirected, computeDeltaModularityUndirected, and the CPM/directed-term helper functions extracted in #1755/phase-22 that existed solely to serve them). - fgetOrZero (typed-array-helpers.ts), which becomes fully unreachable once computeDeltaModularityDirected (its only caller) is gone — it was extracted in #1755 specifically for that function. Removed its direct unit-test block along with it. Chose removal over wiring computeQualityGain to the Partition methods (deduplicating diffCPM/diffModularity vs. computeDeltaCPM/computeDeltaModularityDirected/ computeDeltaModularityUndirected) because the two implementations read through different data-access layers (raw PartitionState typed arrays vs. the PartitionView getter interface) — swapping which one runs in the Leiden hot loop would need rigorous before/after numerical verification on real data to rule out subtle divergence, on a file with a track record of exactly that kind of bug (#1734, #1755). Removal of genuinely unreachable code carries none of that risk. Verified no behavioral change: built this repo's own dependency graph (920 files / 19285 function-level nodes) and ran detectClusters directly (bypassing louvainCommunities' native-Rust preference to exercise the changed JS path) at file- and function-level granularity, both modularity and CPM quality functions, 3 resolutions x 3 random seeds (36 combinations) — before/after membership + quality() output is byte-for-byte identical (matching SHA-256). Also confirmed `codegraph communities -T --json` and `npm run typecheck`/ `npm run lint`/`npm test` (216 files, 3545 passed, 30 skipped, 2 todo) are unaffected. Filed #1895 to track codegraph's own dead-code detector blind spot (an object-literal-property-value reference counts as liveness without checking whether the resulting property is ever invoked) — out of scope to fix here. diff-impact --staged: 1 function changed (makePartition) -> 4 transitive callers across 2 files, all of which only use surviving Partition methods. Fixes #1770 docs check acknowledged Impact: 1 functions changed, 4 affected
…eral values
codegraph roles --role dead inconsistently flagged dispatch-table handler
functions (`{ matches, resolve: someFunction }`-style arrays) as dead,
depending on an unrelated, incidental property of the referenced function:
whether it happened to call another tracked symbol internally (fanOut > 0).
Root cause: codegraph created no edge at all for a bare function identifier
used as an object-literal property VALUE. The role classifier's
`node.kind === 'function' && node.fanOut > 0` heuristic in
classifyUnreferencedNode incidentally rescued whichever handlers happened to
have nonzero fanOut, leaving the rest misclassified dead-unresolved — a
false positive that risks deleting live dispatch-table handlers during
dead-code cleanup.
Fix: both engines now extract a dynamic `calls` edge (dynamicKind /
dynamic_kind = 'value-ref') for every bare-identifier object-literal
property value and shorthand property (`{ resolve: fn }` / `{ fn }`),
attributed to the enclosing scope via the existing findCaller machinery
(falls back to the widest enclosing constant/variable binding for top-level
dispatch tables, same as any other call). Resolution is restricted to
function/method-kind targets only, so plain data references
(`{ name: SOME_CONSTANT }`) are silently dropped rather than fabricating
nonsensical edges to constants.
Reuses the existing `calls` edge kind + dynamic flag (per ADR-002's "no new
edge kind" precedent, and the existing extractCallbackReferenceCalls
mechanism from #1741) rather than inventing a new edge kind — fan-in/fan-out
(scoped to `kind IN ('calls','imports-type')` / `kind = 'calls'`) already
account for it with zero changes to structure.ts/roles.rs's median
computations.
The `fanOut > 0` heuristic in classifyUnreferencedNode is kept, narrowed by
comment: it's no longer load-bearing for the object-literal case but remains
the only rescue for sibling value-reference patterns not yet extracted as
edges (logical-or fallback defaults, ternary defaults, array-of-functions
elements).
Applied to both engines:
- WASM/TS: src/extractors/javascript.ts (collectObjectLiteralValueRefCall,
wired into the shared runCollectorWalk used by both the walk and query
extraction paths), src/domain/graph/builder/stages/build-edges.ts
(resolveFallbackTargets kind filter), src/domain/graph/builder/incremental.ts
(mirrored filter for the incremental/watch-mode path), src/types.ts (new
'value-ref' DynamicKind variant).
- Native: crates/codegraph-core/src/extractors/javascript.rs
(handle_object_literal_pair_value_ref /
handle_object_literal_shorthand_value_ref, wired into match_js_node),
crates/codegraph-core/src/domain/graph/builder/stages/build_edges.rs
(matching kind filter in process_file).
Verified against the exact repro (this repo's own PARAM_NODE_HANDLERS
dispatch table in src/ast-analysis/visitor-utils.ts): all 5 handlers now
show totalDependents=1 and role=core on both engines, with zero dead-flagged
symbols in that file. Resolution-benchmark precision/recall is unchanged
across all 34 language fixtures (javascript 100/100, typescript 95.7/93.6,
aggregate 63.8% recall, identical before and after).
Fixes #1771
docs check acknowledged — internal edge-emission/resolver bug fix, no new
commands, languages, or architecture to document.
Impact: 9 functions changed, 19 affected
… inspect commands Extracts the duplicated "Calls"/"Called by"/"Tests" section rendering from presentation/audit.ts's renderCallRefs/renderRelatedTests and presentation/queries-cli/inspect.ts's renderExplainEdges into shared helpers in the new presentation/call-ref-sections.ts (renderCallRefsSection, renderNoCallEdgesFallback, renderRelatedTestsSection), following the same pattern established by #1756's impact-levels.ts unification. inspect.ts's richer format (indent-aware, "no call edges" fallback, singular/plural "Tests" label) is adopted as canonical in both commands. BEHAVIOR CHANGE: `codegraph audit`'s plain-text output (not --json/--ndjson) now matches `codegraph audit --quick`/`codegraph explain` exactly for these sections: - Prints "(no call edges found -- may be invoked dynamically or via re-exports)" when a function has neither callers nor callees. Previously audit printed nothing in this case. - "Tests (N):" is now "Tests (N file):"/"Tests (N files):" (singular/plural), matching explain's existing label. No changes to `codegraph audit --quick`/`codegraph explain` output — inspect.ts keeps its exact current format, just via the shared helper. docs check acknowledged: this is an internal presentation-layer refactor (no new command, language, or architecture change). CLAUDE.md's presentation/*.ts catch-all row already covers the new shared helper file, matching the precedent set by #1756's impact-levels.ts, which likewise did not get its own table row. Fixes #1772 Impact: 14 functions changed, 6 affected
…heck acknowledged)
Object-pattern destructuring targets (const { a, b } = x; and renamed
const { a: b } = x;) were hardcoded to kind: 'function' in both the TS/WASM
extractor (extractDestructuredBindings, shared by the walk and query paths)
and its native Rust mirror (extract_destructured_bindings), regardless of
what the destructured value actually held. Non-function bindings (e.g.
const { dbPath } = workerData) had no call-graph edges pointing at them by
name, so the dead-code classifier risked flagging them dead-unresolved even
when read repeatedly elsewhere in the file.
Both engines now emit kind: 'constant', matching the existing convention for
plain `const x = <literal>` bindings and array-pattern destructuring (which
was already correct). Every call site of these functions is const-gated, so
'constant' is unconditionally correct — no let/var case exists here. Call-
target resolution is kind-agnostic and constant is already included in the
top-level-binding caller-attribution fallback, so destructured
callback-style bindings (e.g. `const { handler } = router; handler(req)`)
still resolve correctly; verified via the resolution-benchmark suite showing
identical precision/recall across all 40 fixture languages before and after.
Array-pattern destructuring was checked and does not have this bug (already
kind: 'constant' in both engines' TS path); a separate, pre-existing native
vs WASM parity gap for array patterns was found and filed as #1901.
Docs check acknowledged: kind-classification bug fix only, no new
languages/features/architecture changes — README/ROADMAP tables unaffected.
Fixes #1773
Impact: 2 functions changed, 7 affected
Deduplicates the "loop N times, time each run, compute median" pattern
across the 7 remaining call sites that predated scripts/lib/bench-timing.ts:
- query-benchmark.ts's benchDepths()/benchDiffImpact() and
incremental-benchmark.ts's parent-process nativeBatchMs/jsFallbackMs
computations and benchmark.ts's benchQuery() now adopt timeMedian()
directly (converted to async; propagation stops at each call site
since all callers are already at ESM top-level/top-level-await scope).
- incremental-benchmark.ts's and benchmark.ts's "1-file rebuild"
measurements need the phases of the median-timed run, not just the
numeric median, so bench-timing.ts gains timeMedianWithValue(fn, runs,
beforeEach?) — returns the {ms, value} pair for the median-duration
run, with an optional untimed beforeEach(i) hook for per-iteration
setup (writing the probe file) that must stay outside the timed
window.
Verified no methodology regression: ran query-benchmark.ts and
incremental-benchmark.ts before/after on this repo (3 runs and 1 run
respectively) with consistent latencies, plus an isolated microbenchmark
quantifying the await-on-sync-value overhead at ~1 microsecond/call —
negligible against the millisecond-to-hundred-millisecond latencies
these scripts measure.
Fixes #1774
Impact: 4 functions changed, 4 affected
runPerfBenchmarks mixed build benchmarking, stats collection, hub-selection, and query benchmarking in one function, exceeding complexity thresholds (cognitive 15, cyclomatic 13) and the titan-gate halstead.bugs FAIL threshold (1.2559 > 1.0). Extract runBuildBenchmarks(engines, dbPath, nextjsDir, buildGraph) and runQueryBenchmarks(hubName, dbPath, fnDepsData, fnImpactData) as named async helpers; runPerfBenchmarks becomes a thin orchestrator. Pure decomposition, verified with a throwaway call-sequence harness — no control-flow or value changes. Fixes #1775 Impact: 3 functions changed, 3 affected
…dentifiers codegraph roles --role dead flagged Lua functions monkey-patched onto a global/builtin identifier (e.g. `require = tracedRequire`) as dead-unresolved, despite being genuinely invoked at runtime through every later unqualified use of the builtin name. Root cause: neither engine's Lua extractor inspected bare (non-`local`) assignment_statement nodes at all. `require = tracedRequire` produced zero extraction output — not a call, and the assignment itself was invisible to the walker's switch, which only handled function_declaration, variable_declaration, and function_call. Fix: both engines now emit a dynamic `calls` edge (dynamic=1, dynamicKind/dynamic_kind = 'value-ref' — the same classification #1771 uses for object-literal property-value references) from the enclosing scope to the RHS function, for any plain `identifier = identifier` assignment whose LHS matches a recognized Lua builtin/stdlib-module name (LUA_BUILTIN_GLOBALS). `value-ref` resolution already restricts matches to function/method-kind targets only (build-edges.ts / incremental.ts / build_edges.rs, unchanged), so a builtin reassigned to a non-function value is silently dropped rather than fabricating a nonsensical edge. Scoped narrowly to the reported pattern per #1776's own framing: the LHS must be a recognized Lua builtin/module name specifically, because a builtin identifier is not a locally-scoped variable that could ever be tracked by conventional alias/points-to resolution — general local-to-local aliasing (`local a = someFunc; a()`) is a separate, much larger points-to problem this does not attempt to solve, and JS/TS already handles that case correctly via the existing points-to solver. Both the bare top-level form and the `local`-declared shadow form (`local require = tracedRequire`) are handled identically, since shadowing a builtin name is the same "redirect every later unqualified use" idiom, just lexically scoped. Multi-assignment (`a, b = f, g`) is paired positionally to avoid shifting pairs when a mix of identifier/non-identifier variables is present. Applied to both engines: - WASM/TS: src/extractors/lua.ts (LUA_BUILTIN_GLOBALS, handleLuaAssignmentStatement, wired into walkLuaNode's switch). - Native: crates/codegraph-core/src/extractors/lua.rs (LUA_BUILTIN_GLOBALS, handle_lua_assignment_statement, wired into match_lua_node), plus a new `mod tests` block (this extractor previously had none). No changes needed to build-edges.ts / incremental.ts / build_edges.rs — the `value-ref` DynamicKind's function/method-kind resolution filter already applies generically regardless of source language. Broadened the `value-ref` doc comments (types.ts, ADR-002) to describe it as syntax-position-agnostic (object-literal value or assignment to a global/builtin identifier) rather than JS-object-literal-specific. Verified against the exact repro (tests/benchmarks/resolution/tracer/lua-tracer.lua's traced_require): fn-impact now reports totalDependents=1 and role=core on both engines (previously dead-unresolved, totalDependents=0); `roles --role dead` for that file now returns zero symbols. Lua resolution-benchmark fixture is unaffected (100% precision / 15.4% recall, unchanged before and after — the fixture files don't contain this assignment pattern). Full suite: 3581 tests passed (0 failed) across both engines; cargo test: 475 passed (0 failed), including 9 new Lua extractor tests. Filed #1909 for an unrelated, pre-existing gap found incidentally: the WASM Lua extractor is missing eval/computed-key dynamic-call detection that the native extractor already has (from an earlier phase-6 parity PR that only updated the Rust side). Fixes #1776 docs check acknowledged — internal edge-emission/resolver bug fix to an existing language extractor; no new commands, languages, or architecture to document. ADR-002 updated in place to broaden the existing value-ref DynamicKind description. Impact: 2 functions changed, 2 affected
The portable "sed -i (GNU vs BSD)" helper was duplicated: native-tracer.sh and jvm-tracer.sh each defined a byte-identical sedi() function, and go-tracer.sh inlined the same GNU-detection branch twice instead of extracting a helper at all. jvm-tracer.sh additionally left two more GNU-detection branches inlined in its java case (method-body injection, main-dump injection) rather than routing them through the sedi() it had already defined for its kotlin/scala/groovy cases. Extracts sedi() into tests/benchmarks/resolution/tracer/tracer-common.sh, sourced by all three scripts via a path relative to the script's own location (robust regardless of invoking cwd — run-tracer.mjs spawns these with cwd set to the fixture dir, not the tracer dir). Removes every inline copy, including both in go-tracer.sh and the two leftover ones in jvm-tracer.sh's java case. Pure structural dedup: every replaced sed invocation keeps its exact original script content. Verified byte-identical stdout/stderr/exit code before and after for every case reachable in this environment: native-tracer.sh's rust and swift (full end-to-end success) and c/cpp (pre-existing unrelated compile failures, filed as #1914); jvm-tracer.sh's java (pre-existing unrelated BSD-sed syntax failure, filed as #1913); go-tracer.sh (go toolchain unavailable, graceful skip before any sed call is reached). Also verified via run-tracer.mjs directly and from an unrelated cwd to confirm the source path resolves correctly. tracer-validation.test.ts (42/42) and the full suite (3581 passed, 30 skipped, 2 todo) pass; lint is unaffected (Biome doesn't process .sh files). Fixes #1777 Impact: 1 functions changed, 9 affected
…k acknowledged) Issue #1778: /parity found WASM emits dyn=0 (plain static call) while native emits dyn=1/dynamicKind='reflection' for identifier.call(...)/.apply(...)/ .bind(...) call sites, even though both fully resolve the target (confidence 1). Root cause: PR #1693 (closing #1687) made the WASM extractor unconditionally drop dynamic/dynamicKind for these calls on identifier receivers, to fix a narrow dedup-collision bug in build-edges.ts's dynZeroEdgeRows upgrade path. That fix overcorrected — it silenced the `reflection` DynamicKind for every identifier-based .call/.apply/.bind, not just the collision case, diverging from native (which never changed). Chose Option A (revert + fix narrowly), not Option B (mirror WASM's drop in Rust): ADR-002 defines `reflection` as informational metadata independent of resolution/confidence, queryable via `codegraph roles --dynamic` — dropping it for .call/.apply/.bind would erase real information about a genuinely reflective invocation mechanism, and native's classification was never wrong. Changes: - src/extractors/javascript.ts: extractMemberExprCallInfo once again tags .call/.apply/.bind on identifier receivers as dynamic:true, dynamicKind:'reflection', matching the member-expression branch and native. - src/domain/graph/builder/stages/build-edges.ts: emitDirectCallEdgesForCall's dyn=0 -> dyn=1 upgrade (dynZeroEdgeRows) now compares source lines instead of firing on any dynamicKind-tagged collision. It only upgrades when the incoming call's line is EARLIER than the recorded dyn=0 call's line — which only happens when the query path's two-phase collection (query matches, then a supplementary walk pass for bare decorators, #1683) reorders a genuinely-earlier call to arrive later. A later .call/.apply/.bind to an already-called target is collected in the same phase as the direct call (true source order already holds), so it must not flip an established dyn=0 edge — matching native's plain first-recorded-wins dedup. - crates/codegraph-core/src/extractors/javascript.rs: no behavior change (native was already correct); added 4 unit tests pinning the identifier/member-expression reflection tagging so this doesn't regress. - tests/parsers/javascript.test.ts, tests/engines/dynamic-call-ffi.test.ts: updated to assert the restored dynamic/reflection tagging at the parser level. - tests/integration/issue-1778-reflection-dynamic-kind-parity.test.ts: new engine-parity tests (wasm + native) covering the #1778 minimal repro (no prior direct call -> dyn=1), the original #1687 dedup scenario (direct call then .call() -> single dyn=0 edge), the reverse-order case, and a #1683 regression guard (bare decorator before call-expression decorator still upgrades to dyn=1). Verified: `node scripts/parity-compare.mjs` shows zero edge diffs across all 42 fixtures (previously 6/8/12 diffs on dynamic-javascript/javascript/ jelly-micro). Resolution-benchmark precision/recall unchanged for every language. Full npm test suite green (3589 passed). cargo test green (479 passed, +4 new). Fixes #1778 Impact: 6 functions changed, 13 affected
…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
Greptile SummaryThis PR fixes a bug where TypeScript's per-specifier inline
Confidence Score: 5/5The change is narrowly scoped to tracking an additional per-name flag through an existing pipeline; the existing file-level edge logic is untouched, and the new per-name filter only activates for imports-type edges. The fix is consistent across all four pipeline paths (full build, incremental, native FFI, pure-native) and both engine extractors. The modifier detection correctly uses the tree-sitter field convention (child(0) is always the optional modifier in import_specifier) and correctly uses local binding names (post-alias) in both typeOnlyNames and the downstream filter. Tests cover extractor-level unit cases, Rust unit cases, and a dual-engine integration scenario that exercises the exact real-world pattern from the PR description. No files require special attention. Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A["import { value, type Foo } from './mod'"]
A --> B["extractImportNames / scan_import_names_depth"]
B --> C["For each import_specifier node"]
C --> D{"child(0).kind == 'type' | 'typeof'?"}
D -- Yes --> E["typeOnlyNames.push(localNode.text)"]
D -- No --> F["(skip modifier tracking)"]
E --> G["Import { names: ['value','Foo'], typeOnlyNames: ['Foo'] }"]
F --> G
G --> H["importNamePairs / import_name_pairs"]
H --> I["{ local:'value', original:'value', typeOnly:false }"]
H --> J["{ local:'Foo', original:'Foo', typeOnly:true }"]
I --> K{"emitNamedSymbolEdges kind == 'imports-type'?"}
J --> K
K -- "typeOnly=false → skip" --> L["no imports-type edge for 'value'"]
K -- "typeOnly=true → emit" --> M["imports-type edge: consumer.ts → Foo"]
G --> N["importEdgeKind: typeOnly=false → 'imports'"]
N --> O["file-level edge: consumer.ts → mod.ts (imports)"]
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
flowchart TD
A["import { value, type Foo } from './mod'"]
A --> B["extractImportNames / scan_import_names_depth"]
B --> C["For each import_specifier node"]
C --> D{"child(0).kind == 'type' | 'typeof'?"}
D -- Yes --> E["typeOnlyNames.push(localNode.text)"]
D -- No --> F["(skip modifier tracking)"]
E --> G["Import { names: ['value','Foo'], typeOnlyNames: ['Foo'] }"]
F --> G
G --> H["importNamePairs / import_name_pairs"]
H --> I["{ local:'value', original:'value', typeOnly:false }"]
H --> J["{ local:'Foo', original:'Foo', typeOnly:true }"]
I --> K{"emitNamedSymbolEdges kind == 'imports-type'?"}
J --> K
K -- "typeOnly=false → skip" --> L["no imports-type edge for 'value'"]
K -- "typeOnly=true → emit" --> M["imports-type edge: consumer.ts → Foo"]
G --> N["importEdgeKind: typeOnly=false → 'imports'"]
N --> O["file-level edge: consumer.ts → mod.ts (imports)"]
Reviews (1): Last reviewed commit: "fix: track inline per-specifier type-onl..." | Re-trigger Greptile |
Codegraph Impact Analysis19 functions changed → 38 callers affected across 4 files
|
Owned scope confirmed via this PR's own single commit diff-stat: the typeOnlyNames sparse field on Import (types.ts/types.rs) and its propagation through both extractors (javascript.ts/javascript.rs), both JS edge-building paths (build-edges.ts/incremental.ts, relocated to the shared import-utils.ts importNamePairs since that extraction landed on main after this branch was cut), the native FFI pipeline (build_edges.rs), the pure-native pipeline (import_edges.rs), and pipeline.rs's tuple-destructure call site all merged in cleanly alongside main's independent changes (barrel-reachability, #1849 wildcard-reexport, #1781 satisfies_expression, #1930 value-ref filter docs, and this branch's own upstream #1812/#1957 hierarchy-edge scoping fix). The remaining conflicting files were unrelated stack noise, resolved by taking main's content. All 11 owned files verified byte-exact against both this PR's own diff (vs main) and main's independent additions (vs ORIG_HEAD). Impact: 55 functions changed, 190 affected
Summary
import { value, type Foo } from 'mod'), in addition to whole-statementimport type { Foo } from 'mod'. Both engines derivedtypeOnlyper import statement only (text.startsWith('import type')), so a mixed statement's inline-flagged names got no type-only tracking at all — notimports-type(statement isn't fully type-only) and not usable as acallsconsumer either (never called/constructed). This undercounted real consumers reported bycodegraph exportsfor any type commonly imported this way — including this repo's owndb/index.tsre-exports (e.g.import { openRepo, type Repository } from '../db/index.js').Importwith a sparsetypeOnlyNames?: string[]field (mirrors the existingrenamedImports"only populated when needed" convention), recording the local binding names of specifiers carrying the inlinetype/typeofmodifier. Per the tree-sitter-typescript grammar,import_specifierisoptional(choice('type', 'typeof'))followed by the name/alias fields, so the modifier — when present — is always the specifier's first child.src/extractors/javascript.ts): both the query-dispatch path (handleImportCapture, used by the real WASM worker build) and the walk path (handleImportStmt, used by directextractSymbols()calls/unit tests).crates/codegraph-core/src/extractors/javascript.rs):scan_import_names_depthnow also collects per-specifier type-only names.build-edges.ts,incremental.ts):importNamePairsnow reports atypeOnlyflag per name (whole-statement OR inline), andemitNamedSymbolEdgesonly emits animports-typesymbol edge for names actually flagged, instead of gating the whole loop on the statement-level flag.build_edges.rs, used when the native addon is loaded regardless of parsing engine) and the pure-native pipeline (import_edges.rs, used by the standalone native builder): mirrored the same per-name filtering logic and addedtypeOnlyNames/type_only_namesto the respectiveImportInfo/Importshapes.importsedges for a mixed statement stayimports(notimports-type) — the statement as a whole still imports a real value too. Only the symbol-level edge used for consumer-crediting changes.Root cause detail
The WASM worker build path loads compiled
dist/output, notsrc/— anpm run buildwas required for the extractor fix to actually take effect in the real file-based build path (unit tests callingextractSymbols()directly are unaffected by this and would have looked "fixed" without it).Test plan
tests/parsers/javascript.test.tsunit tests (walk path, directextractSymbols): inline modifier trailing/leading a value specifier, multiple type-only names in one statement,typeofmodifier, no modifier at all (field staysundefined), whole-statementimport type(field staysundefined,typeOnly: truealone still covers it), and a renamed type-only specifier (type Repository as Repo).tests/integration/issue-1813-inline-type-modifier.test.ts+tests/fixtures/issue-1813-inline-type-modifier/, run against bothwasmandnativeengines via realbuildGraph: a mixedimport { computeSize, openRepo, type Repository, type Widget } from './types.js'creditsRepository/Widgetwith a symbol-levelimports-typeedge, does not creditopenRepo/computeSize, and keeps the file-level edgeimports.import_edges.rs(import_name_pairs_*, pure-native pipeline) andbuild_edges.rs(mixed_import_inline_type_modifier_credits_only_flagged_name, native FFI pipeline).codegraph exports types.ts --jsonnow reportsconsumerCount: 1for bothRepositoryandWidget(was0before the fix) — identical output forwasmandnative.npm test— full suite green (225 files, 3715 passed, 30 skipped, 2 todo — all pre-existing).npm run lint— clean.npm run build— clean (required for the WASM worker to pick up the extractor change, see root cause detail).cargo testincrates/codegraph-core— 517 passed, 0 failed. Native addon rebuilt locally vianapi build --platform --release+ codesign to verify against the real native path.codegraph diff-impact --staged -T— blast radius limited to the changed import-edge functions (importNamePairs,emitNamedSymbolEdges,emitEdgesForImport,toNativeImportInfo,handleImportCapture,handleImportStmt,extractImportNames) plus the new fixture files, as expected.Fixes #1813
Stacked on #1812 (base branch
fix/issue-1812-extends-implements-edges-resolve-by-symbol) — only the extractor/edge-building/test diff described above is this issue's change.