| Name | Modified | Size | Downloads / Week |
|---|---|---|---|
| Parent folder | |||
| README.md | 2026-09-18 | 22.5 kB | |
| v2.3.9 source code.tar.gz | 2026-09-18 | 6.2 MB | |
| v2.3.9 source code.zip | 2026-09-18 | 6.4 MB | |
| Totals: 3 Items | 12.7 MB | 0 | |
[2.3.9] - 2026-09-18
Added
visualizecan draw a neighbourhood instead of the whole repository. The D3 page falls back to one bubble per community past 3000 nodes or 9000 edges, which a shallow clone of django (45,111 nodes, 397,762 edges) passes by 15x and 44x, so every serious codebase got the bubble view. Nine new flags seed a view and bound it:--seed-symbol,--seed-file,--seed-changedwith--seed-changed-base,--seed-flow,--path-fromand--path-tofor a shortest path, and--depth,--render-depthand--max-nodesfor how much is carried and drawn. Nodes outside the neighbourhood are absent from the payload rather than dimmed. A hop is a semantic edge (CALLS, IMPORTS_FROM, INHERITS, IMPLEMENTS, TESTED_BY, DEPENDS_ON); CONTAINS is structural and deliberately not a hop. Search, filters, community colouring, the flow selector and the keyboard shortcuts all keep working, and the default output is still one self-contained HTML file. The whole-repository view is unchanged when no seed flag is passed.cross_repo_search_toolacceptsrepos, a list of registry aliases or folder names, to search only part of the registry. Names that match no entry come back inunknownand names that match several entries inambiguous; both lists are bounded like every other list (#915).query_graph_toolresolves dotted targets such asDetails.QueryHandler.Handlethrough an indexednodes.symbolcolumn, added by migration v10. See Upgrade notes for the full schema move (#942, [#934]).updateandbuild_or_update_graph_toolreport files that failed to parse: the status ispartial, the summary names the files and the CLI prints a warning on stderr. A failed file keeps its previous graph rows (#952).CRG_HOOK_WORKTREES=1keeps the generated pre-commit hook active inside a linked Git worktree (#953).CRG_DISCOVERY_TIMEOUTbounds each Git command that discovers what changed when a review tool or command was not handed an explicit file list. It defaults to 5 seconds, capped atCRG_GIT_TIMEOUTso an unset-but-lowered general budget still wins; setting it explicitly is an instruction and is used as given, aboveCRG_GIT_TIMEOUTincluded. When it is unset andCRG_GIT_TIMEOUTwas set explicitly, discovery inherits that value, so the documented remedy for slow Git keeps working. Read on every call rather than frozen at import.CRG_GIT_TIMEOUTkeeps its 30-second default and still governs build, update and watch (#262).stagingis promoted totestingautomatically, once a day, when it has commitstestinglacks, every required status check is green on its tip, and the promotion gate has not failed on thetestingtip..github/workflows/auto-promote.ymlopens the promotion pull request and merges it with a merge commit, so contributor authorship survives; the decision lives inscripts/auto_promote.pyand reads the required contexts from thetestingruleset at run time. It merges only a pull request it opened itself — same repository,staging→testing, labelledauto-promotion, and pinned with--match-head-committo the commit whose checks were read — all re-verified immediately before the merge, so a fork branch namedstaging, a base branch changed after the fact, or a promotion pull request opened by hand cannot be merged by it. A hand-started run defaults to a dry run. Requires Allow GitHub Actions to create and approve pull requests under Settings → Actions → General. Promotion tomainis never automatic and the workflow cannot target it.
Changed
- Keyword search is rebuilt, and it needs a rebuilt graph.
_fts_searchwrapped the whole query in one pair of double quotes, so every multi-word search was an exact-adjacency phrase match: on a build of this repositorypassword hashingreturned 0 rows. Queries are now built from tokens — each quoted, given a prefix term when it carries at least three alphanumeric characters, ANDed first and ORed only if the AND finds nothing, so recall widens without costing precision. A phrase typed in quotes stays a phrase and suppresses the OR widening, and the expression a prose question builds is bounded. Two new indexed columns feed it:nodes.docstring(2,451 of this repository's 6,517 nodes carried a docstring the FTS index could not see, because an external-content FTS5 table can only index real columns of its content table) andnodes.name_tokens, the camelCase splitsunicode61cannot produce, which raises inner-camel segment recall@20 from 24.7% to 54.7% for a 2.7% larger index. An exact-identifier boost and a symbol-name coverage boost keep BM25 precise against the widened candidate set; the shippedsearch_qualitybenchmark goes from MRR 0.667 to 1.000. - A test gap that tests reach only through a caller is reported as its own
class rather than as an unqualified gap.
TESTED_BYis a direct edge, so a private helper the suite exercises only through the public function above it carried none and was reported untested — 5 of the 74 gaps on one measured delta, includingmain.py::_offload. Each changed symbol is now classified in one batched pass: directTESTED_BY, else a two-hop upward walk over incomingCALLSlooking for a tested caller, which is the newGraphStore.get_caller_test_routes. Reachability is reported as a note, never as coverage: the gap set is unchanged, gap order stays class-blind so truncation cannot hide a real gap first, and risk scoring is not discounted — 32 of the 504 symbols the walk reaches are never executed by the suite, and no depth, fan-out or same-file rule tried moved that below ~5%. Reached gaps carrycovered_via,covered_depthandcovered_by, and the rendered pull-request comment gives them their own heading with an "indirect" Tested column. Two new environment variables bound the walk:CRG_CALLER_TEST_ROUTE_DEPTH(default 2 — over the measured delta depths 3, 4 and 5 rescue exactly zero) andCRG_CALLER_TEST_ROUTE_MAX_CALLERS(default 500). - Hints,
next_tool_suggestionsand the prompt templates name the registered tools (detect_changes_tool, notdetect_changes), so agents are no longer told to call tools that do not exist. The generated instruction blocks already named them and are unchanged from 2.3.8, so reinstalling is not needed for this fix (#821). detect_changes,get_review_context,get_affected_flows,get_impact_radius,get_minimal_contextandupdate --briefresolve a branch ref to its merge base withHEAD, matching GitHub's "Files changed" view on divergent branches. Commit IDs and revision expressions are used as given, and a shallow clone that cannot resolve the merge base falls back to the ref (#847).- Per-file node and edge writes, bare-endpoint resolution and signature computation run as batched statements in a single transaction each, instead of one autocommitted row at a time, which cuts build time on large graphs (#858, [#721]).
- The generated pre-commit hook skips automatic checks in a linked
worktree, so a commit there cannot silently create a second graph for
another branch. Detection uses the git directory's
commondirfile and needs onlygit rev-parse --absolute-git-dir(Git 2.13). Reinstall upgrades the exact hook block written by earlier releases (#953). - Every MCP tool that can reach Git discovery, a graph traversal, FTS, an
embedding provider or the filesystem now runs its work on a worker thread
through one shared helper. A tool that exceeds
CRG_TOOL_TIMEOUTanswers withstatus: errornaming itself and the budget, instead of leaving the client to time the request out itself as MCP error -32001. Onlyget_docs_section_toolandlist_repos_toolstill run inline; they read one small file each (#262, [#46], [#136]).
CRG_TOOL_TIMEOUT keeps its meaning: it bounds read-only tools, and it
does not bound build_or_update_graph_tool, run_postprocess_tool,
embed_graph_tool, generate_wiki_tool or apply_refactor_tool. Those
write — to graph.db, to the wiki tree, to your source files — and a
timeout cancels the wait, not the worker, so bounding them would report
failure to the client while the write went on regardless.
- Change discovery is bounded honestly. Each Git command in the chain that
works out what changed gets
CRG_DISCOVERY_TIMEOUT(5 seconds) rather than the 30-secondCRG_GIT_TIMEOUT, and runs withrequire_vcs, so exhausting that budget raises aChangeDiscoveryErrorand the tool answersstatus: error. A shorter budget on its own would not have been enough: discovery that ran out of time silently is what produced [#913]'s false all-clear, and the same silence at the base resolution degrades a three-dot diff into a two-dot one. RaisingCRG_GIT_TIMEOUTstill raises discovery with it, so the documented remedy for slow Git keeps working (#262). get_minimal_context_toolruns its change discovery on that same budget. It previously spent up to ~130 seconds on five Git subprocesses of its own — the tool agents are told to call first, and the one most likely to hit a client's request ceiling (#262).
Fixed
- Go and Ruby imports resolve into the repository instead of staying bare
strings. Go reads the module path from the nearest
go.mod(nested modules win over their ancestors, and localreplacetargets are honoured) and maps an in-repo import to the package DIRECTORY it names; the standard library, undownloaded dependencies and anything the ignore patterns exclude stay unresolved. Ruby resolvesrequire_relativeagainst the requiring file andrequire,load,autoloadandrequire_allagainst the repository's load roots (gemspecrequire_paths,lib,test,spec, the Railsapproots, and$LOAD_PATH.unshiftin a root script); gems stay unresolved.importers_offor cli/cli'spkg/iostreams/color.gogoes from 0 to 424, and for jekyll'stest/helper.rbfrom 0 to 51. - A Go import names a package and a Ruby
require_allnames a directory tree, so both emit ONE edge naming that directory, taggedextra.import_scope, and the read path expands a directory to its member files.importers_ofmatches edges targeting a file's own package,imports_ofreportsimport_target_kind, and the impact traversal follows a package target at every hop without spending one. One edge per package keeps the edge count proportional to import statements rather than to imports times package size: kubernetes/kubernetes records 93,650 import edges where an edge per member file would be 588,972 (73,507 of them from one imported package), and builds in about 146s rather than 524.6s. It also keeps an incremental update in agreement with a rebuild, which a per-member fan-out cannot do, because an edge's targets would then depend on which files were in the package when the importing file happened to be parsed. - Binding imports that previously stayed bare costs build time and disk, on every repository measured, and never saves either. Measured with the import change off and on against the same parent commit — which already carried the batched writes below, so these figures isolate the import change and are not a comparison against 2.3.8: a kubernetes/kubernetes build goes from 115.1s and 122.8s to 148.7s and 146.5s (about 24% slower) and its database from 4.41 GB to 5.36 GB (21.5% larger), run isolated and alternating on one idle machine; cli/cli goes from a 7.6s build and a 281.1 MB database to 9.3s and 325.9 MB. What that buys is that a bound import also lets the call resolver attribute cross-file calls: on cli/cli the CALLS edges bound to an indexed node go from 5503 to 22564 of 67503, with the edge count unchanged.
- The impact traversal's directory branch is pinned to an index seek on
(target_qualified, kind). Left to itself SQLite drove it from the covering index onkindalone and rescanned everyIMPORTS_FROMrow once per frontier directory, which was 18 seconds of a 19-second kubernetes traversal; pinned it is 1.8s, against 6.5s for the same answer under the per-file fan-out.tests/test_import_scope.pyasserts the plan. - No import edge names a path the build does not index. 73,512 kubernetes
import edges named a path the build never indexed, 73,507 of them files
under
vendor/, which**/vendor/**excludes, so every one of them was a confident-looking path that matched no node. Five remain. Such an import keeps its bare module string, which is visibly external. get_impact_radiusbounds every list in its response by a fixed ceiling -- the same 100 nodes, 150 edges and 200 filesget_review_contextapplies to the same radius -- and reportsedges_omitted,changed_nodes_omittedandimpacted_files_omittedalongside the existingnodes_omittedandtotal_impacted.edgesandchanged_nodeshad no ceiling at all andmax_resultswas not exposed on the MCP tool, so the response grew with the repository: 138k tokens on one changed file of cli/cli and 189,163 on one of kubernetes, against a documented 12k budget. That kubernetes response is now 23,669 tokens. Edges that touch the changed code are kept first, andmax_resultsis now an argument ofget_impact_radius_tool.- Every specifier in a Go
import ( ... )block carries its own line. 8636 of cli/cli's 8687 import edges (99.4%) were stamped with the line of theimport (token. - Ruby import extraction dispatches on the call's method name instead of
testing whether the node text contains "require". 31 of jekyll's 227
import edges (14%) were not on a require line at all, with targets such
as
Missing --ssl_cert or --ssl_key. Both are required.; those call subtrees were also being dropped whole, so their calls are now extracted.autoload, which is how jekyll declares its entire internal module graph, produced no edge at all. - Freshness metadata follows what was stored. A no-op
updatethat confirmsHEADadvances the Git anchor, so queries after a commit no longer carry a stale-graph caveat, and a file that fails to parse no longer holds the anchor back for the files that did parse (#952). updatecompares stored hashes with the files on disk, so a change that was indexed and then reverted is re-parsed even when the Git diff is empty. Each indexed file is still read only once per update, and binary checks read only the first 8 KB (#860).- Failed C++ identity migrations are retried file by file instead of
rebuilding files that migrated correctly, and a file that failed and was
then ignored is dropped from the pending set instead of making every
later update report "Identity migration pending for ignored file" and
stopping
watchfrom starting (#944). - The Spring and Temporal resolvers take interface implementors only from Java, Kotlin and Scala classes, so Go struct embeddings are no longer mistaken for Java implementations while Kotlin and Scala implementors keep resolving (#834).
- Go embedded structs and interface composition produce
INHERITSedges again, including groupedtype (...)declarations and generic embedded types (#834). - tsconfig path aliases probe
.mtsand.ctstargets,.mtsand.ctsfiles are indexed as TypeScript in full builds, and.mjsand.cjsimport specifiers resolve to.mtsand.ctssources before falling back to.tsand.tsx(#874). forgetanddead-coderesolve the database the way the other read-only commands do, without creating graph state when none exists, and still migrate a legacy top-level.code-review-graph.dbinstead of reporting "No graph found" (#874).- C, C++ and Bash grammar probes put the parent interpreter's
tree_sitter_language_packlocation on the child's path, so grammars installed in a venv or withpip install --userare no longer reported missing, and probe failures carry an install hint (#810, [#807]). - Go methods whose receiver type carries a comment or parentheses are attached to their type (#872).
- Kotlin import names are read from the AST, including
asaliases, and wildcard imports no longer produce an entry (#873). - PHP
include,include_once,requireandrequire_oncewith a literal path create import edges. Dynamic paths are not evaluated, and the empty-result caveat says so (#875, [#819]). - Python
import a.b as cis extracted as an import ofa.b(#955). get_all_filesno longer fails on malformedextraJSON left behind by a killed writer (#870, [#864]).- Dotted Python module imports (
srclayouts,odoo.addons.*) count as evidence when resolving bare call targets, both in post-processing and inquery_graph(#960, [#903]). - Embedding runs enumerate nodes directly instead of through stored file
paths, so legacy stores with non-canonical paths are embedded, and
embed_graph_toolno longer reports semantic search as active when nothing was embedded (#962). inheritors_ofmarks results found by bare name withinferred_by: "bare_name"when several classes or types share that name (#966).references_toincludes dependents whose import specifier the parser could not resolve (npm workspace alias, tsconfig path mapping), markedtarget_resolution: "unresolved", when the bare name identifies exactly one node (#969).- Source files are read as UTF-8 regardless of the system locale in review and flow snippets and in the eval runner (#928).
- Watch mode drops events on paths the OS cannot stat instead of ending the watch loop (#959, [#897]).
- Watch mode reloads ignore rules when
.code-review-graphignoreor a build manifest changes, removes newly ignored files from the graph, indexes newly included ones, and keeps a source file created at a path previously inferred as build output (#909). - Incremental reconciliation refuses a graph whose File markers only partly overlap the requested root, so files of a parent or sibling repository are not deleted (#909).
- Legacy graph paths are purged by their stored spelling, so normalising them cannot delete a different current row (#911).
- When promotion to a recursive watch fails, the existing watches are kept
and the health file reports
degraded(#927). - A watcher thread that dies before the first liveness tick is detected (#891).
crg-daemon stopkeeps the PID file until the process has exited and exits non-zero if it is still running (#958).- The Qoder skills are bundled in the wheel, so
install --platform qoderworks from a pip install and never copies the target project's ownskills/directory.uninstallremoves those bundled workflows from.qoder/skills/too — it derived the list from the target project's own top-levelskills/directory, which a normal repository does not have, so it removed none of them (#909).
Upgrade notes
- Schema version: 9 → 13. Four migrations land in this release, and they run automatically the first time any command opens the graph:
- v10 adds the indexed
nodes.symbol, back-filled fromqualified_name. - v11 adds the indexed
edges.target_resolution, back-filled over everyCALLSandREFERENCESrow. It is what thereferences_toentry above means bytarget_resolution: "unresolved". - v12 adds
nodes_fts_state, the mirror of the FTS index. - v13 adds
nodes.docstringandnodes.name_tokens, back-fills both —name_tokensin a Python loop over every node — then drops and rebuildsnodes_fts.
v13 is a full-table rewrite plus a complete FTS5 rebuild. On a large graph
(this release measures kubernetes/kubernetes at 5.36 GB) the first open
after upgrading will take a while and is not resumable. Back the database
up first if that matters, or delete it and run code-review-graph build.
Rolling back needs a rebuild. Migrations are forward-only, and 2.3.8 has no newer-database check — it opens a v13 graph without complaining and reads it with a v9 reader. This release adds that check, so a 2.3.9 build reading a future schema says so in one line instead.
- The VS Code extension has to be repackaged and republished. Any
extension build a user can already have installed rejects a v13 database
with "Database was created with a newer version (schema v13). Update the
extension." — the only tagged release,
vscode-v0.2.1, accepts v6, and the untagged 0.2.2 in this repository accepted v9. The extension is versioned and published separately from the Python package, so upgrading the CLI does not upgrade it. This repository now carries 0.3.0, which accepts v13; until that.vsixis built and installed, the Code Graph panel will report the error above after the CLI migrates the graph. - Run
code-review-graph buildonce to get the parser fixes.updateskips any file whose hash is unchanged, and nothing stamps the parser version into the graph, so a file that has not been edited is never re-parsed after an upgrade. Every parse-time fix above — Go and Ruby import resolution, per-specifier Go import lines, Rubyrequiredispatch, Kotlin imports, PHPinclude/require,import a.b as c, Go receiver comments, Go embeddedINHERITS, tsconfig.mts/.cts— changes a file's rows only when that file is re-parsed.importers_ofon a Go package stays at 0 until then, and theextra.import_scoperead path has no edges to expand. The search changes are the exception: migration v13 back-fillsnodes.docstringfrom what the parser already stored inextra, so keyword search improves without a rebuild. - Re-run
code-review-graph installto pick up the hook and config changes. The generated pre-commit hook is not self-updating; reinstall is what replaces an earlier release's block with the one that skips automatic checks in a linked worktree. - GitHub Action cache. The cache key segment moves from
schema9toschema13, so the first run after upgrading restores no graph cache and builds from scratch. Self-hosted runners can delete the oldcode-review-graph-schema9-*entries.