fix(watcher): handle both endpoints of a rename; serialise concurrent updates - #1424
Merged
Merged
Conversation
… updates
- on_moved processed only event.dest_path, so the node for the source
path and every symbol in it stayed in the graph forever. on_created,
on_modified and on_deleted all handle src_path; only the move path
ignored it. Every rename duplicated every symbol in the file, and a
normal refactoring session — or a `git checkout` between branches —
accumulated them indefinitely until find_callers began returning dead
paths. Verified live with a running watcher:
baseline: File nodes = newname.py, oldname.py alpha* rows = 4
fixed: File nodes = newname.py alpha* rows = 2
- Debounce is keyed per path, so N files changed inside the interval
fire N threading.Timer threads running _handle_modification in
parallel, with no lock anywhere in RepositoryEventHandler. Concurrent
handlers do read-modify-write on the shared imports_map (lost updates)
and interleave delete_file_from_graph / add_file_to_graph /
delete_outgoing_calls_from_files for overlapping caller sets, so one
can delete edges another has just created. A branch switch or
`git pull` is the normal trigger. Graph updates are now serialised on
an RLock.
- Timers were never removed from self.timers after firing, so the dict
grew without bound for the life of the watcher. Entries are now
dropped when the timer runs, and self.timers is guarded by its own
lock (cancel_timers could previously race with _debounce).
Three existing tests in test_graph_builder_perf_fixes.py construct the
handler through __new__ and set attributes by hand; they now also supply
the locks that __init__ creates. Making the lock optional instead would
have reintroduced exactly the bug being fixed.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Contributor
|
Hi! 👋 Join our CodeGraphContext Discord channel to collaborate: https://discord.gg/dR4QY32uYQ |
Contributor
🔍 PR Code Graph Analysisfix(watcher): handle both endpoints of a rename; serialise concurrent updates (#1424) 📊 Interactive VisualizationView the blast radius graph: PR Reviewer Dashboard 📦 ArtifactsThe graph JSON has been uploaded as a build artifact: Generated by CodeGraphContext using FalkorDB Lite |
Shashankss1205
added a commit
that referenced
this pull request
Jul 31, 2026
* Update Footer.tsx
* fix(extension): clean up event listeners on panel disposal
* fix: stop pip install button from drifting on hover
* docs: add code of conduct
* fix: remediate E2E audit findings and release v0.5.1
Address critical MCP/API parity bugs, watcher incremental relink regressions,
database backend reliability, parser correctness, CLI safety, and security
hardening identified in the full codebase audit.
Co-authored-by: Cursor <cursoragent@cursor.com>
* Improve indexing progress handling in CLI
* feat: shared embedded DB layer, multi-backend embeddings, SCIP CALLS pass
Extract Kùzu/Ladybug into database_embedded_kuzu with GraphQueryInterface (M4),
add multi-backend embedding/vector resolve, harden deploy templates, lint CI,
and wire Tree-sitter CALLS resolution into the SCIP indexing pipeline.
Co-authored-by: Cursor <cursoragent@cursor.com>
* feat: added active status indicator of purple color to navbar links
* feat: improve comparison section UI with responsiveness and enhancements
* feat: enhance Interactive Visualizations section with animations and full-screen modal
* feat: describe your change here
* docs: add contribution guide
* Improve README documentation with inline explanations
* Removed --file option from find variable command
* Fix Python graph builder NoneType split crash (issue #1334)
Three crash sites fixed:
- tools/languages/python.py: _get_node_text() returns '' instead of None
- tools/indexing/resolution/inheritance.py: guard .split() calls in
resolve_inheritance_link() when base_class_str is None
- tools/indexing/resolution/inheritance.py: guard .split() in
build_embeds_links() when base list entry is None
Fixes #1334
* Guard imp['name'] .split() across all call sites (calls.py, inheritance.py, graph_builder.py)
Additional None-guard fixes discovered during full-index testing:
- inheritance.py:95 — guard imp['name'].split('.') via (imp.get('name') or '')
+ filter out imports with no name
- inheritance.py:326 — guard imported.split('.') when imported is None
- calls.py:1754,2145 — same guard as inheritance.py:95
- graph_builder.py:531 — same guard
Fixes #1334
* fix: wire up scroll-to-top button and fix invalid JSX in MoveToTop
* fix: update Open Graph and Twitter meta tags
* fix(ci): re-baseline stale parser goldens to restore green CI
The parser golden regression tests (tests/integration/test_parser_goldens.py)
have failed on main for ~18 consecutive runs, blocking CI on every open PR.
The language parsers evolved but the recorded baselines were never refreshed.
Regenerated all 21 language baselines via the sanctioned --update-goldens flag
(tree-sitter 0.25.2 / language-pack 0.13.0, matching CI). Verified locally:
21/21 golden projects pass. Net effect: +71 nodes, +209 edges captured
(mostly parser improvements, incl. new Emacs Lisp support 0->26 nodes).
A few languages show a net node/edge loss reflecting genuine parser regressions
already shipped in v0.5.1 (Perl -17, C# -8, Swift -4, Java -1); these will be
filed as separate tracking bugs.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* test: fix 10 pre-existing stale test failures blocking CI
Beyond the golden re-baseline, main had 10 unit/integration failures + 1 e2e
failure, all from production changes that intentionally evolved behavior
without updating the tests. None are real regressions:
- python parser: exclude synthetic <module> frame (added by _attach_module_context)
- embeddings/vector resolver (5): fake driver now reports neo4j backend so the
multi-backend detection path matches the mocked neo4j responses
- graph-builder CALLS queries (2): assert new inline line-match + backtick-quoted
labels instead of the old WHERE-equality form
- CLI inventory (2): account for the new 'bundle merge' git-driver subcommand
- e2e test_clean_up: enable ALLOW_DB_DELETION for the delete journey
Verified locally: all 10 + e2e pass.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Index all Go packages with scip-go (./... instead of .) (#1337)
scip-go was invoked as `scip-go index .`, which indexes only the current
package (Go's `.` package pattern). On any multi-package repository this
produced a near-empty index and CGC silently fell back to Tree-sitter, so
SCIP mode was effectively disabled for real-world Go repos.
Use the recursive `./...` pattern in both the local _build_command path and
the Docker fallback override.
Add a unit test on _build_command for the Go branch. The golden regression
suite runs with SCIP_INDEXER=false, so it cannot exercise the scip-go
command path — a unit test is the correct level for this fix.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix: per-repo context resolver must not overwrite explicit CGC_RUNTIME_DB_TYPE (#1254)
Two related bugs that together cause silent backend mis-routing, particularly
on Windows where FalkorDB Lite is unavailable:
**Bug 1 — server.py: resolver verdict unconditionally overwrites process env**
`MCPServer.__init__` sets `os.environ["CGC_RUNTIME_DB_TYPE"] = ctx.database`
whenever `resolve_context()` returns a non-empty `database` field. This
overwrites any value the user (or launcher wrapper) already placed in the
process env — e.g. via `CGC_RUNTIME_DB_TYPE=falkordb-remote` in a project's
`.mcp.json` env block — with whatever the resolver defaulted to.
Fix: only write when the variable is not already set:
`if ctx.database and not os.environ.get("CGC_RUNTIME_DB_TYPE"):`
**Bug 2 — config_manager.py: per-repo branch ignores process env for default**
When `resolve_context()` takes the per-repo branch (local `.codegraphcontext/`
folder present), it initialises `local_db` from
`load_config().get("DEFAULT_DATABASE", "falkordb")`. `load_config()` reads
`.env` files from disk; it does not consult the current process environment.
So `CGC_RUNTIME_DB_TYPE=falkordb-remote` in the MCP launcher env is invisible
here and the resolver defaults to bare `"falkordb"`.
Fix: check process env before file-based config:
`local_db = (os.environ.get("CGC_RUNTIME_DB_TYPE")
or os.environ.get("DEFAULT_DATABASE")
or load_config().get("DEFAULT_DATABASE", "falkordb"))`
An explicit `database:` key in the repo-local `config.yaml` still takes
precedence over all of the above (`local_db = local_raw.get("database", local_db)`).
**Combined failure mode (Windows + per-repo folder)**
With both bugs active: a repo with a `.codegraphcontext/` folder but no
pinning `config.yaml` causes the per-repo branch to fire, default to
`database="falkordb"` (Lite), write that back to `CGC_RUNTIME_DB_TYPE`, and
then `get_database_manager()` picks up "falkordb". FalkorDB Lite is
unavailable on Windows (`core/__init__.py:93-98` falls back to KuzuDB). The
KuzuDB single-writer lock then crashes `cgc mcp start` with a -32000 error,
leaving a zombie process and silently discarding the `falkordb-remote`
settings the user configured.
**Workaround** (still needed until this is released): add a
`.codegraphcontext/config.yaml` pinning `database: falkordb-remote` to any
repo that uses the remote backend and carries a local `.codegraphcontext/`
folder.
* fix: read COMPLEXITY_THRESHOLD from config instead of hardcoded default (#1293)
The `cgc analyze complexity --threshold` option had a hardcoded default
of 10 that ignored the COMPLEXITY_THRESHOLD config value set via
`cgc config set COMPLEXITY_THRESHOLD <value>`.
Changed the default to None and added logic to read from config when
not explicitly provided via CLI, falling back to 10 if the config
value is missing or invalid.
* fix(cgcignore): anchor directory patterns to path-segment boundaries (#1367)
CGCIgnoreMatcher compiled non-root-anchored patterns with an optional
`(?:.*/)?` prefix and applied them via re.search, so a directory pattern
like `out/` matched the substring inside a segment — `layout/`, `checkout/`,
`workout/`, `logout/` — and likewise `build/` matched `rebuild/`, `target/`
matched `retarget/`. discover_files_to_index then pruned those directories,
silently dropping every file under them from the graph (no output when app
logs are at WARNING+).
Require a path-segment boundary before the token by using `(?:\A|.*/)` so a
bare `out/` matches the segment `out` only — consistent with gitignore
semantics. Root-anchored, file (`*.png`) and wildcard patterns are unaffected.
Adds a regression test.
Fixes #1366.
* Fix repo_path scoping in find_all_callers/callees; slim find_callers payload (#1364)
* fix: repo_path scoping in find_all_callers/find_all_callees
In find_all_callers/find_all_callees the repo_filter referenced `caller.path`
/ `callee.path`, but those variables are out of scope after the
`WITH p, nodes(p) as path_nodes, relationships(p) as rels, ... as last_node`
projections. On FalkorDB this raises "'caller' not defined" whenever repo_path
is passed, making both tools unusable with a repo filter.
Reference in-scope variables instead: the ultimate caller is the path start
node (path_nodes[0]); the deepest callee is last_node. who_calls_function is
left untouched (its `caller` is bound by the MATCH and stays in scope).
Verified against a FalkorDB backend: find_all_callers('run_cmd', repo_path=...)
now returns the expected callers instead of erroring.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* perf: slim down find_callers payload (~26-43% fewer tokens)
who_calls_function returned redundant fields per row that inflate the tool
output consumed by LLM agents:
- caller_docstring: almost always null noise
- caller_is_dependency: only used for ordering (kept in ORDER BY)
- full_call_name: redundant with the queried target
- target_file_path: the target's own file, a constant repeated on every row
Drop these from the three RETURN clauses and hoist target_file_path to the
result envelope once. Kept: caller_function, caller_file_path,
caller_line_number, call_line_number, call_args. Measured against a FalkorDB
backend: run_cmd 1066->786 tokens (-26%), getClient 1791->1015 (-43%).
Note: this changes the find_callers output shape (drops fields, adds a
top-level target_file_path). Happy to gate it behind a flag if preferred.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* perf: raise find_all_callers/callees result cap 100 -> 500
find_all_callers/find_all_callees UNWIND the CALLS edges of a variable-length
path and return DISTINCT ... LIMIT 100. For high-fan-out targets this silently
truncates the transitive caller/callee set at 100 edges (e.g. a core helper
called across a whole codebase). Raise the cap to 500.
Note: there is also a per-tool default of 50 for these query types in
utils/tool_limits.py, but it is currently NOT applied to analyze_code_relationships
results — the handler trims only 'if isinstance(results, list)', while
analyze_code_relationships returns a dict — so the Cypher LIMIT is the effective
cap. Flagging that as a separate latent issue; this commit only raises the
effective (Cypher) limit.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: adpcpess <175317047+adpcpess@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix decorator resolution with missing import source (#1350)
* feat: add Python 3.14 parser compatibility (#1263)
Update tree-sitter dependency constraints to use tree-sitter-language-pack 1.6 across supported Python versions, including Python 3.13 and 3.14.
Expand CI coverage to exercise Python 3.12, 3.13, and 3.14, install the parsing extra in workflows, and document the new dependency bounds while leaving the backend driver pins from main intact.
Amp-Thread-ID: https://ampcode.com/threads/T-019ec9fc-a435-7370-844d-7b114747334e
Co-authored-by: Amp <amp@ampcode.com>
* Create test_real_sse.py (#1318)
* feat: add critical_logger() to debug_log.py for complete log level coverage (closes #1232) (#1359)
* Fix watch-mode imports map race (#1261)
* fix: snapshot imports map during call resolution
Snapshot the shared imports map before building the per-language import view so concurrent watch-mode updates cannot resize the dictionary during iteration.\n\nAdd a regression test that mutates the same map from another thread while the import filter is reading it.
* test: harden imports map race regression
Increase bounded waits in the concurrency regression test and delegate unused Path attributes to the wrapped pathlib.Path object.
* ci(security): add CodeQL static analysis workflow (#1265)
* removed testing scripts (closes #1115) (#1240)
Signed-off-by: D4rk-Pho3nix <manish.srmist23@gmail.com>
* fix-issue-1226 (#1325)
* fix: add missing self parameter to get_cypher_query() in placeholder language toolkits (closes #1281) (#1361)
* fix: harden parser tests for portable temp files (#1205)
Co-authored-by: Saurabh Kumar Bajpai <saurabhkumarbajpaiai@Saurabhs-MacBook-Air.local>
* Skip SCIP documents outside the project root instead of crashing ingest (#1351)
SCIP indexes can reference documents outside the project root — e.g.
scip-go emits Go build cache paths (~/Library/Caches/go-build/...) for
cgo/generated code. should_skip_file() let such paths through (its
relative_to ValueError handler returned False), so they reached the
persistence writer whose unconditional relative_to(repo) raised
ValueError and aborted the entire ingest. Combined with --force
deleting the old index first, a single out-of-root document left the
database nearly empty.
Fix by rejecting out-of-root paths at the ingest filter, and apply the
filter unconditionally (previously it was skipped entirely when no
ignore spec was built), since the out-of-root check is independent of
.cgcignore. Follow-up to #1073, which fixed the adjacent ignored-
directory bypass in the same filter.
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
* fix: validate setup wizard credentials (#1207)
Co-authored-by: Saurabh Kumar Bajpai <saurabhkumarbajpaiai@Saurabhs-MacBook-Air.local>
* feat: implement visualize_graph.py utility with HTML graph rendering (closes #1229) (#1360)
* feat: ai-powered code knowledge graph explorer with natural language querying #1166 (#1300)
Co-authored-by: kashviporwal-byte <kashviporwal-byte@users.noreply.github.com>
* fix: honor CGC_RUNTIME_DB_PATH env var in _default_global_db_path (#1295)
The MCP server resolves its DB path via _default_global_db_path,
which only consulted FALKORDB_PATH for falkordb and otherwise
hardcoded the default under CONFIG_DIR. On Windows machines
where CONFIG_DIR contains non-ASCII characters (e.g. C:\Users\<name>\),
this crashes KuzuDB with a path encoding error:
RuntimeError: IO exception: Cannot open file.
path: C:\Users\<name>\.codegraphcontext\global\db\kuzudb
By checking CGC_RUNTIME_DB_PATH first (matches what cgc index and
cgc stats already honor via _initialize_services), the MCP server
uses the same relocated path and stays consistent with the CLI.
* feat: add --summarize flag to index command (#1235)
* perf(calls): make CALLS resolution linear instead of quadratic (#1304)
build_function_call_groups was O(total_calls x distinct_functions) on
large repos. cProfile (300-file slice) pinned two hot spots:
1. functions_named() scanned the entire function_index on every call in
the callback-argument pre-pass (~48% of runtime). Precompute a
name -> [(file_path, func)] dict once (O(F)); build order matches the
previous per-call scan order exactly, so resolved output is unchanged.
2. Path(x).resolve().as_posix() was called ~172k times, hitting the
filesystem (realpath/lstat) on the same paths repeatedly (~18%).
Memoize via a module-level lru_cache, cleared per run for freshness in
long-lived processes.
Measured on a 1,491-file Python repo (build_function_call_groups only,
parse excluded): 257.6s -> 6.5s (39.5x); the curve goes from quadratic to
linear in file count. Output is byte-identical: every resolved edge
(caller/called/line/resolution_tier/confidence/labels) matches before and
after, verified on Python, JavaScript and Java by running old vs new on
the same parsed input.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* Update tree_sitter_parser.py (#1329)
* Detect TypeScript monorepo workspaces for scip-typescript (#1338)
scip-typescript indexes a single tsconfig-rooted project by default. A
monorepo has no root tsconfig.json — its packages are enumerated by a
pnpm-workspace.yaml or a package.json `workspaces` field — so the bare
`index` failed and CGC silently fell back to Tree-sitter, disabling SCIP
for real-world TypeScript monorepos.
Detect the workspace layout in the typescript branch and pass the matching
scip-typescript flag (--pnpm-workspaces / --yarn-workspaces; yarn and npm
share the workspaces field, and scip-typescript has no npm-specific flag).
Single-project behaviour is unchanged.
Add unit tests on _build_command covering pnpm, yarn/npm (array + object
form), precedence, and malformed package.json.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat: add opt-in GCF output format for 62% fewer tokens on tool responses (#1290)
Add CGC_OUTPUT_FORMAT=gcf environment variable to encode tool responses
using GCF (Graph Compact Format) instead of JSON.
Measured on CodeGraphContext's own data shapes:
- find_callers (12 results): 63.9% fewer tokens
- find_dead_code (15 functions): 60.2% fewer tokens
- Cypher query (20 records): 61.5% fewer tokens
- complex_functions (10 results): 66.7% fewer tokens
- class_hierarchy (8 classes): 60.6% fewer tokens
- Overall: 62.5% reduction
GCF is an optional dependency (pip install gcf-python). Falls back to
JSON silently if not installed. Zero behavior change without the env var.
* [Feature]: Improve Repository Analysis Summary UI and Graph Visibility (#1187)
* feat: add repository analysis summary panel
* chore: remove generated all_files.txt
Removed the generated `all_files.txt` file from the repository.
This file was unintentionally included in the previous commit and is not required for the application's functionality. Removing it keeps the repository clean, reduces unnecessary file additions in the pull request, and aligns with project contribution guidelines.
No functional changes were made.
* feat: improve repository analysis layout and graph visibility
* Make 'rich' optional: add lightweight fallback Console/Table in config_manager.py (#1249)
* docs: improve README onboarding (#1211)
Co-authored-by: Saurabh Kumar Bajpai <saurabhkumarbajpaiai@Saurabhs-MacBook-Air.local>
* docs: add beginner setup steps (#1206)
Co-authored-by: Saurabh Kumar Bajpai <saurabhkumarbajpaiai@Saurabhs-MacBook-Air.local>
* fix: make watch startup sync opt-in (#1269)
Amp-Thread-ID: https://ampcode.com/threads/T-019ecf16-9928-72d1-8fd5-e6627e7e0df5
Co-authored-by: Amp <amp@ampcode.com>
* docs: remove hardcoded Neo4j env variables from default MCP configuration to support zero-config setup (#1314)
* closes #1103 (#1239)
* Feature/docker hub publishing (#1241)
* feat: dockerize project for docker hub distribution
* fix: address PR feedback on entrypoint, compose version, and npm ci fallback
* fix(cli): allow visualize to bind to custom host for Docker accessibility
* test(e2e): fix test_clean_up by enabling ALLOW_DB_DELETION
* feat(server): implement prompt text truncation safety (#1238)
* closes #1102
Signed-off-by: D4rk-Pho3nix <manish.srmist23@gmail.com>
* added test case ( closes #1102 )
Signed-off-by: D4rk-Pho3nix <manish.srmist23@gmail.com>
---------
Signed-off-by: D4rk-Pho3nix <manish.srmist23@gmail.com>
* Make local SCIP indexer subprocess timeout configurable (#1336)
The local SCIP indexer subprocess was capped at a hardcoded 300s. Large
repositories whose indexer runs longer than 5 minutes were killed and
silently fell back to Tree-sitter (e.g. scip-typescript on a large
monorepo measured at 381s > 300s), with no visible failure.
Add a SCIP_LOCAL_INDEXER_TIMEOUT_SECONDS config key (default 300, so
existing behaviour is unchanged) read from the CGC config at call time.
Non-numeric or non-positive values fall back to 300. Users can raise it
with:
cgc config set SCIP_LOCAL_INDEXER_TIMEOUT_SECONDS 1800
Scope is limited to the local indexer path; the Docker fallback (600s)
and the go-mod-tidy pre-step (120s) are unchanged.
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* Add architecture diagram (#1278)
* feat: enhance Interactive Visualizations section with animations and full-screen modal
* docs: add Mermaid architecture diagram for CGC workflow
* fix: buffer full request body before SSE JSON-RPC parsing (#1113) (#1319)
* Update mcp_sse.py
* Create test_mcp_sse.py
---------
Co-authored-by: Shashank Shekhar Singh <Shashankshekharsingh1205@gmail.com>
* test(api): mock request.body() in SSE disconnect tests
PR #1319 made handle_messages() buffer the request body up front
(await request.body()), but the disconnect-test mock_request didn't
expose an awaitable body(), so test_handle_messages_exits_cleanly_on_disconnect
began failing with 'MagicMock can't be used in await'. Add an AsyncMock
body() returning a valid JSON-RPC payload so the test exercises the
disconnect path again. Restores green main.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* chore(release): bump version to 0.5.2
Published to PyPI: https://pypi.org/project/codegraphcontext/0.5.2/
Includes the CI restoration + ~48 merged PRs since 0.5.1.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* Fix BUG-001: Prevent local .env from overriding global config in global mode (#1283)
* docs: add architecture overview diagram (#1257)
Co-authored-by: Saurabh Vishwakarma <saurabhvishwakarma.7054@gmail.com>
* feat: repository digital twin and architectural evolution simulator (#1299)
Co-authored-by: kashviporwal-byte <kashviporwal-byte@users.noreply.github.com>
* feat: Add cgc prompt command for custom LLM prompts (#583) (#613)
* feat: implement cgc prompt command for custom LLM prompts (issue #583)
- Add new 'cgc prompt' CLI command group with add/list/remove subcommands
- Create project_config.py module for managing .cgc/config.json
- Store custom prompt paths in project-level config (relative paths)
- Implement build_system_prompt() to prepend custom prompts at runtime
- Integrate with MCP server initialization to inject custom prompts
- Add file validation, duplicate prevention, and missing file handling
- Maintain full backward compatibility when no prompts are configured
- Use proper logging for runtime warnings
Changes:
- src/codegraphcontext/cli/project_config.py (new)
- src/codegraphcontext/cli/main.py (add prompt command group)
- src/codegraphcontext/prompts.py (add build_system_prompt function)
- src/codegraphcontext/server.py (use build_system_prompt in initialize)
Resolves #583
* fix: resolve serverInfo merge-conflict syntax error
---------
Co-authored-by: Shashank Shekhar Singh <Shashankshekharsingh1205@gmail.com>
* fix: label heuristic call edges separately (#1208)
* fix: label heuristic call edges
* test: align CALLS-query assertions with heuristic-label change
---------
Co-authored-by: Saurabh Kumar Bajpai <saurabhkumarbajpaiai@Saurabhs-MacBook-Air.local>
Co-authored-by: Shashank Shekhar Singh <Shashankshekharsingh1205@gmail.com>
* feat(javascript): compute cyclomatic_complexity for functions and methods (#1148) (#1231)
* feat: compute cyclomatic_complexity for JS functions/methods (#1148)
* test: regenerate JavaScript golden for cyclomatic_complexity (#1231)
---------
Co-authored-by: Shashank Shekhar Singh <Shashankshekharsingh1205@gmail.com>
* feat: Multi-graph support for FalkorDB (#776)
* feat: Multi-graph support for FalkorDB
FalkorDB supports multiple graphs per instance (each stored as a
separate Redis key). This change allows each indexed repository to
use its own graph, selected via an optional graph_name parameter.
Changes:
- FalkorDBManager and FalkorDBRemoteManager now cache multiple graph
handles and accept graph_name in get_driver()
- New list_graphs() method and list_graphs MCP tool to discover
available graphs
- Optional graph_name parameter added to all query, analysis, and
management tool definitions
- graph_name threaded through handlers, CodeFinder, GraphBuilder,
and CGCBundle
- Neo4j/KuzuDB get_driver() accepts (and ignores) graph_name for
interface uniformity
- Fully backward compatible: defaults to FALKORDB_GRAPH_NAME env var
(or 'codegraph') when graph_name is not specified
Known limitation: graph_name is accepted by add_code_to_graph's
schema but the indexing pipeline currently writes to the default
graph only. Reads, deletes, and schema creation are graph-aware.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test: Update fixtures for multi-graph API
Adjust test doubles and singleton-reset helpers to match the new
get_driver(graph_name=None) signature and the _graphs dict (replacing
the old single _graph attribute):
- test_database_falkordb_remote.py: _graph → _graphs dict in singleton
resets and connection-state tests; set _driver alongside _graphs so
is_connected() short-circuit behaves correctly.
- test_graph_builder_perf_fixes.py: GraphBuilder.driver is now a
property; fixture provides a fake db_manager whose get_driver()
returns the recording driver instead of assigning gb.driver directly.
- test_kuzu_relationship_queries.py / test_cli_commands.py: fake
DBManager get_driver() accepts optional graph_name.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(graph_builder): drop self.driver assignment that conflicts with @property
The PR defines driver as a @property (no setter); the merge accidentally
kept upstream's instance assignment from __init__, which raised
AttributeError "property 'driver' of 'GraphBuilder' object has no setter"
on construction.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test: update mock get_driver to accept graph_name kwarg
The PR widened DatabaseManager.get_driver() to take an optional
graph_name. Upstream's new test files use ad-hoc mock managers
defining get_driver(self) with no kwargs, so query_handlers calling
get_driver(graph_name) raised TypeError. Mirror the production signature.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test(falkordb_schema): stub db_manager instead of driver property
The PR refactored GraphBuilder.driver from instance attribute to
@property (no setter), so test stubs that set gb.driver directly now
fail. Give the stub a fake db_manager whose get_driver() returns the
desired _FakeDriver — same effective wiring through the property.
* fix(db): accept graph_name kwarg on ladybug and nornic managers
When tools call get_driver(graph_name) for multi-graph FalkorDB support,
the other backends were raising TypeError because their get_driver()
signatures didn't accept the kwarg. KuzuDB was already fixed; this
extends interface parity to ladybug and nornic.
* chore(report): refresh CGC report timestamp
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* test: update delete assertion for graph_name kwarg
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: Shashank Shekhar Singh <Shashankshekharsingh1205@gmail.com>
* fix: nested call context misattribution and multi-line import capture (#1292)
* fix: resolve nested call context misattribution and multi-line import capture
Two related bugs in the Python parser that cause dead-code false
positives and missing cross-file CALLS edges:
1. Nested call context misattribution (Type A)
When a function call is nested inside a method's arguments
(e.g. results.append(_helper(data))), the inner call's context
was incorrectly set to the method name ("append") instead of the
enclosing function ("process"). Since method names are not
Function nodes in the graph, this creates orphan CALLS edges and
causes dead-code analysis to report false positives.
Fix: Pass local_names (set of locally defined function/class
names) through _walk_call_tree. In _record_call, only use
enclosing_caller as context when it is in local_names; otherwise
fall back to _get_parent_context. In _walk_call_tree, only
propagate called_name as next enclosing_caller for direct calls
(identifier type); attribute calls (obj.method()) should not act
as nested callers.
2. Multi-line from...import capture (Type B1)
_find_imports used child_by_field_name('name') which only returns
the first import name. For multi-line imports like
from pkg import (a, b, c, d, e), only 'a' was captured and the
remaining 4 names were silently dropped, breaking cross-file
CALLS edge resolution.
Fix: Use children_by_field_name('name') to capture ALL imported
names from from...import statements.
3. Stale test assertion fix
test_parse_simple_function expected len(funcs) == 1 but
_attach_module_context injects a synthetic <module> frame.
Updated to filter out <module> before asserting.
Tests:
- test_nested_call_inside_method_uses_enclosing_function_context
- test_method_name_collision_uses_enclosing_function_context
- test_multiline_from_import_captures_all_names
- test_multiline_from_import_with_alias
All 9 Python parser tests pass.
* test: regenerate sample_project golden for nested-call-context fix (#1292)
---------
Co-authored-by: Shashank Shekhar Singh <Shashankshekharsingh1205@gmail.com>
* fix(security): restrict apoc procedures to a minimal allowlist (#1006) (#1405)
* feat(security): optional API key auth for HTTP endpoints (#1008) (#1406)
* fix(security): enforce read-only at session level for user queries (#1010) (#1407)
* Fix CLI/core safety bugs: bundle unpack, hook git-absent, config data loss (#1379)
- registry_commands.load_bundle_command: unpack the 4-tuple returned by
_initialize_services (db_manager, graph_builder, code_finder, ctx) instead
of 3 values, which raised ValueError: too many values to unpack and leaked
the DB driver. Driver is still closed in the finally block.
- hook_manager: route git subprocess calls through a _run_git helper that
converts a missing git binary (FileNotFoundError) or a failed config call
(CalledProcessError) into HookError, so `cgc hook install/uninstall/status`
no longer crash with an uncaught exception when git is absent (callers only
handle HookError).
- config_manager.save_context_config: read-merge the existing config.yaml
before writing so unrelated top-level sections (notably workspace_mappings)
are preserved instead of being erased. Prevents config data loss. Mirrors
the read-merge pattern used by _save_workspace_mappings. Adds a unit test.
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* fix: parser robustness (bugs.md) (#1381)
- typescript/javascript parse(): errors='ignore' + try/except like other langs
- discovery/graph_builder: index literal dotfiles (.gitignore/.dockerignore/.env) by name
- cpp_toolkit: accept caller label form (fix Unsupported query type)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* fix: cleanup deprecations, stale fork URLs, null-guards (bugs.md) (#1380)
- report_generator: datetime.utcnow() -> timezone-aware now()
- pyproject/README/api/app: Shashankss1205 fork URLs -> org URLs
- registry_commands: guard None name/description JSON fields
- embeddings: guard embed_batch length mismatch (don't silently drop)
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* feat: added vercel analytucs (#715)
* feat: add floating scroll navigation buttons (#1182)
* fix(registry): stop leaking GITHUB_TOKEN to huggingface.co; use POSIX paths for scoping (#1409)
- _github_headers() attached `Authorization: token $GITHUB_TOKEN` and was
used on exactly one request: the registry manifest on huggingface.co.
A developer with GITHUB_TOKEN exported — routine in CI and dev shells —
leaked it to a third-party host on every registry call. Hugging Face
requests now use HF_TOKEN/HUGGING_FACE_HUB_TOKEN via a separate header
builder; _github_headers stays for github.com callers.
- HF_REGISTRY_REPO was interpolated into the manifest URL unvalidated, so
a crafted value could control the host portion and redirect the request
(and any credential on it) anywhere. It is now matched against
`owner/name` and falls back to the default with a warning.
- cgc_bundle scoped repositories with `str(repo_path.resolve())` and
`os.sep`, but every graph path is written through
writer._normalize_path -> Path.resolve().as_posix() (#1080). On Windows
the prefix could never match, so `cgc bundle export --repo <path>`
produced a valid-looking but empty bundle. All 8 resolve() sites and 7
separator concatenations now use POSIX form.
- The same native/POSIX mismatch existed in the pre-scan imports_map
across 14 language parsers. Resolution normalises at the end so the
final payload was fine, but the intermediate substring heuristics
compare a '/'-joined import name against the stored path and could
never match on Windows, silently downgrading resolution to the
AMBIGUOUS first-candidate tier.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(graph): stop silent failures in schema DDL, FalkorDB errors and label discovery (#1408)
Four independent graph-layer defects, all of which fail quietly.
- create_graph_schema ran ~43 DDL statements inside a single try/except
whose only handler was a warning. The first failure skipped every
remaining statement, so the graph could end up without indexes on
Class, Variable, Parameter and the rest; every MERGE then degenerated
into a full label scan, turning indexing quadratic with nothing
surfaced (the warning is invisible under the shipped default
ENABLE_APP_LOGS=CRITICAL). Each statement now runs independently and
the failures are reported with a count and the offending statements.
- FalkorDBSessionWrapper.run treated any error containing "already
exists"/"already created"/"already indexed" as benign — for *every*
query, not just DDL. A genuine data-write failure was therefore
converted into an empty success wrapper (.data() == [], .single() is
None), so callers raised a TypeError far from the real cause. The
allow-list now applies only when the statement is schema DDL, matched
on its leading keyword rather than by searching the whole query text
(which also matched those words inside string literals).
- _get_all_node_labels fell off the end of its Kùzu/Ladybug branch,
returning None, when discovery found no labels. The caller iterates
the result, so delete_repository_from_graph aborted with a TypeError
*after* it had already deleted every relationship, leaving a
half-deleted repository. It now falls through to the canonical
NODE_LABELS list.
- FalkorDBManager.close_driver dropped its reference without releasing
the redis connection pool, so connected_clients grew by one per
get_driver()/close_driver() cycle and never came back down — in a
long-running MCP server that switches contexts, trending toward
maxclients.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(indexer): merge pre-scan symbols, reach generic dotfiles, surface parse failures (#1410)
- pre_scan_for_imports used dict.update() per language, which *replaces*
the value list. A symbol defined in two languages therefore kept only
the last-scanned language's paths: in a polyglot repo a Java `Widget`
and `render` disappeared behind the Python ones, so cross-file
CALLS/INHERITS resolution for them fell to a lower-confidence tier or
failed outright. Paths are now merged, de-duplicated, order-preserving.
- `.gitignore` and `.dockerignore` were listed among generic
*extensions*, but dotfiles have no suffix (Path(".gitignore").suffix
== ""), so they never matched and never got a File node. Moved to the
filename set, in both discovery and the GraphBuilder copy that is
supposed to stay in sync with it. `.env` is deliberately NOT
reinstated: making it reachable would newly pull secrets-bearing files
into the graph (#1313).
- Parse failures were invisible. Most parsers catch their own exceptions
and return {"path", "error"} instead of raising, which is the same
shape as the benign "generic file type" and "no parser" returns, so a
genuinely broken file took the identical pipeline branch as a .md file
and the run still reported success with no failure count anywhere.
parse_file now marks real failures with `parse_failed`, the pipeline
collects them before the error entries are dropped, and the count is
reported in the summary table and the logs.
Verified end to end on a repo containing a latin-1 encoded .py file:
| Total scanned files | 3 |
| Files failed to parse | 1 | <- previously absent
| Function nodes | 2 |
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(cgcignore): honour user negations and stop `**` crossing segment boundaries (#1403)
Two independent .cgcignore matching bugs.
1. Built-in defaults were appended *after* the user's patterns in both
`build_ignore_spec` and `read_cgcignore_patterns`. Matching is
last-match-wins, so the defaults unconditionally overrode the user's
rules and a `!build/` negation could never re-include a directory the
default list ignores. Source under build/, dist/, out/, target/ and
env/ was therefore unindexable with no escape hatch. The comment on
read_cgcignore_patterns claimed the opposite of what the order did.
2. `**` was translated by splitting on the literal token, stripping the
surrounding slashes and re-joining with a bare `.*`. That let the
wildcard match *within* a path segment, so `docs/**` also ignored
`docstring.py` and `docs_helper/`, `**/build` also ignored `rebuild/`
and `xbuild`, and `vendor/**/*.go` also ignored `vendorabc.go`.
Files silently vanished from the graph with no diagnostic.
`**` now joins with a separator-aware bridge: leading `**/` becomes
`(?:.*/)?`, trailing `/**` requires at least one child segment, and an
interior `/**/` matches zero or more whole segments.
Adds a parametrized regression test covering both the must-not-match and
must-still-match directions for `**`, plus a test that a user negation
overrides a default. Updates the merge-order assertion, which documented
the old (incorrect) precedence; the patterns there contain no negations,
so its matching behaviour is unchanged.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: add nornic to valid backends in config db command (#1414)
The config_db command in main.py had a hardcoded allowed list that
was missing 'nornic', causing 'cgc config db nornic' to return an
error even though nornic is a fully supported backend.
config_manager.py, core/__init__.py, and all documentation already
treated nornic as valid. This commit aligns main.py with the rest
of the codebase.
Fixes #1400
Co-authored-by: suraj kumar <157868021+suraj-k-umar@users.noreply.github.com>
* docs: document database deletion safety flag (#1412)
ContribPilot: CP-0065
Issue: CodeGraphContext/CodeGraphContext#1401
* fix(mcp): warn on destructive tools, flag Spring truncation, correct stale docs (#1413)
MCP contract fixes:
- list_jobs_tool was the only handler without **args, but dispatch is
handler(**args) and its schema declares no additionalProperties:false,
so a client sending any extra key triggered a TypeError that surfaced
as -32603 Internal error — indistinguishable from a server crash.
- delete_repository and load_bundle were described as "Delete a
repository from the graph." and "Load a pre-indexed bundle.", with
nothing marking them irreversible. load_bundle.clear_existing had no
description at all despite dropping previously indexed data — to a
model it reads like "clear the existing copy of this bundle". Both now
state the consequence. (The ALLOW_DB_DELETION guard still defaults
closed; this is about the contract the model reads.)
- find_java_spring_endpoints and find_java_spring_beans hard-capped at
100 rows with no `truncated` flag, unlike every sibling tool, so a
300-endpoint app silently reported 100 as complete. Both now return
`truncated` and `result_limit`.
- The Cypher fallback guidance read "use the correct property names
(e.g. `path` vs `path`)" — a corrupted instruction.
Docs corrections, all verified against the code:
- contributing.md documented CGC_LOG_LEVEL and CGC_SKIP_REINDEX, neither
of which appears anywhere in the codebase. The real knobs are
DEBUG_LOGS / ENABLE_APP_LOGS / LIBRARY_LOG_LEVEL.
- config.md listed the SCIP_LANGUAGES default as 5 languages; it is 10.
- indexing.md and cli.md stated watchers reconcile the graph with disk on
startup. sync_on_start defaults to False; --sync-on-start is required
and was mentioned in neither doc.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(cli): repair bundle load and index exit codes (Typer default leakage) (#1415)
Two criticals with one shared root cause and one shared symptom.
Root cause: Typer resolves parameter defaults only when *it* invokes a
command. A command that calls a sibling as a plain Python function must
pass every Typer-defaulted parameter explicitly, or the callee receives
the `typer.models.OptionInfo` sentinel — which is also truthy.
- `cgc bundle load` and `cgc load` failed 100% of the time, before the
bundle was even read:
Error: Context '<typer.models.OptionInfo object at 0x...>' is not
registered. Create it with: cgc context create <typer.models.
OptionInfo object at 0x...>
bundle_load called bundle_import without `context`, and load_shortcut
called bundle_load without it in turn. Both now forward it, and both
commands gained the --context flag their sibling `bundle import`
already had.
- Auditing the class turned up two more instances the report missed:
registry_download -> bundle_import dropped `yes`, and index_abbrev ->
index dropped `summarize`. The latter is user-visible: because
OptionInfo is truthy, `cgc i` *always* printed the codebase summary.
- Separately, `cgc index` exited 0 on every failure. `typer.Exit`
subclasses RuntimeError and its str() is empty, so the bare
`except Exception` caught it, printed nothing and returned 0 — CI
pipelines running `cgc index && deploy` treated indexing failure as
success, and this is also how the symlink abort (I-C1) exits 0.
typer.Exit is now re-raised ahead of the generic handler, which itself
exits 1.
Verified end to end: `cgc bundle load` and `cgc load` import successfully;
`cgc index <missing>` and `cgc index --force <missing>` exit 1; a valid
index still exits 0; `cgc i` prints no summary and `cgc i --summarize`
does.
Adds an AST-based test that fails if *any* command delegates to another
without supplying its Typer-defaulted parameters, so the class cannot
regress.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(cli): count nodes not rows in stats, make doctor honest, allow report --repo (#1411)
- `cgc stats <path>` counted rows, not nodes. A variable-length
`-[:CONTAINS*]->` match yields one row per distinct path to a node, and
a method is reachable both as Repo->..->File->Function and as
Repo->..->File->Class->Function (a nested function three ways). On
CGC's own repo that inflated functions by 55%: `cgc stats` reported
5501 but `cgc stats <path>` reported 8531. Now count(DISTINCT ...).
- `cgc doctor` section 2 is titled "Checking Database Connection", but
for the default falkordb backend the whole check was an import probe —
it never opened a connection, unlike the neo4j and falkordb-remote
branches. It now connects and runs a trivial query, and reports when
the backend actually in use differs from the configured one. On this
machine that immediately surfaced a real silent fallback:
✓ FalkorDB Lite is installed
✓ FalkorDB Lite connection successful
⚠ Configured backend is 'falkordb' but 'kuzudb' is actually active
- doctor also printed "All diagnostics passed! System is healthy." while
a ⚠ was on screen. Warnings are now tracked separately from failures
and the summary distinguishes the two (exit code is unchanged: warnings
still exit 0).
- `cgc report` had no way to target a repository; with several indexed it
silently picked the one with the most files, disclosed only in the
report body. generate_report already accepted repo_path — the CLI just
never passed it. Adds --repo/-r.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(cli): correct delete exit code, -h/--host collision, duplicate command and messages (#1404)
Five small user-facing defects.
- `cgc delete <not-indexed>` printed "Repository not found in graph" and
exited 0, so `cgc delete X && ...` treated a no-op as a successful
delete. The not-found branch now exits 1, and the bare `except
Exception` (which also returned 0) does too. `typer.Exit` is re-raised
ahead of it so the code survives.
- `-h` was bound to `--host` on `cgc visualize`, so `cgc visualize -h`
died with "Option '-h' requires an argument". `--host` moves to `-H`,
and `-h` is registered as a --help alias app-wide via context_settings
so it now works on every subcommand, not just the root.
- `index` carried a duplicated `@app.command()` decorator.
- The bundle-import conflict message told CLI users to "Use
clear_existing=True", which is the MCP tool parameter; the CLI flag is
--clear. It now names both.
- `cgc index <relative-path>` echoed the resolved absolute path on start
but the raw argument on completion.
Adds regression tests for the delete exit codes, `-h` on five
subcommands, `--host -H`, and single registration of `index`.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(security): validate bundle labels and relationship types before interpolation (#1417)
Importing a .cgc bundle executed arbitrary Cypher as the CGC user.
Node labels and relationship types cannot be passed as query parameters —
they are interpolated into the query text — so a bundle is an untrusted
source of executable Cypher unless every identifier is validated.
_validate_bundle checked only that four files exist and that
metadata.json has a cgc_version key (never comparing its value); labels
and relationship types were never checked at all.
A bundle whose node carried the label
Evil) WITH n MATCH (v:Victim) DETACH DELETE v //
produced, and ran, exactly this (verified by recording the query handed
to the driver):
CREATE (n:Evil) WITH n MATCH (v:Victim) DETACH DELETE v //) SET n = $props RETURN id(n) as new_id
Bundles are a first-class distribution mechanism with a public registry,
downloaded with no checksum and no signature, and load_bundle is
reachable from the HTTP gateway — so this is remotely triggerable.
Every label and relationship type is now required to match
[A-Za-z_][A-Za-z0-9_]* before it reaches a query. Validation runs twice:
once up front over nodes.jsonl and edges.jsonl so the bundle is rejected
before anything is written (the import batches with no transaction, so
lazy validation would let a malicious label halfway through the file run
after earlier nodes had committed), and again at each interpolation site
as a backstop.
$ cgc bundle import evil.cgc
Import failed: Invalid bundle: Refusing to import bundle: invalid node
label 'Evil) WITH n MATCH (v:Victim) DETACH DELETE v //'. Node labels
must match [A-Za-z_][A-Za-z0-9_]* — a value outside that set can inject
arbitrary Cypher.
EXIT=1
Legitimate bundles are unaffected: export/import of a real repository
round-trips unchanged.
Note this fixes the injection only. Bundle provenance — no checksum, no
signature, no version compatibility check — remains open.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: stop counting <module> as a function, declare HEURISTIC_CALLS on Kuzu (#1421)
- The Python parser adds a synthetic `<module>` frame per file as the
attribution target for module-level calls. It was counted as a real
Function and reported as dead code — the first row of
`cgc analyze dead-code` was an artifact that does not exist in the
source, and every function total was inflated by one per Python file.
The frame is now marked `is_synthetic`, excluded from the index
summary (with a name-based fallback so graphs indexed before the marker
existed still count correctly) and excluded from the dead-code query.
before: 2 files -> Function nodes 3
after: 2 files -> Function nodes 1
- Incremental update re-parses changed ∪ callers ∪ inheritors and feeds
all of them back through link_function_calls, but only caller_paths had
their outgoing CALLS cleared first. On Neo4j/Nornic the writer uses
CREATE rather than MERGE, so the inheritance-only neighbours had their
edges re-created on top of the existing ones and duplicates multiplied
on every save. FalkorDB and Kùzu use MERGE, which is why this never
showed up locally.
- Found while verifying the above: `cgc analyze dead-code` is completely
broken on KùzuDB. writer.py emits HEURISTIC_CALLS for resolution tier
>= 8, but the rel table was never declared in the Kùzu schema, so those
edges could not be written and every query matching
[:CALLS|HEURISTIC_CALLS] failed with "Binder exception: Table
HEURISTIC_CALLS does not exist". Reproduced on pristine main. Declared
with the same bindings as CALLS. KùzuDB is the Windows default and the
fallback backend everywhere else.
- Corrected the schema.py comment claiming the FalkorDB indexes are
"sufficient for MERGE to perform correct deduplication". Indexes do not
enforce uniqueness — CALL db.constraints() returns [] — deduplication
holds because MERGE dedupes within an UNWIND and FalkorDB serialises
writes per graph, which is an assumption worth stating rather than a
guarantee the schema provides.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(indexer): a symlinked file no longer aborts the entire index run (#1418)
_normalize_path calls .resolve(), which follows symlinks, so any symlink
inside a repo pointing outside it makes relative_to raise. One call in
add_file_to_graph was bare — unlike the guarded one 17 lines above it and
its sibling in add_minimal_file_node — and the ValueError unwound all the
way out of run_tree_sitter_index_async.
Reproduced on a repo containing normal.py, zzz_last.py and
sub/link.py -> ../../outside/target.py:
before: An error occurred during indexing: '.../outside/target.py' is
not in the subpath of '.../repo'
EXIT=0
Functions in graph: normal_fn (zzz_last_fn never indexed)
File nodes: .../outside/target.py (orphan, outside the repo
prefix, so delete_repository_from_graph cannot
remove it)
after: Total scanned files 3 | Function nodes 6 | EXIT=0
Functions in graph: normal_fn, outside_fn, zzz_last_fn
The symlink target is now indexed without a directory hierarchy and a
warning is logged, instead of taking the run down with it.
Also wraps the per-file graph writes in the pipeline so that no single
unwritable file can abort a run. There is no transaction around that
loop, so an escaping exception left a partially written graph with no
rollback and every remaining file silently unindexed; failures are now
collected, logged, and counted alongside parse failures.
Note the exit code stays 0 here only because this run now succeeds — the
separate `cgc index` exit-code bug is fixed in the PR for #1385.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix: classify Cypher DDL by leading keyword; disambiguate nested functions (#1423)
Two independent correctness bugs that both silently produce wrong answers.
1. Query classification was a substring search over the whole query text:
if "CREATE FULLTEXT INDEX" in query.upper():
return "RETURN 1"
which also matches those words inside a *string literal*. An ordinary
read query was therefore replaced by "RETURN 1", returning a
fabricated value with the wrong shape and no error:
$ cgc query 'MATCH (f:Function) WHERE f.source CONTAINS "CREATE INDEX"
RETURN count(f) AS c'
[ { "1": 1 } ] <- was; should be [ { "c": N } ]
Both backends had the pattern (FalkorDB's _translate_schema_query and
Kùzu's _rewrite_kuzu_compat_patterns). Detection moves to a shared
utils/cypher_ddl.py that blanks string literals and comments, then
anchors on the statement's leading keyword. execute_cypher_query is
the designated expert fallback for LLM agents, so an agent inspecting
CGC's own schema-management code hits this directly.
2. The nested-function CONTAINS write matched the enclosing function by
name and path only:
MATCH (outer:Function {name: row.outer, path: $file_path})
so any file with two same-named functions — the same method name on
two classes, which is extremely common — got a false containment edge
from each of them. Reproduced:
class Alpha:
def run(self): # line 2
def inner_helper(): ... # line 3
class Beta:
def run(self): ... # line 8
before: inner_helper CONTAINED by run@2 AND run@8 (run@8 is false)
after: inner_helper CONTAINED by run@2 only
The Python parser already computed the enclosing definition's line —
_get_parent_context returns (name, type, line) — but destructured it
into `_` and threw it away. It is now carried as context_line (and
class_context_line, which the sibling class_fn_batch query needs and
which the parser never populated) and used to disambiguate the match.
Parsers that do not report a line pass -1, which keeps the old
name-only behaviour rather than dropping the edge.
All 20 parser golden tests pass, so the added parser fields do not
change existing extraction.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(watcher): handle both endpoints of a rename; serialise concurrent updates (#1424)
- on_moved processed only event.dest_path, so the node for the source
path and every symbol in it stayed in the graph forever. on_created,
on_modified and on_deleted all handle src_path; only the move path
ignored it. Every rename duplicated every symbol in the file, and a
normal refactoring session — or a `git checkout` between branches —
accumulated them indefinitely until find_callers began returning dead
paths. Verified live with a running watcher:
baseline: File nodes = newname.py, oldname.py alpha* rows = 4
fixed: File nodes = newname.py alpha* rows = 2
- Debounce is keyed per path, so N files changed inside the interval
fire N threading.Timer threads running _handle_modification in
parallel, with no lock anywhere in RepositoryEventHandler. Concurrent
handlers do read-modify-write on the shared imports_map (lost updates)
and interleave delete_file_from_graph / add_file_to_graph /
delete_outgoing_calls_from_files for overlapping caller sets, so one
can delete edges another has just created. A branch switch or
`git pull` is the normal trigger. Graph updates are now serialised on
an RLock.
- Timers were never removed from self.timers after firing, so the dict
grew without bound for the life of the watcher. Entries are now
dropped when the timer runs, and self.timers is guarded by its own
lock (cancel_timers could previously race with _debounce).
Three existing tests in test_graph_builder_perf_fixes.py construct the
handler through __new__ and set attributes by hand; they now also supply
the locks that __init__ creates. Making the lock optional instead would
have reintroduced exactly the bug being fixed.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(parsers): stop silently dropping non-UTF-8 source files (#1425)
Nine parsers opened source with encoding="utf-8" and no errors= handler,
while the other 20 already passed errors="ignore". Among the nine are the
most-used languages: Python, JavaScript, TypeScript, TSX, Go, CSS, HTML,
Ruby and Elixir — 15 open() call sites in total.
A UnicodeDecodeError was caught upstream and turned into a result with an
`error` key but no `unsupported` key, which the pipeline treats exactly
like a .md file: the file got a bare File node, contributed no symbols,
and the run still reported "Successfully finished indexing" with no
failure count anywhere. A codebase with legacy latin-1 files lost them
entirely, in silence.
Verified end to end on a latin-1 encoded .py file:
before: Function nodes from latin.py = NONE
after: caf_handler, plain_one
Uses errors="ignore" to match the convention already in the other 20
parsers. The audit recommended errors="replace"; I measured both and
"ignore" is better here:
replace -> 'def caf�_handler():' -> tree-sitter extracts "_handler"
ignore -> 'def caf_handler():' -> tree-sitter extracts "caf_handler"
U+FFFD is not a valid identifier character, so "replace" truncates the
symbol name at the bad byte; "ignore" keeps a usable, still-distinct
identifier.
All 20 parser golden tests pass, so extraction for well-formed sources is
unchanged.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(cli): confirm single-repo delete; stop unwatch/watching reporting success (#1426)
- `cgc delete <path>` destroyed a repository's entire graph with no
confirmation, no --yes flag, no dry run and no undo, while `--all`
required a typer.confirm *and* typing the literal string "delete all".
Proven with stdin closed: the delete completed and exited 0. The
asymmetry was actively misleading — anyone who had seen the heavy
--all guardrails would reasonably assume single deletes were guarded
too. It now prompts, showing the resolved path, and takes --yes/-y for
CI. The `rm` shortcut forwards the flag explicitly (omitted, it keeps
its truthy OptionInfo sentinel and would skip the prompt — the same
delegation bug as #1415).
- `cgc unwatch` and `cgc watching` are advertised in `cgc --help` as
"Stop watching a directory for changes." and "List all directories
currently being watched." Both bodies are console.print only; no
watcher state is touched, and both exited 0. `cgc unwatch` accepted a
path that had never been watched and did not exist, then echoed it
back as "Path specified:", which reads like confirmation. They now
report plainly that the operation is not available from the CLI, point
at the two things that do work (Ctrl+C, or the MCP tool), and exit 1.
Their help text is marked [MCP only].
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(viz): make visualization work without the unshipped viz/dist bundle (#1419)
pyproject.toml declares `codegraphcontext = ["viz/dist/**/*"]` and
MANIFEST.in has `recursive-include src/codegraphcontext/viz/dist *`, but
src/codegraphcontext/viz/dist is never produced — there is no build step
that creates it — so every published wheel shipped without it. That made
`cgc visualize`, the `cgc v` shortcut, the global -V/--visual flag and
the MCP visualize_graph_query tool all hard-fail with SystemExit(1). The
error message told users to run ./scripts/sync_viz_dist.sh, which did
not exist either.
Two changes.
- utils/visualize_graph.py is a complete, dependency-free, single-file
HTML force-directed renderer that had no callers anywhere in the
package. It is now wired up as the fallback when viz/dist is absent, so
the command degrades to a working (simpler) visualization instead of
exiting 1:
$ cgc visualize --repo <path>
Falling back to the built-in offline renderer.
Rendered 10 nodes and 9 edges to /tmp/cgc_graph_xxxx.html
EXIT=0
The renderer handles both driver shapes: Neo4j/FalkorDB return objects
with .labels/.type, while Kùzu and Ladybug return plain dicts carrying
_label plus _src/_dst. Getting this wrong yields a silently empty
graph, so both are covered by tests. Note Kùzu relationships also carry
_label, so _src/_dst is what distinguishes them from nodes.
- Adds the missing scripts/sync_viz_dist.sh: builds the frontend in
website/ and syncs website/dist into src/codegraphcontext/viz/dist so
the packaging declarations that already exist actually have something
to package. It should run before `python -m build` in the release
workflow.
The offline output is fully self-contained — asserted in tests — so it
works from a bare `pip install` with no assets and no network.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* test(cli): strip ANSI before asserting on Rich-rendered help output (#1428)
Three tests I added assert on `result.output` from CliRunner. Rich emits
colour codes (and soft-wraps) whenever it believes the terminal supports
them, which it does on CI but not in a plain local run — so these passed
locally and failed on main, with the expected text plainly visible in the
failure message, just interleaved with escape sequences:
assert '--host -H' in '...\x1b[1;36m-\x1b[0m\x1b[1;36m-host\x1b[0m
\x1b[1;32m-H\x1b[0m...'
Affected: test_visualize_host_moved_to_capital_h,
test_report_accepts_a_repo_flag, test_bundle_load_accepts_a_context_flag.
Each file now normalises with a local _plain() that strips ANSI and
collapses whitespace. Verified both ways: FORCE_COLOR=1 and without, the
unit suite is 901 passed / 7 skipped / 0 failed in both.
The helper is duplicated rather than shared because tests/ has no
__init__.py anywhere and adding one changes pytest's collection mode.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* test(cli): cover the prompt sub-app in the canonical command smoke matrix (#1432)
test_all_canonical_cli_commands_run_with_kuzudb asserts that every command
discovered in main.py is exercised by the smoke matrix. The `prompt`
sub-app (add/list/remove) was added without matrix entries, so the test
has been failing:
Extra items in the right set:
('prompt', 'list')
('prompt', 'remove')
('prompt', 'add')
Confirmed pre-existing by reproducing it at 6be7627, before the recent
batch of audit fixes.
Adds the three entries; `add`/`remove` take a path, so the test writes a
throwaway prompt file under tmp_path.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* ci(docker): publish to GHCR when Docker Hub credentials are absent (#1437)
Docker Build & Publish has been failing on every push to main at the
"Log in to Docker Hub" step:
##[error]Username and password required
DOCKERHUB_USERNAME / DOCKERHUB_TOKEN are not configured for this
repository, and because the login step ran unconditionally the whole job
aborted there — taking the GHCR publish, the image smoke test and the
provenance attestation down with it, even though GHCR authenticates with
the built-in GITHUB_TOKEN and needs no extra secret.
The `secrets` context is not available in a step-level `if`, so the
credentials are probed once in a new step that exposes `has_dockerhub`,
`images` and `primary_image` as outputs. Docker Hub login is gated on
that, the metadata action tags only the registries we can actually push
to, and the smoke test and attestation follow whichever registry is
primary.
Behaviour is unchanged once the secrets are added: both images are
tagged and pushed exactly as before. Until then the job publishes to
GHCR and emits a ::warning:: instead of failing.
Multi-line step outputs use the heredoc form; the %0A escape only worked
with the deprecated ::set-output command.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* ci(docker): lowercase the GHCR image name in the smoke test (#1438)
#1437 got the workflow past the Docker Hub login and the image now builds
and pushes to GHCR successfully, but the next step failed:
docker run --rm ghcr.io/CodeGraphContext/CodeGraphContext:main cgc --version
##[error]Process completed with exit code 125
Container registries reject uppercase in image names, and
${{ github.repository }} is "CodeGraphContext/CodeGraphContext".
docker/metadata-action lowercases its own tags — the run log shows it
pushed ghcr.io/codegraphcontext/codegraphcontext:main — but the
primary_image output I added carried the raw mixed-case value straight
into `docker run`.
Lowercases GHCR_IMAGE once and uses that for both the metadata image list
and primary_image. The Docker Hub image is already lowercase, so that
branch is unaffected.
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
* ci(docker): gate the Docker Hub description update on credentials too (#1439)
Third and final s…
Shashankss1205
added a commit
to Extrodox/CodeGraphContext
that referenced
this pull request
Aug 8, 2026
main added _update_lock/_timers_lock after this branch was cut (CodeGraphContext#1424, serialising concurrent watcher updates). Tests that build the handler via __new__ must set them, as the neighbouring tests in this file already do.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #1391 and the watcher-concurrency finding (
I-M11) from the 0.5.2 audit.1. Renaming a file left the old node and all its symbols forever (#1391)
event.src_pathwas never processed, sodelete_file_from_graph(src)never ran.on_created,on_modifiedandon_deletedall handlesrc_path— only the move path ignored it.Verified live, with a running
cgc watch --poll, renamingoldname.py→newname.py:alpha*function rowsmain)newname.py,oldname.pynewname.pyEvery rename duplicated every symbol in the file. A refactoring session — or a
git checkoutbetween branches — accumulates them indefinitely untilfind_callersstarts returning dead paths.2. Concurrent handlers with no locking (
I-M11)Debounce is keyed per path, so N files changed inside the interval fire N
threading.Timerthreads running_handle_modificationin parallel, and there was no lock anywhere inRepositoryEventHandler. Concurrent handlers:self.imports_map(lost updates), anddelete_file_from_graph/add_file_to_graph/delete_outgoing_calls_from_filesfor overlapping caller sets, so one handler can delete edges another has just created.A branch switch or
git pullis the normal trigger. Graph updates are now serialised on anRLock; a test spawns 6 concurrent handlers and asserts max observed concurrency is 1.3. Unbounded timer growth
Timers were never removed from
self.timersafter firing, so the dict grew for the life of the watcher. Entries are now dropped when the timer runs, andself.timershas its own lock —cancel_timerscould previously race with_debounce. This is plausibly related to #744 ("CGC Watch continuously consumes memory until it eats all available RAM"), though I have not confirmed it is the whole story there.The first live rename test failed before reaching any of the above:
That is the missing Kùzu rel table I fix in #1421 — and it means incremental watching does not work at all on KùzuDB, which is the Windows default and the universal fallback backend. That is a bigger blast radius than the
analyze dead-codebreakage documented on #1421; I've noted it there too.To verify this PR live I temporarily applied #1421's schema addition, then reverted it —
git diff upstream/mainon this branch showswatcher.pyonly.Tests
8 new tests, 6 fail against unpatched
main: both rename endpoints processed, directories ignored, stale-node removal (including surviving a failing delete), fired timers removed, superseded timers cancelled, and handler serialisation under 6 concurrent threads.Three existing tests in
test_graph_builder_perf_fixes.pybuild the handler via__new__and set attributes by hand; they now also supply the locks__init__creates. Making the lock optional instead would have quietly reintroduced the bug.Full unit suite: 740 passed, 7 skipped, 0 failed (excluding
test_cgcignore_patterns.py— flaky onmain, see #1422).Closes #1391