fix(embed): resolve default DB path from positional dir, not cwd#2002
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
extractImportNames's export_specifier branch only ever recorded the declaration name (X), never the external alias (Y) a consumer of the barrel actually imports. resolveBarrelExport/resolve_barrel_export therefore had no way to translate a consumer's requested name (Y) back to X, so calls resolved through a renamed barrel re-export were silently dropped. Extends Import.renamedImports (added for #1730) to export_specifier nodes and threads the rename table through barrel resolution in both engines: resolveBarrelExport/resolve_barrel_export now return the declared name alongside the resolved file, and every caller (import edges, call-edge resolution, native orchestrator) uses that name instead of the originally-requested one. codegraph watch's separate DB-only incremental rebuild path can't recover a barrel's rename table when the barrel itself isn't part of the current watch batch -- filed as #1967, out of scope here since standard codegraph build (including hash-based incremental skip) is unaffected. Closes #1823 Impact: 28 functions changed, 59 affected
const { a: b } = await import(...) recorded the original exported name
(a) instead of the local binding actually used at call sites (b), so
calls like b(...) never resolved to the imported symbol. The
pair_pattern branch of extractDynamicImportNames (TS/WASM) and
extract_dynamic_import_names (Rust) preferred tree-sitter's `key` field
over `value` - the same class of bug fixed for static
`import { X as Y }` specifiers in #1730.
Both engines now record the local alias in `names`, with the
local -> original mapping in `Import.renamedImports` so call-edge
resolution can still find the target module's real export. This reuses
the renamedImports mechanism #1730/#1823 already threaded through
resolution, so no resolver changes were needed - only extraction.
The Rust fix also corrects the same key-vs-value bug in CJS require()
destructuring (which shares collect_object_pattern_names), bringing it
in line with WASM's already-correct extractCjsRequireBinding.
Impact: 3 functions changed, 10 affected
`resolveByReceiver`'s qualified-name lookups (typed-method, prototype-alias,
and direct-qualified-method) built the symbol-table key from the call
receiver's local text or its typeMap-resolved type name, neither of which
accounts for a renamed import binding (`import { X as Y } from './mod'; Y.method()`).
The symbol table stores definitions under their declared name (X), not the
importing file's local alias (Y), so the lookup silently missed and no
`calls` edge was created.
Thread `importedOriginalNames` (already computed for direct call-name
resolution, #1730) through `resolveByMethodOrGlobal` into `resolveByReceiver`,
and de-alias the qualifier immediately before building each qualified lookup
key. Mirrored in the native Rust resolver (`resolve_call_targets`).
insert_edges, build_and_insert_call_edges, and Stage 5's do_insert_nodes call all discarded their Result via `let _ =` (or an unchecked () return for insert_edges), so a transaction that never started or failed to commit left run_pipeline returning Ok(...) over an incomplete graph. NativeDatabase::build_graph then reported a "successful" build to JS with nothing beyond a stderr eprintln to show for it. Thread the Result through all three call sites via `?` so a write failure aborts run_pipeline and surfaces as a thrown napi error. tryNativeOrchestrator already catches that and falls back to the JS/WASM pipeline, so this turns a silent partial build into a real retry. Fixes #1827
consumers[] entries returned by exportsData (codegraph exports CLI, file_exports MCP tool) conflated two different shapes: a real caller (calls edge, source = function/method/class node) and a whole-file import-type reference (imports-type edge, source = the importing file node itself, line fixed at 0). Renderers had no way to tell them apart, so a type-only import rendered as if it were a real call-site, e.g. "consumer.ts (consumer.ts:0)". Add a consumerKind: 'file' | 'symbol' discriminator derived from the consumer's source node kind, and update the CLI renderer to print file-level entries as "<file> (type-only import)" instead of a fabricated call-site line.
… tooling CONTRIBUTING.md described the pre-TypeScript codebase: a plain-JS src/ tree with files that no longer exist (cli.js, builder.js, parser.js, queries.js, embedder.js, db.js, mcp.js, cycles.js, export.js, watcher.js, config.js, constants.js), Node >= 20 (actual requirement is >= 22.12.0 per package.json), an experimental "feat/rust-core" branch for the native engine (long since merged and now the default), .test.js examples (suite is .test.ts), and a stale WASM-grammars-are-committed claim (they're built from devDependencies via the prepare script on every npm install, not committed). Rewrite Project Structure, Architecture Notes, Code Style, Commands, Testing, Regression Benchmarks, Parser Improvements, and WASM Grammars sections to match the current TypeScript layer-based src/ tree, dual-engine architecture, and tooling. Project Structure and Architecture Notes now point to CLAUDE.md for the module-by-module breakdown instead of duplicating it, so this doc can't drift out of sync with the codebase the same way again.
codegraph exports/dead-export analysis only credited a symbol-level
imports-type edge when the importing statement was syntactically marked
type-only (import type { X } or the inline import { type X } modifier).
TypeScript also allows importing an interface or type alias through a
plain import { X } from 'y' with no type keyword at all -- an extremely
common pattern in codebases that don't enforce
import/consistent-type-imports. Since interfaces and type aliases are
erased before runtime, they can never receive a calls edge either, so a
plain import was the only possible consumption signal and it was being
ignored -- permanently misclassifying every such interface/type alias as
a dead export.
Fix the root cause: classify imports-type crediting by the resolved
target's node kind (interface/type in a .ts/.tsx file), not by the
importing statement's syntax. A plain import of a value symbol (function,
class, const) is unaffected -- its consumption credit still requires a
real calls edge. Scoped to TypeScript files specifically, since other
languages reuse the 'interface'/'type' node kinds for constructs that are
runtime-observable (e.g. a Go type alias, a Java interface dispatched
through at runtime).
Mirrored in both engines: the WASM/JS full-build and incremental
pipelines (build-edges.ts, incremental.ts) and both native Rust import-edge
paths (the FFI-hybrid build_edges.rs and the native orchestrator's
import_edges.rs).
Impact: 13 functions changed, 25 affected
The PostToolUse hook's fallback branch (used when the codegraph binary isn't on PATH) invoked node against src/cli.js, a file that has never existed in this repo. The CLI entry point is TypeScript at src/cli.ts, compiled to dist/cli.js per package.json's bin.codegraph. Since stderr is redirected to /dev/null, the branch failed silently and the graph never rebuilt for contributors without a global codegraph install. Add a regression test asserting the hook never references src/cli.js and does invoke package.json's bin.codegraph entry point.
…'s path track-edits.sh resolved the session-edits.log location via a bare `git rev-parse --show-toplevel`, relying on the hook process's own ambient cwd. Edit/Write tool calls carry only an absolute file_path with no associated "current directory" state, so that ambient cwd is not guaranteed to match the worktree that actually owns the edited file — causing edits to be logged to the wrong worktree (or dropped) and guard-git.sh to later block legitimate commits with "NOT edited in this session". Derive PROJECT_DIR from the edited file's own path instead (`git -C "<dir containing file_path>" rev-parse --show-toplevel`, walking up to the nearest existing ancestor for not-yet-created Write targets), mirroring the `-C "$WORK_DIR"` pattern guard-git.sh already uses on the read side of this same check. Applied to both the live hook and its docs/examples mirror. Fixes #1838
The small-incremental fast path's directory-metrics refresh (refreshAffectedDirectoryMetrics / refresh_affected_directory_metrics) discovers cross-directory neighbors by querying live import edges from the affected directories. This works for added/modified files but not for removed files, since purgeFilesFromGraph/purgeFilesData deletes both directions of their edges before the structure stage runs, leaving no live evidence to discover a lone connecting neighbor directory from. Add captureRemovedFileNeighbors (WASM) / capture_removed_file_neighbors (native), which reads a removed file's forward+reverse import-neighbor file set in detectChanges/detect_changes, before the purge step, and threads it through to the directory-metrics refresh so the neighbor directory's ancestor chain still gets folded into the affected set. Fixes #1839 Impact: 6 functions changed, 10 affected
--no-new-cycles, --max-blast-radius, and --no-boundary-violations were renamed to --cycles, --blast-radius, and --boundaries respectively; the docs examples still referenced the old names. Also drop the fabricated --max-complexity predicate (no such check flag exists) and add --signatures to the documented predicate list. Add a regression test that derives the valid flag set from the check command's own option definitions so future renames fail the test instead of drifting silently again.
Cycle detection (codegraph cycles / check --cycles / manifesto noCycles) previously treated every call edge identically, so a cycle held together only by a low-confidence dynamic-dispatch guess (dynamic = 1 AND confidence < 1) was reported the same as one backed entirely by confirmed static edges. A single fabricated resolver edge could therefore surface as a "confirmed" architectural cycle and fail CI. findCycles() now runs Tarjan a second time on the edge set with speculative edges removed and diffs the two SCC decompositions — any cycle that only exists in the full run is marked `speculative: true` instead of `confirmed`. Threads `dynamic` through getCallEdges (both the SQLite and native/rusqlite paths) so the classifier has the metadata it needs; the SCC primitive itself (JS and native) is unchanged and reused for both passes, so no engine can diverge on the classification. `codegraph cycles` gains `--exclude-speculative` and annotates speculative cycles in table/JSON output. `check --cycles` and the manifesto `noCycles` rule ignore speculative-only cycles by default via the new `check.excludeSpeculativeCycles` config default.
… functions via parameter type extractCallbackReferenceCalls/extract_callback_reference_calls gated identifier-argument dynamic calls on a name/position allowlist (CALLBACK_ACCEPTING_CALLEES/POSITIONAL_CALLBACK_ARG_INDEX), which by definition can never enumerate a project's own arbitrary higher-order functions (e.g. processEach(users, logUser), where processEach's `fn` parameter is typed as a function-shaped alias). Add a same-file, TS-only signal: collectCallbackParamShapes walks each file once to record, per function/method declaration, which parameter positions carry a function-shaped type (inline arrow-function type, `Function`, or a `type X = (...) => ...` alias resolved through one level of indirection). extractCallbackReferenceCalls now allows an identifier/member-expression argument through when the callee's own parameter at that position is function-shaped, in addition to the existing name/position allowlist. Mirrored in both engines (WASM walk and query paths, native Rust). Recovers the 3 callback edges intentionally dropped by #1741 (runCallbackDemo -> logUser/upperUser/hasEmail in the typescript resolution fixture); typescript resolution-benchmark recall floor ratcheted 0.72 -> 0.9 to lock in the fix. Cross-file callees remain gated by the name/position allowlist only — recognizing those needs the resolver's import-resolution machinery and is tracked as a follow-up. Closes #1845
…-parses The native orchestrator's run_pipeline classified every barrel-like file (reexports.length >= ownDefs) in file_symbols as "barrel-only" and skipped its non-reexport imports (imports-type/imports/dynamic-imports) — on full builds, and even for a genuinely-changed file on incremental builds. WASM only ever applies that skip to files transiently re-parsed purely to resolve a reexport chain (resolve-imports.ts::reparseBarrelFiles, incremental-only), never to a file that's actually part of the build's changed set, and never on a full build. Scope detect_barrel_only_files to the candidate paths reparse_barrel_ candidates merges into file_symbols (now returned from that function) instead of every key in file_symbols, matching WASM's semantics exactly. On a full build the candidate list is empty, so no file is ever classified barrel-only, mirroring WASM's `ctx.barrelOnlyFiles` staying empty there. Both divergences were silently dropping real edges depending purely on which engine built the graph. Impact: 3 functions changed, 0 affected
…into apply-results.ts ast-analysis/engine.ts and domain/wasm-worker-entry.ts each maintained an independent, near-verbatim copy of hasFuncBody, indexByLine, matchResultToDef, storeComplexityResults, and storeCfgResults. Both copies carried the same bug (issue #1743) and had to be fixed in lockstep, with nothing enforcing they stay in sync. These functions only operate on plain WalkResults/Definition data with no DB handle or parser-global state, so they're safe to share across the worker-thread boundary. Extract them into ast-analysis/apply-results.ts and have both call sites import from it instead of maintaining separate copies.
…edges into watch-mode rebuild codegraph watch's single-file incremental rebuild (rebuildFile/buildCallEdges in builder/incremental.ts) implemented only a subset of the full-build call resolution cascade, silently dropping three categories of edges whenever the file containing the call site was rebuilt via watch mode instead of a full build: 1. CHA/RTA virtual-dispatch edges (interface dispatch, this/super-dispatch) — added a CHA/RTA post-pass (applyChaDispatchPostPass) that runs after all of a rebuild's edges (target file + reverse deps) are written, using a new DB-driven ChaContext builder (buildChaContextFromDb in cha.ts) since the class hierarchy and RTA instantiation evidence span the whole project, not just the files held in memory during a single-file rebuild. 2. Points-to/alias-resolved edges — wired buildPointsToMapForFile (moved from build-edges.ts into resolver/points-to.ts so both the full-build and incremental paths share one implementation) into buildCallEdges as a fallback when direct resolution finds nothing, mirroring the full-build pts fallback. 3. Dynamic-sink edges for flag-only dynamic call kinds (eval, computed-key, reflection, unresolved-dynamic) — added sink-edge emission keyed off symbols.calls[].dynamicKind, using a shared FLAG_ONLY_DYNAMIC_KINDS constant (moved to shared/kinds.ts) so both paths flag the same kinds. New CHA/pts/sink edges insert their technique/dynamic_kind directly via a new local prepared statement (insertCallEdgeExt), bypassing the generic insertEdge used elsewhere in this file — the existing #1744 technique backfill only knows how to stamp 'ts-native' on NULL-technique edges, which would have mislabeled these. Its backfill query now also excludes dynamic_kind IS NOT NULL rows so sink edges keep technique=NULL as intended. Impact: 16 functions changed, 19 affected
applyEnvProvenance hardcoded a second literal list of the four CODEGRAPH_LLM_* env var names to decide whether to attribute provenance.llm = 'env'. If a new override were ever added to ENV_LLM_MAP without updating this list, the override would work but `codegraph config --explain` would silently fail to attribute it. Move ENV_LLM_MAP above the provenance helpers and derive the key list via Object.keys(ENV_LLM_MAP) so the two can never drift apart.
… candidate resolveByGlobal's exact-name lookup returned every same-named symbol in the codebase that cleared the 0.5 directory-proximity confidence threshold, and resolveCallTargets/emitDirectCallEdgesForCall then emitted a calls edge to each one instead of the single most likely target. A bare (no-receiver) or this/self/super call carries no type info to disambiguate, so fanning out turned one real call site into N-1 false edges (e.g. four object-literal close() methods in src/db/connection.ts each getting a calls edge from every caller that destructures a close() handle). Fix: reduce the exact-name lookup to the single unambiguous highest-confidence candidate. A genuine tie at the top confidence has no principled winner and now resolves to nothing rather than betting on all of them, falling through to the narrower same-class-sibling fallback instead. Mirrored in both engines: src/domain/graph/resolver/strategy.ts (WASM/TS) and crates/codegraph-core/src/domain/graph/builder/stages/build_edges.rs (native). Updates two pre-existing tests whose fixtures/assertions had baked in the old fan-out behavior as expected (#1519's byName suite, #1752's reverse-dep reconnect baseline, which had already flagged this exact scenario as a "separate, pre-existing concern — filed independently"). Impact: 2 functions changed, 10 affected
`embed [dir]` ignored its positional `dir` argument when resolving the default `.codegraph/graph.db` path (no `--db` flag), instead searching upward from process.cwd() via findDbPath(). This let embed silently attach to an unrelated graph.db found between cwd and the git root, or fail with a confusing "no database found" error even right after a successful `build <dir>`. findDbPath() and openReadonlyOrFail() now accept an optional rootDirHint that anchors the search at a known project root instead of cwd, mirroring build's <rootDir>/.codegraph/graph.db convention. buildEmbeddings() (the `embed` DB path) and resolveStickyModel() (the sticky-model DB lookup) both thread the resolved `dir` argument through as that hint.
Greptile SummaryThis PR fixes a path-resolution bug where
Confidence Score: 5/5Safe to merge — the change is additive (new optional parameters, no call-site breakage) and directly addresses a well-understood path-resolution bug with targeted tests that fail on the pre-fix code. The fix correctly threads No files require special attention. Important Files Changed
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A["codegraph embed [dir]"] --> B["execute([dir], opts)"]
B --> C["root = path.resolve(dir || '.')"]
B --> D{"--db flag set?"}
D -->|"yes: dbPath = opts.db"| E["resolveStickyModel(dbPath, root)"]
D -->|"no: dbPath = undefined"| E
E --> F["openReadonlyOrFail(dbPath, resolveBusyTimeoutMs(dbPath ?? root), root)"]
F --> G["findDbPath(dbPath, rootDirHint=root)"]
G --> H{"customPath set?"}
H -->|yes| I["resolveCustomDbPath(dbPath)"]
H -->|no| J["findRepoRoot(rootDirHint)"]
J --> K["walkUpForDbPath(rootDirHint, ceiling)"]
K -->|found| L["return existing DB path"]
K -->|not found| M["base = rootDirHint || ceiling || cwd()"]
M --> N["return base/.codegraph/graph.db"]
B --> O["buildEmbeddings(root, model, dbPath, opts)"]
O --> P["findDbPath(undefined, rootDir=root)"]
P --> J
%%{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["codegraph embed [dir]"] --> B["execute([dir], opts)"]
B --> C["root = path.resolve(dir || '.')"]
B --> D{"--db flag set?"}
D -->|"yes: dbPath = opts.db"| E["resolveStickyModel(dbPath, root)"]
D -->|"no: dbPath = undefined"| E
E --> F["openReadonlyOrFail(dbPath, resolveBusyTimeoutMs(dbPath ?? root), root)"]
F --> G["findDbPath(dbPath, rootDirHint=root)"]
G --> H{"customPath set?"}
H -->|yes| I["resolveCustomDbPath(dbPath)"]
H -->|no| J["findRepoRoot(rootDirHint)"]
J --> K["walkUpForDbPath(rootDirHint, ceiling)"]
K -->|found| L["return existing DB path"]
K -->|not found| M["base = rootDirHint || ceiling || cwd()"]
M --> N["return base/.codegraph/graph.db"]
B --> O["buildEmbeddings(root, model, dbPath, opts)"]
O --> P["findDbPath(undefined, rootDir=root)"]
P --> J
Reviews (5): Last reviewed commit: "fix(test): close DB handles in #1869 roo..." | Re-trigger Greptile |
| const base = ceiling || rootDirHint || process.cwd(); | ||
| return path.join(base, '.codegraph', 'graph.db'); |
There was a problem hiding this comment.
Fallback base prefers git root over
rootDirHint
When walkUpForDbPath finds no existing DB, the fallback is ceiling || rootDirHint || process.cwd(). If rootDirHint sits inside a git repo (the common case), ceiling is the git root and wins — so the default DB path becomes <git-root>/.codegraph/graph.db rather than <rootDirHint>/.codegraph/graph.db. This diverges from build <dir>'s convention, which always writes to <dir>/.codegraph/graph.db. A user who runs codegraph embed /some/subproject before building will see an error pointing to the wrong location: No codegraph database found at <git-root>/.codegraph/graph.db instead of <rootDirHint>/.codegraph/graph.db. Swapping the order to rootDirHint || ceiling || process.cwd() would mirror build's behavior.
There was a problem hiding this comment.
Confirmed and fixed — swapped to rootDirHint || ceiling || cwd. Added a regression test that fails against the pre-fix priority order and passes after.
| function resolveStickyModel(dbPath: string | undefined, rootDir: string): string | null { | ||
| try { | ||
| const db = openReadonlyOrFail(dbPath, resolveBusyTimeoutMs(dbPath)); | ||
| const db = openReadonlyOrFail(dbPath, resolveBusyTimeoutMs(dbPath), rootDir); |
There was a problem hiding this comment.
resolveBusyTimeoutMs ignores rootDirHint — the same class of bug the PR fixes for the DB path.
When --db is not passed (dbPath is undefined), resolveBusyTimeoutMs(undefined) calls loadConfig(undefined), which loads db.busyTimeoutMs from process.cwd()'s config rather than the target root's config. If the project at root configures a non-default db.busyTimeoutMs, the sticky-model pre-flight read uses the wrong timeout. The fix is to pass root as the fallback rootDir when dbPath is absent.
| const db = openReadonlyOrFail(dbPath, resolveBusyTimeoutMs(dbPath), rootDir); | |
| const db = openReadonlyOrFail(dbPath, resolveBusyTimeoutMs(dbPath ?? rootDir), rootDir); |
There was a problem hiding this comment.
Applied your suggested fix exactly.
Codegraph Impact Analysis7 functions changed → 136 callers affected across 83 files
|
…dir-ignores-positional-dir-for-default # Conflicts: # .claude/hooks/track-edits.sh # .claude/skills/titan-grind/SKILL.md # CONTRIBUTING.md # crates/codegraph-core/src/ast_analysis/complexity.rs # crates/codegraph-core/src/db/connection.rs # crates/codegraph-core/src/domain/graph/builder/stages/build_edges.rs # crates/codegraph-core/src/domain/graph/builder/stages/import_edges.rs # crates/codegraph-core/src/domain/graph/resolve.rs # crates/codegraph-core/src/extractors/javascript.rs # crates/codegraph-core/src/features/structure.rs # crates/codegraph-core/src/graph/classifiers/roles.rs # crates/codegraph-core/src/shared/constants.rs # docs/architecture/decisions/002-dynamic-call-resolution.md # docs/examples/claude-code-hooks/track-edits.sh # scripts/lib/bench-timing.ts # scripts/token-benchmark.ts # src/ast-analysis/visitors/ast-store-visitor.ts # src/cli/commands/embed.ts # src/db/connection.ts # src/db/index.ts # src/db/repository/nodes.ts # src/domain/analysis/exports.ts # src/domain/analysis/query-helpers.ts # src/domain/analysis/symbol-lookup.ts # src/domain/graph/builder/call-resolver.ts # src/domain/graph/builder/incremental.ts # src/domain/graph/builder/stages/build-edges.ts # src/domain/graph/builder/stages/build-structure.ts # src/domain/graph/builder/stages/native-orchestrator.ts # src/domain/graph/cycles.ts # src/domain/graph/resolve.ts # src/domain/graph/resolver/strategy.ts # src/domain/parser.ts # src/domain/search/generator.ts # src/domain/search/search/keyword.ts # src/extractors/javascript.ts # src/extractors/lua.ts # src/features/check.ts # src/features/cochange.ts # src/features/structure.ts # src/infrastructure/config.ts # src/infrastructure/doctor.ts # src/presentation/check.ts # src/presentation/impact-levels.ts # src/types.ts # tests/benchmarks/resolution/tracer/tracer-common.sh # tests/graph/algorithms/leiden-native-parity.test.ts # tests/graph/algorithms/louvain.test.ts # tests/graph/cycles.test.ts # tests/integration/check.test.ts # tests/integration/cli.test.ts # tests/integration/exports.test.ts # tests/integration/issue-1742-reexport-symbol-scope.test.ts # tests/integration/issue-1743-incremental-cyclomatic-staleness.test.ts # tests/integration/issue-1773-destructured-binding-kind.test.ts # tests/integration/issue-1781-dynamic-import-destructure-consumer.test.ts # tests/integration/issue-1812-hierarchy-scoped-resolution.test.ts # tests/integration/issue-1825-renamed-import-receiver.test.ts # tests/integration/issue-1852-watch-cha-pts-dynamic-sink.test.ts # tests/parsers/ast-nodes.test.ts # tests/parsers/javascript.test.ts # tests/parsers/lua.test.ts # tests/unit/config-user.test.ts # tests/unit/docs-check-flags.test.ts # tests/unit/hook-extensions.test.ts # tests/unit/hook-track-edits-worktree.test.ts # tests/unit/resolve.test.ts Impact: 61 functions changed, 208 affected
findDbPath's no-DB-found fallback checked `ceiling || rootDirHint`, so whenever rootDirHint sat inside a git repo (the common case) the git ceiling won, producing <git-root>/.codegraph/graph.db instead of the intended <rootDirHint>/.codegraph/graph.db — defeating this PR's own fix for a nested/monorepo rootDirHint. Swapped the priority. resolveStickyModel also called resolveBusyTimeoutMs(dbPath) directly, so when dbPath is undefined (no --db) it read db.busyTimeoutMs from cwd's config instead of rootDir's — the same class of bug this PR fixes for the DB path itself. Fall back to rootDir when dbPath is unset. Added a regression test for the fallback-priority fix; confirmed it fails against the pre-fix code and passes after. Impact: 2 functions changed, 107 affected
Two verification queries chained .prepare()/.get()/.all() directly off a fresh `new Database(...)` without ever closing it. POSIX tolerates unlinking a file with an open handle, masking the leak on macOS/Linux, but Windows refuses — afterAll's fs.rmSync failed with EBUSY: resource busy or locked, unlink '...\graph.db'.
Summary
codegraph embed [dir]andcodegraph build [dir]resolved their default.codegraph/graph.dblocation inconsistently when--dbwas not passed:buildcorrectly defaults to<dir>/.codegraph/graph.db, butembedignored the positionaldirargument entirely and instead searched upward fromprocess.cwd()viafindDbPath(undefined). This letembedsilently attach to an unrelatedgraph.dbsitting between cwd and the git root, or throw a confusing "no database found" error right after a successfulbuild <dir>.Fix
findDbPath()andopenReadonlyOrFail()(src/db/connection.ts) now accept an optionalrootDirHintthat anchors the default-path search at a known project root instead ofprocess.cwd()— mirroringbuild's<rootDir>/.codegraph/graph.dbconvention while preserving the existing git-ceiling-aware upward search and all otherfindDbPathsemantics. Both params are additive/optional, so every other call site (~30 across the codebase) is unaffected.buildEmbeddings()(src/domain/search/generator.ts) — the actual DBembedwrites to — now passes its ownrootDirparameter as that hint instead of defaulting to cwd.resolveStickyModel()(src/cli/commands/embed.ts), which does a pre-flight DB read to reuse a previously-stored embedding model, had the identical bug (callsopenReadonlyOrFail(dbPath)with an undefineddbPathwhen--dbisn't passed) and is fixed the same way.--dbhelp text onembedto matchbuild's documented default.No native (Rust) changes —
embed/buildEmbeddingsis a TS-only search/CLI feature with nocrates/codegraph-coreequivalent.Test plan
tests/search/embedding-strategy.test.tsthat reproduces the exact repro from embed [dir] ignores positional dir for default DB path resolution (inconsistent with build) #1869: two unrelated directories each with their owngraph.db, cwd set to one,buildEmbeddings(targetDir, ..., undefined)called on the other — asserts embeddings land intargetDir's DB and the cwd DB is never touched. Verified this test fails against the pre-fix code and passes after.tests/unit/db.test.tsforfindDbPath's andopenReadonlyOrFail's newrootDirHintparameter (wins over cwd, explicit--dbstill wins over the hint, unchanged default behavior when no hint is given).tests/unit/embed-command.test.tsverifyingembed'sexecute()threads the resolved positionaldirthrough to both the sticky-model lookup andbuildEmbeddings.npm run lint— clean (only a pre-existing unrelated warning in a test fixture).npm run build— clean (tsctypecheck passes).npx vitest run tests/unit/db.test.ts tests/unit/embed-command.test.ts tests/search/embedding-strategy.test.ts— all 78 tests pass.npm test(full suite): 125 pre-existing failures, all intests/parsers/*(missing optional WASM grammars not rebuilt in this worktree) andtests/integration/*/tests/benchmarks/*(missing native@optave/codegraph-darwin-arm64addon in this worktree —node_moduleshere has no@optave/*packages at all). Confirmed none of the failing tests touchdb/,search/,embed, orgenerator— this is a worktree-environment gap (in the spirit of worktree env: stale better-sqlite3 native binary + missing WASM grammars block npm test / titan-gate #1733), not a regression from this change.Fixes #1869
Stacked on #1863 (base branch
fix/issue-1863-resolvebyglobal-creates-a-call-edge-to-every) — only theembed/DB-path-resolution diff above is this issue's change.