Repository navigation
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) <[email protected]>
|
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 <[email protected]> * 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 <[email protected]> * 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) <[email protected]> * 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) <[email protected]> * 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) <[email protected]> * 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) <[email protected]> * 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) <[email protected]> * 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) <[email protected]> --------- Co-authored-by: adpcpess <[email protected]> Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]> * 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 <[email protected]> * 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 <[email protected]> * 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 <[email protected]> * 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 <[email protected]> * fix: validate setup wizard credentials (#1207) Co-authored-by: Saurabh Kumar Bajpai <[email protected]> * 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 <[email protected]> * 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 <[email protected]> * 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) <[email protected]> * 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 <[email protected]> * docs: add beginner setup steps (#1206) Co-authored-by: Saurabh Kumar Bajpai <[email protected]> * 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 <[email protected]> * 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 <[email protected]> * added test case ( closes #1102 ) Signed-off-by: D4rk-Pho3nix <[email protected]> --------- Signed-off-by: D4rk-Pho3nix <[email protected]> * 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) <[email protected]> * 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 <[email protected]> * 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 <[email protected]> * 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 <[email protected]> * 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 <[email protected]> * feat: repository digital twin and architectural evolution simulator (#1299) Co-authored-by: kashviporwal-byte <[email protected]> * 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 <[email protected]> * 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 <[email protected]> Co-authored-by: Shashank Shekhar Singh <[email protected]> * 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 <[email protected]> * 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) <[email protected]> * 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) <[email protected]> * 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) <[email protected]> * 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) <[email protected]> * 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) <[email protected]> * test: update delete assertion for graph_name kwarg --------- Co-authored-by: Claude Opus 4.7 (1M context) <[email protected]> Co-authored-by: Shashank Shekhar Singh <[email protected]> * 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 <[email protected]> * 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 <[email protected]> * 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 <[email protected]> * 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 <[email protected]> * 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) <[email protected]> * 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) <[email protected]> * 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) <[email protected]> * 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) <[email protected]> * 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 <[email protected]> * 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) <[email protected]> * 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) <[email protected]> * 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) <[email protected]> * 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) <[email protected]> * 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) <[email protected]> * 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) <[email protected]> * 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) <[email protected]> * 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) <[email protected]> * 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) <[email protected]> * 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) <[email protected]> * 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) <[email protected]> * 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) <[email protected]> * 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) <[email protected]> * 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) <[email protected]> * 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) <[email protected]> * 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) <[email protected]> * 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 branch was successfully deployed
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