[2.4.0] - 2026-11-10
Two new platforms, MiMo Code and DeepSeek Harness, so install now supports
18. HTML, XSLT, Liquid, JSON and plain YAML files are indexed, and so are
.cjs and .kts files, which every build used to skip. The graph links calls,
imports and tests more carefully: a call it could not check no longer looks
like a real link, every call edge says how sure it is, calls to inherited
methods and imports of workspace packages are followed, and tests_for finds
Java, Ruby and PHP tests. The graph schema moves from 13 to 15. Several new
settings are off unless you turn them on. Read Upgrade notes at the end of
this section before you upgrade.
Added
- DeepSeek Harness is a supported platform (#1100, #1101, thanks @sumingwang233).
install --platform dshadds oneinsertentry for DSH's own MCP client to
~/.dsh/cordis.patch.yml(or$DSH_HOME/cordis.patch.yml), which every DSH
profile reads, and the graph block toAGENTS.md. Comments,!!jsvalues and
other entries in the file are kept byte for byte, anduninstalltakes the
entry out again. Plaininstallpicks DSH up when its home folder exists. - HTML pages (
.html,.htm) are indexed (#521). They used to give nothing at
all. Inline scripts are parsed as JavaScript or TypeScript with the page's own
line numbers, and a<script src>becomes an import of the local file, so
importers_oflists the pages that load a script. Anonclickthat calls a
function on the page becomes a call. Adiv,sectionorarticlewith an id
becomes a newDocSectionnode, which you can find by its anchor or its first
heading. The body text is not indexed. Style blocks and heading anchors stay
out, and pages over 2 MB keep only their file node. Runbuildto pick up
pages in a graph you already have. - XSLT stylesheets (
.xsl,.xslt) are indexed (#1052). Each template and
xsl:functionbecomes aFunctionnode, named by itsnameor written as
match=para mode=toc.xsl:importandxsl:includebecome imports, and
xsl:call-templatebecomes a call.xsl:apply-templatesis chosen from the
input document when the stylesheet runs, so it becomes a call only where its
selectnames a step or itsmodenarrows the choice. Where neither does, no
edge is added anddynamic_apply_counton the template says so. A template with
amatch, named or not, is never reported as dead code. Runbuildto pick up
stylesheets in a graph you already have. - CI now installs every direct dependency at its declared minimum, on each
supported Python, and runs the test suite there.tests/test_dependency_floors.py
keepspyproject.tomland the pinned list in step in both directions. (#896) - A
Dockerfilethat works with Docker and Podman, so the tool can run from a
container. It installs this source, includes Git, and runs as a non-root user.
Mount a repository at/workspace; the graph stays in that repository. It
stores/workspacepaths, so use the container for later commands or rebuild
on the host. No image is published. See the README. (#1037) - Liquid templates (
.liquid) are indexed. A template getsVariablenodes for
assignandOutputnodes for{{ ... }}, andrenderandincludelink to
thesnippets/<name>.liquidfile, soimporters_ofanswers who renders a
snippet and it counts towards impact.raw,comment,doc,schema,
javascriptandstylesheetblocks,#comments and quoted text are not read
as code. Names hold no line number, so an edit above them does not rename them,
and a repeated name is one node that lists every line.Outputnodes are found
by search only withkind="Output": on Shopify's Dawn theme they otherwise took
all ten top hits for "product". (#878, #348) get_impact_radius_tool(detail_level="compact")andcode-review-graph impact --detail-level compactreturn the blast radius as ranked repository-relative
file paths and counts, with no node or edge records.max_files(default 25,
at most 200) limits each file list without shrinking the traversal, and
changed files the graph has no nodes for are listed inunmatched_files. The
CLI can also ask for--detail-level minimal.build --enrichandupdate --enrichresolve Python method calls on a
local variable with Jedi, from theenrichmentextra (PR #823). For
s = make()thens.authenticate(), the CALLS edge to the bare name
authenticatethen namessvc.py::OldService.authenticate, so
callers_ofand the impact radius find the call. Jedi was a declared
extra that no build ever ran. It is off unless you ask for it, with the
flag orCRG_ENABLE_ENRICHMENT=1(for hooks, the watcher and MCP builds),
because it is slow: on an 88-file Python repository a full build went from
about 5 to 32 seconds and resolved 43 more calls. Having the extra installed,
for example through[all], changes nothing on its own: the graph and the
build time stay the same.CRG_DISABLE_ENRICHMENT=1keeps it off even
when asked for. With it on,updatescans only the Python files it
re-parses, plus their dependents, whose edges it rewrites, and a target
with no node in the graph is never written.get_knowledge_gaps_toolnow reportsimport_cycles: groups of files that
import each other, with one loop written out and its import lines.load_time
says whether the loop can fail as the files load;falsemeans every loop has
an import that runs late: inside a function, a dynamicimport()or a Ruby
autoload. To tell these apart, import edges now record
extra.import_context:type_only,deferredorparent_package. Run
code-review-graph buildonce to get it on an existing graph. Part of #434.query_graph_toolhas a newpath_topattern that answers how one symbol or
file reaches another:pattern="path_to", target="A", to="B", or
code-review-graph query path_to A --to B. Each hop says how sure it is, and
the answer names the weakest hop. A longer route through bound edges beats a
shorter one that guesses at a name.code-review-graph install --steeringadds an opt-in Claude Code hook that puts
graph context beside each search and read.enrichhas existed for a long time,
butinstallnever wrote the hook that runs it. It is off by default because it
changes what the model sees on every such call.--no-steeringremoves only
that hook, anduninstallremoves it too. A failed lookup never stops the tool
call, and a broken graph or odd input now leavesenrichsilent.serve --http --multi-reporuns one HTTP server for every repository in the
registry, so the embedding model is loaded once (#607). Each tool call must
name a registered repository inrepo_root. A call with norepo_root, a
relative path or a path that is not registered is refused with a clear
message. Before, a shared HTTP server quietly answered such a call from
--repoor its own working directory, which could be a repository the
client never named. The registry is read on every call, soregisterand
unregistertake effect without a restart. The flag needs--httpand
cannot be combined with--repo,--auto-watch,--multi-worktreeor
CRG_DATA_DIR; the daemon still watches several repositories. stdio and
plainserve --httpare unchanged.- An opt-in local log of context savings, and a
context-historycommand to
read it (#813). SetCRG_CONTEXT_HISTORY=1and every response that carries
acontext_savingsestimate (the impact, review-context, detect-changes and
compact architecture tools, and thedetect-changes,update --brief,
impactandarchitecturecommands) adds one JSON line to
context-history.jsonlnext tograph.db: the time, the tool, the
estimated full, graph and saved tokens, and where the call has them
detail_level, the changed-file count and the depth. No file names, paths,
symbol names or source text are written, nothing is sent anywhere, and the
log rolls over at 1 MiB, so it never takes much more than 2 MiB.code-review-graph context-historyprints it per UTC day or, with--by tool, per tool, with
--days Nand--json. Off by default; nothing is written in read-only
mode. - The GitHub Action has an opt-in
detailsinput that adds graph context
to the review comment (#882). It takes a list of sections:riskshows
each term of the top symbols' risk scores, so a reader sees why a score
moved and not only that it did;communities,hubsandcouplingshow
the communities the change touches, its most connected symbols and the
links that cross a community or language boundary;impactshows its
impact radius and direct callers;gapsranks the test gaps by what
depends on them;graphgives the graph's size, community count, the
method that found the communities (Leiden or file-based grouping), and
versions. The block is folded away, shows at most 5 rows per list, is
capped at 12,000 bytes with a note saying what was cut, and never takes
room from the report. Left empty, the default, the Action runs the same
commands and posts the same comment byte for byte. The data comes from the
newcode-review-graph detect-changes --details <sections>option, which
adds adetailsobject to the JSON and nothing else. Building communities
now also records the method used, in graph metadata. - Symlinked folders can be followed, as an opt-in (#269). Pass
--follow-symlinkstobuild,updateorwatch, or set
CRG_FOLLOW_SYMLINKS=1; it is off by default, so nothing changes unless you
ask. A project folder of links to code kept elsewhere
(inputs/app -> ~/src/app) now builds a graph instead of an empty one.
Links to folders outside the repository are followed too, because you made
the link and asked for it, and their files are stored under the link's path
inside the repository, never the outside path. A file inside the repository
keeps its own path, so a link to a folder the graph already holds adds no
copies. A link to a folder that holds it, or holds the repository, is
skipped and logged, and each real folder is read once, so links cannot
loop. At mostCRG_MAX_FOLLOWED_FILESfiles (default 10,000) are taken, and
a warning says when the cap is reached.updateandwatchadd and drop
these files as links and the files behind them come and go, matching a full
build;watchalso watches each followed folder outside the repository.
Search, review context, flows, rename andforgetwork on these files
through their in-repo path, and imports between them point there too. Any
other path outside the repository is still refused, and the review context
and flow source snippets no longer read one either. Symlinked files are
still never followed. - Python modules that replace themselves in
sys.modulesare followed to the
module they install. A compatibility shim such as
sys.modules[__name__] = importlib.import_module("app.services.billing")
hands importersapp/services/billing.py, but the graph stopped at the shim:
import billingtargeted the shim file,billing.charge(...)and the
TESTED_BYedge from a test calling it stayed unresolved becausecharge
is not defined in the shim, andfrom billing import chargenamed a node
that does not exist. The import edge, module-receiver calls and
from shim import namenow resolve to the real module, through chains of
shims and through re-exports to the defining file. Only the literal,
module-level form is followed; a computed or relative module name, a swap
inside a block, or a target outside the repository leaves the file as the
module. Because importers now depend on the real module, editing only the
shim does not re-parse them onupdate; runbuildafter re-pointing a
shim. semantic_search_nodes_toolcan return the code with the hits.
include_source=Trueattaches each hit's source lines, and
expand_neighbors=Truealso attaches the direct callers and callees of the
top hits with their source.max_source_linescaps each snippet, one
line budget caps the whole response, cut-off snippets are marked
source_truncated, and a stored path that resolves outside the repository
is never read. Source is not attached whendetail_level="minimal". Without this an agent got names and line numbers and opened
the same files by hand, so the search cost tokens on top of the reads
instead of replacing them.- The README and
docs/REPRODUCING.mdpublish the first canonical
agent_baselinecapture (2026-09-22): against a grep-and-read-top-3-files
baseline, the graph answer is a median 6.65x smaller across 18 questions on
6 repositories (range 4.2x to 72.6x). It predates the test-file ranking
below, so a run today gives different numbers until it is redone. The
per-repo CSVs are inevaluate/results/; the 2026-05-25impact_accuracy
CSVs are removed, as the 2026-08-02 captures replace them. - Codex installs a global CRG skill with MCP-first guidance and a read-only CLI fallback (#841).
installalso writespost-checkout,post-mergeandpost-rewritegit
hooks that runupdate, so the graph follows a branch switch, a merge or a
git pull(merge or rebase mode) instead of waiting for the next commit.
post-checkoutruns on branch checkouts only and skips the checkout a
rebase makes before replaying local commits;post-rewriteupdates once the
rebase has finished and ignores--amend, whichpre-commitalready covers.
A rebase with nothing to replay never runspost-rewrite, so it updates at
that checkout. An existing hook in a language other than shell is left alone.
Each block is marked, appended to an existing hook, skipped in linked
worktrees unlessCRG_HOOK_WORKTREES=1, and removed byuninstall.
--no-hooksskips them along withpre-commit.- Oracle PL/SQL files get structural parsing: packages, procedures, functions and the calls between them (#785).
- Robot Framework
.robotand.resourcefiles are indexed (#967, #963). - Static Laravel routes produce
Endpointnodes andHANDLESedges at parse time (#1072, part of #851). - Spring AOP advice reaches its targets through
@annotationandexecutionpointcuts (#916, #592). servehas an opt-in mode for parallel graph access across Git worktrees (#1063).- Read-only MCP tools carry
readOnlyHint, so clients can skip the approval prompt (#1124, #1123). - The troubleshooting guide explains the local-checkout workaround for Zed SSH projects (#1130).
.jsonfiles are indexed; before, they gave no nodes at all (part of #436).
Each key and array shape becomes aJsonPathnode joined byCONTAINS
edges, in the same value-free shape as generic YAML: array items share one
[*]path, and values never enter the graph. Comments and trailing commas,
as intsconfig.json, are fine. JSON is often data, so a lot stays out:
translation files keep only a file node by the rule YAML uses (#1150), and
so do test files, files underdocs,examples,samples,demoor
datafolders, and files over 256 KB. A file stops at 1,000 paths, and an
object with more than 200 keys shares one.*path.package-lock.json,
npm-shrinkwrap.json,*.lock.json,project.assets.jsonand*.min.json
are ignored. On vega, 473 JSON files add 1,228 paths, mostly its 37
package.jsonfiles; without the folder rule,docs/alone would have
added 12,725. Search ranks JSON below code unless you ask for it with
kind="JsonPath", a$path or a.jsonname, and JSON rows no longer
take the places code needs: sixtranslation.jsonfiles had pushed
appWithTranslationout of the hits for "translation" on next-i18next.
detect_changes_toolreportschanged_json_paths. A JSON or YAML key
escaped to a lone surrogate, such as"\ud800", is left out; before, a
YAML one stopped the whole build. Runbuildto add JSON to a graph you
already have.- MiMo Code is a supported platform (#593, PR #688 by @Aditya-Tripuraneni).
install --platform mimoadds the server undermcpin
.mimocode/mimocode.jsoncif that file exists, otherwise in
.mimocode/mimocode.json, and writes the graph instructions toAGENTS.md.
Other servers, settings and comments are kept. MiMo reads both files, so
uninstallremoves the entry from both.
Changed
- Dependency minimums are raised to versions that are tested and that install
on every supported Python:mcp>=1.29.0,anyio>=4.8.0,tree-sitter>=0.26.0,
tree-sitter-language-pack>=0.13.0andpyyaml>=6.0.2. PyYAML 6.0 fails to
build on Python 3.12 and newer, andmcp1.29 andfastmcp3.2.4 already need
a neweranyiothan the old minimum of 4.0. Ranges stay ranges; pin exact
versions in your own lockfile if you need them. (#896, #265) stagingis no longer promoted totestingautomatically. TheAuto promoteworkflow and its script are gone, so a person reviews and merges every promotion, and a test fails if any workflow merges a pull request.- Generic
.ymland.yamlfiles are indexed asYamlPathnodes that hold key
paths and value types, never values.detect_changesreports them as
changed_yaml_paths, bounded bymax_results; the analysis behind it takes
at mostCRG_MAX_CHANGED_YAML_PATHS(200) YAML paths, and as many JSON
paths.pnpm-lock.yamlis ignored at
any depth. - Search ranks test code below production code unless the query asks about
tests (kind="Test", or a word such as "test", "spec", "fixture" or
"mock"). A two-line test stub that names a symbol used to score as well as
the implementation and could outrank it. - The installed instruction block tells agents to answer from
include_source=True, expand_neighbors=Trueand open a file only when a
snippet is cut off. It still says not to change code from graph output
alone. Earlier blocks stay recognised for upgrade and uninstall. - The MCP server instructions now route by question type instead of introducing
the server as a code-review tool. With deferred tool loading an agent sees
only the tool names and these instructions, so "for code reviews" kept it on
Grep for callers, importers and covering tests everywhere else. The new text
maps each relationship question to its tool and leaves plain-text search to
Grep. Measured with Claude Code on one TypeScript monorepo: across 8 ordinary
tasks that never mention the graph (implement, review, brainstorm, plan), the
graph was used in 1 run before and 6 after; plain-text questions stayed on
Grep (0 of 4 moved).tests/test_server_instructions.pykeeps every tool and
query pattern the text names registered. - Stored vectors are scored with numpy when it is installed, about 20 times faster, and numpy stays optional (#1090, #1089).
CRG_VECTOR_CACHE=1keeps the decoded embedding vectors in memory between
searches instead of reading them all back from SQLite on every query. A
search over 42,000 vectors of 4,096 dimensions goes from 2.3 s to 0.40 s once
the cache is warm, with the same ranking. It is off by default because it
costs RAM and holds a read connection open for the life of the process, and
on Windows that keepsgraph.dbfrom being deleted or replaced.find_dead_codecollects its edge evidence once, not once per function (#1082, #982).- Plain C headers skip the C++ parse through a prefilter (#876, #802).
- The dev dependency range allows pytest 9 (#1140).
- Builds and updates use less memory and fewer database commits. The MCP server loads its tool modules on first use, so an idle server holds about 89 MB instead of 144 MB; new flows, flow members and summary rows are written in one transaction each; the files that depend on a changed file come from one query; and community detection no longer decodes every edge's metadata. The graph itself is unchanged (#1139).
- fastmcp 4 is allowed (
fastmcp>=3.2.4,<5) anduv.lockpins 4.1.6 (#970).
fastmcp 4 brings the mcp SDK 2.x, so the server now answers
server/discoverand serves clients on the 2026-07-28 protocol, which
issue #919 found it could not do. Older clients still use the
initializehandshake as before. The prompts importMessagefrom
fastmcp.prompts, which works on fastmcp 3 and 4; the old
fastmcp.prompts.promptpath is gone in 4 and stopped the server from
starting. - The
build-graph,review-deltaandreview-prskills are written from one source,DOCUMENTARY_SKILLSinskills.py.scripts/generate_documentary_skills.pyrewrites them, and a test fails if a file is edited by hand (#822). install --gitignoreadds the files and folders that run created to a
marked block in the repository's.gitignore(#471). It is off by default,
because.claude/,.mcp.jsonandCLAUDE.mdcan be team files. Only
paths that did not exist before the run and that git does not track are
listed, so a file you wrote or committed is never ignored. A path some rule
already ignores is not repeated. Running install again merges into the same
block,--dry-runshows the lines it would add, anduninstallremoves the
block and nothing else, so.gitignorecomes back byte for byte. The
.code-review-graph/entry keeps its own handling.
Fixed
- A fresh clone, set up as CONTRIBUTING.md says, now passes and stays clean:
- Installing for any platform inside this checkout no longer leaves untracked
files. The root.gitignorenow lists what Cursor, Windsurf, Gemini CLI,
Kiro, MiMo, OpenCode, CodeBuddy and Copilot write there, and
tests/test_install_outputs_are_ignored.pyfails if a platform starts
writing a new path. tests/test_token_budget.pyno longer fails when the temp folder has a
long path (the macOS default, or a long--basetemp). Answers hold
absolute paths, so each one is now measured as if the test repo lived at
the path CI gives it. No budget changed.- One parser test failed on Python 3.14, which
uv syncpicks when it is
the newest Python installed. Python 3.14 parses the long expression the
test used to breakast.parse, so the test now forces the failure on any
Python that accepts it. The rest of the suite already passed on 3.14.
- Installing for any platform inside this checkout no longer leaves untracked
- The release gate's corpus check expects this release's numbers. Its recorded
baselines dated from before the new YAML, JSON, HTML and Liquid support, Ruby
and Java test finding, and the C# and nested-scope identity changes, so 3 of
its 8 repositories reported moves that were on purpose. The time budgets are
unchanged. - The determinism check reorders the file list again. Since
buildstarted
reading the file list through a new function, the check's reorder never
reached it, so the three file-order tests built the same order twice: two
passed for no reason and the known node-id bug looked fixed, which failed the
release gate. The check now reorders the functionbuilduses, and fails if a
reordered build never read the reordered list. - The docs match the code again.
docs/, the referenceget_docs_section_tool
serves,CONTRIBUTING.md,SECURITY.md,AGENTS.md, the Copilot
instructions, two skills and the VS Code README now cover what the last two
release trains added: all 19 query patterns, the search source options, the
write lock and exit75, the new git and steering hooks, untracked files,
Liquid and the other new node kinds, confidence tiers and schema v15. They also
drop claims the code no longer backs. - The read-only query script in the bundled skill (
crg_readonly.py) now
takespath_towith--to, so it offers every pattern the CLIquery
command accepts. It stops before running the CLI whenpath_tohas no
--to. A test now fails if the script's list drifts fromtools/query.py
or the CLI again; the patterns the CLI does not take yet are listed there
with the reason. - The VS Code
codeReviewGraph.cliPathsetting no longer talks about a bundled
version. The extension does not include the CLI; left empty, it runs
code-review-graphfrom yourPATH(#369). install --dry-runshows what each file would become, andinstallwrites
nothing until you have answered its question (#173). The dry run used to list
file names only, and a real install wrote the MCP config before it asked
aboutCLAUDE.mdand the other instruction files. Every install write (MCP
configs,.gitignore, skills, hooks, git hooks, the OpenCode plugin, the
instruction files, the--steeringhook and the--gitignoreblock) now
goes into a plan first.--dry-runprints that plan
as one unified diff per file, a new file as its full content, cut at 60
lines a file with a note saying how many lines were left out, and writes
nothing. A real install lists the files it will change, asks about the
instruction files, and only then writes exactly the plan it showed. Ctrl-C
at the question stops the install with nothing written, and a file whose
content or permissions change while the question is open is left alone and
reported. A file reached by two names, such asCLAUDE.mdlinked to
AGENTS.md, is planned and written once. Commented JSONC configs and hook
settings are previewed and written through the same splice, so their
comments are kept. A hook settings file install will leave alone is named in
the dry run too. Saying no to the question also keeps the instruction files
out of the--gitignoreblock.query_graph_toolno longer answers "ambiguous" when a full name has one
real symbol. The name search splits on underscores, sohybrid_searchalso
matched everytest_hybrid_search_*and came back with 34 candidates and no
answer, at about 5,200 tokens even withdetail_level="minimal". A full name
now means that name: a test or test helper with the same name gives way to the
real symbol, and the summary says how many it skipped. Two real symbols with
the name are still ambiguous. The same query now costs about 450 tokens and
answers it. An ambiguous answer atminimalis five short rows sent once.callees_oflists calls bound to a real node before bare-name guesses, so a
fewlencalls can no longer fill the window and hide the real callee.installno longer wipes a hook settings file it cannot read as strict JSON.
One comment or trailing comma in.claude/settings.json,.qoder/settings.json,
.codebuddy/settings.json,.gemini/settings.json,~/.codex/hooks.jsonor
~/.cursor/hooks.jsonmade the reader fail, and the file was overwritten with
nothing but our hooks and no backup, so every setting in it was lost. These
files are now read as JSONC, only what changed is written into a commented
file, and every comment stays with the setting it describes. A file that cannot
be merged, such as broken JSON or ahookssetting of the wrong shape, is left
byte for byte as it was and named in the output, andinstallno longer says
it installed those hooks. So is a file with a/*comment that never closes,
where the hooks would have landed inside the comment and never run. A refused
Gemini CLI install no longer rewrites the hook scripts in.gemini/hooks/.
Cursor's file now gets a.baklike the others.- Adding a member to a commented JSONC config, as
installdoes for an MCP
entry, no longer moves a trailing comment onto the new member or drops a
comment from an empty object. .cjsfiles are indexed as JavaScript and.ktsfiles as Kotlin, as.mjs
and.ktalready were. Every build skipped them, so their functions, imports
and calls were missing from the graph and read as if they did not exist.crg-daemon stopclears the health and state files of watchers that were
killed outright. A killed watcher cannot clear its own, and on Windows that is
every watcher, becausestopends the daemon withTerminateProcessand the
Job Object takes the children with it. The file left behind read as a stalled
watcher forever.stopalso now stops any live watcher when it finds the
daemon already gone. Health files for watchers the daemon never ran are left
alone.- Graphs no longer hold data nothing reads (#1059). Schema v15 drops
community_summaries,flow_snapshotsandrisk_index, which every full
postprocess rewrote and nothing read, and whose rows kept pointing at deleted
nodes. It also dropsidx_nodes_qualified,idx_edges_sourceand
idx_edges_target, as other indexes cover them. A new graph of this repository
is 17% smaller, 252 MB down to 210 MB, with the same answers at the same speed.
An upgraded graph keeps its file size and reuses the freed space for later
writes; to shrink the file, delete it and rebuild, or runVACUUM. Older
releases refuse a v15 graph, as with any schema change. Build results no longer
carrysummaries_computedor thesummaries_stiming. find_large_functions_tool,semantic_search_nodes_tooland
traverse_graph_toolnow have hard ceilings. At a hugelimitor
token_budgetthey returned up to 1.8M tokens on this repository, and now
return under 25,000.limitandtoken_budgetreject zero, negatives and
booleans. The ceilings also bound the work: search ranks three times what it
shows, the large-functions query fetches only what it shows and counts the rest
in SQL, and the walk stops when the budget ends.find_large_functions_tool
gainsdetail_level, andminimalcosts less thanstandard.result_count
on these tools now means the count before the cut, as onquery_graph_tool.
(#895, #888)CRG_MAX_IMPACT_NODESnow reachesget_impact_radius_tool. A literal default
of 500 shadowed it, so the setting did nothing through MCP. (#888)traverse_graph_toolindfsmode no longer misses what lies past a node it
first reached at the depth limit, then reached again by a shorter path.- Imports through an npm alias resolve to the local package (#343, PR #880).
With"sharedLib": "npm:@scope/shared@^1.0.0"in an app'spackage.json,
import x from 'sharedLib/some/module'was stored as a bare string, so
importers_ofand the impact radius found none of the apps that use the
shared package. Now, when one package in the repository is named
@scope/shared, the import reaches its file. Entry points are read as for a
workspace member (exports, thensource,moduleandmain, with
dist/tried undersrc/). A wrong edge is worse than a missing one, so
the import stays bare when the nearestpackage.jsonthat names the
dependency gives it an ordinary version, when the local package's
versionis outside the alias range, when two local packages share the
name, when thatpackage.jsoncannot be read, or when the package or file
is one the build ignores. tsconfigpathsstill win. An alias also wins
over a workspace member with the same name, because npm installs the
aliased package under it.updatedoes not re-parse importers when only a
package.jsonchanges; runbuildafter changing an alias. - A file added by
updatenow gets a community, as it does afterbuild.
It was in no community, so the update saw nothing affected, skipped
community detection and left the file's nodes without one until the next
full build (#1058, PR #1122 by @jay23606). - On Windows,
buildandupdatestarted with stdin on theNULdevice
(subprocess.DEVNULL, Node'sstdio: "ignore") parse with threads.
NULpassesisatty(), so they used a process pool, and a caller that
stopped the run, such as a pre-commit hook timeout, left up to 8 worker
processes behind each time. Only a real console now keeps the process pool
(#1163, PR #1166 by @xOscarCodes). - The impact-accuracy benchmark's co-change mode no longer reports
predicted_files = 0on every commit (PR #823).git diffgives
repo-relative paths and the graph stores absolute ones under the resolved
root, so the seed never matched a node. The graph-derived ground truth now
counts real neighbour files too, andseed_filestays relative to the
repository. - Members of nested classes keep the full scope chain in every language that
did not already (#934): in Java, Kotlin, Swift, Scala and others,
Alpha.Same.runandBeta.Same.runare two nodes instead of one, a
nested class is contained by its enclosing class, and three levels keep all
three. Local calls stay inside their own class, and a JVM import of a nested
class (import app.Outer.Inner) targetsOuter.Inner. On gson this leaves
no danglingCONTAINSedges (700 before) and 14 danglingCALLS(154
before). Existing graphs reparse their files with nested classes, and the
callers of those members, once on the next update. - C# declarations no longer collapse across namespaces, and calls no longer
bind to a type the caller cannot name. Qualified names now carry the
namespace and the complete containing-type path
(/repo/Handlers.cs::App.Report.ExportHandler.Run), and a dedicated
namespace-aware pass binds each call from its own lexical scope:using
directives are read at the byte offset of the namespace body that declares
them, so a reopened or file-scoped body cannot leak its imports elsewhere;
global::and namespace aliases keep their meaning;Alias::Typesearches
only namespace aliases; and an unqualified call reaches a static method in
an enclosing type without crossing a nearer method group. File co-location
and short-name uniqueness are no longer treated as visibility evidence, so
an ambiguous or unsupported reference stays unresolved instead of pointing
at the wrong declaration. Overload selection, generic binding (#943),
inherited members andusing staticremain outside this change (#946, with
the nested-type identity needed by #934 and #937). - C# bindings are now re-evaluated on every update rather than fixed at first
parse. Each call keeps its raw reference, so a binding is revoked when the
usings or declarations behind it change,TESTED_BYmirrors move with their
call, and deleting a callee returns incoming calls to raw references so
recreating the declaration binds them again. A binding change marks flows
dirty and the next full postprocess retraces them, which keeps callers and
entry points outside the parsed files correct (#946). - C# generic declarations are keyed by arity, so a constructed receiver reaches
the declaration it names —I<int>binds toI<T>,Ibinds to the separate
I, andPair<int>binds to neither when onlyPair<K, V>is declared. The
receiver's spelling is retained on the call and the arity is read from the
syntax, so nested arguments and tuples count correctly. Type argument
substitution, constraints and overload selection remain out of scope (#946). - Existing C# graphs reparse once on the next incremental update to adopt the
new identities; a file that fails to parse is retried on its own instead of
holding the version gate open and repeating a full rebuild (#944). Graphs
that carry embeddings should re-runcode-review-graph embedafterwards:
the renamed C# nodes leave their previous vectors orphaned until a refresh
purges them. - Unresolved C# member-call targets retain the receiver spelling (for example,
Derived::Sharedinstead ofShared), socallers_ofcan return fewer
matches for unsupported bindings, including inherited
members and receiver types missing from the index (#946). - A file inside a Python package no longer answers an absolute standard library
import.import jsonandimport typingin Flask's own files were linked to
flask/json/__init__.pyandflask/typing.py, which made a false dependency
and a false import cycle. Python 3 does not look in a package's own folder, so
those imports now stay external. Atyping.pybeside a script, or at the top
of asrc/folder, can still answer them. - A multi-clause Elixir function is now one node that spans all its clauses. The
graph keeps one node per name, so the last clause used to overwrite the others:
a change to an earlier clause found no function at all.parse_bytesnow merges
them, from the first clause's first line to the last clause's last line, and
keeps the first clause's parameters. Clauses of different arities share the
node too, as the graph has no arity in its names, so another function can sit
inside the span (#979). tests_forfinds Java and Ruby tests. On spring-petclinic, production symbols
with a test went from 5 to 61, and on sidekiq from 198 to 443. Run a full
buildto pick this up.- Java: a test in
src/test/javacan now reachsrc/main/java, for an import
and for a class in its own package, which needs no import.this.field.x()
andvar x = new Cart()now give the type too. - Ruby:
it "..." do,describe,before,letandtest "..." doare test
nodes in test files. A call is matched to a method only when the file says
what class it is made on: a constant, a constructor,described_class, or a
name assigned from one of those. A constant is looked up in the modules
around it first, as Ruby does, soCartinsidemodule ShopisShop::Cart. - A call in a test on something of unknown type no longer matches a method by
name alone, in either language. An inherited method is not linked yet, so
owner.setId()does not count forBaseEntity.setId. - By name,
CartTests,CartTest,CartITandCartSpecnow count for the
classCart. Free text in a test's title no longer counts, so
it "tests json output"is not a test ofjson. - A Ruby or Java method called only by its tests stays a flow entry point, as
it was when those calls did not bind.
- Java: a test in
forget,embedandwatchnow obey the write lock, asbuild,updateand
postprocessalready did, so none of them can rewrite a graph that a build has
only half written.forgetandembedtake it for the whole run and exit75
if another writer holds it, saying that nothing was done; both take--wait.
embed_graph_toolanswersstatus: contended.watchtakes it for each write
and lets go between them, so a running watcher never stops a build. If the lock
is held, a watcher keeps its changes and retries, and can still be stopped.- A call the graph could not bind no longer looks like one it did (#984). The
parser wrote a file path in front of a name it could not check, so
h.hashCode()becameOne.java::Helper.hashCodethoughHelperhas no such
method,self.shared()named the subclass whensharedwas inherited, and
Vec::new()in a Cargo crate becamesrc/lib.rs::new. None of these names a
node, yet each looked like a real link. Now no edge keeps a qualified name that
has no node: it goes back to the plain name, keeps the qualified one in
extra.claimed_endpoint, and returns to it if that node appears. A guess like
this only counts for a method of the class the call named. In Rust, a type from
outside the crate stays as written (Vec::new), which also lets
WalkBuilder::newreach the real method;inner::bar()on an inline module
lands on it; and on macOS and Windows the typeGlobis no longer read as the
fileglob.rs. On ripgrep, 729 invented call targets and 341 invented test
links went to 0, and real call links rose from 4,064 to 4,296 with none lost.
spring-petclinic went from 2 to 0 and gin had none; their real links did not
change. - Every edge claimed confidence 1.0 whatever had happened to it. For
CALLS,
REFERENCESandTESTED_BY,confidence_tieris nowEXTRACTED(1.0) when
the source names a real node,INFERRED(0.8, or the resolver's own score) when
a resolver chose it,AMBIGUOUS(0.4) when several nodes fit, andUNRESOLVED
(0.2) when none does. On gin, 14,353 of 16,249 such edges had no node and still
said 1.0. No schema change: the next build, or an update that changes a file,
puts an existing graph right. callers_of,callees_ofandreferences_tosay when part of an answer is
only name matches, in the existingconfidencefield: "3 of 8 call sites match
by name only and may be wrong; resolution='direct' keeps the confirmed ones".tests_forno longer counts a test as covering a function because both call
the same outside name.String::new()in a function and in a test linked the
two, and so did the invented targets above: ripgrep'sdecompress.rs::add
claimed 10 tests, none of which reaches it, which lowered its risk score.- A call to an inherited method through a subclass now finds the method
(#984, building on the idea in #1036 by Anthony Ala).this.handle()in
Leaf extends Alpha,$this->dispatch()in a PHP test case and
pet.getName()on a JavaPetthat inheritsgetNamenamed no node, so
callers_ofandtests_foron the real method found nothing. The edge now
goes to the ancestor that declares the method, asINFERRED, and its
TESTED_BYmirror goes with it. A class is known by its file, not its short
name, and a parent counts only when the file, its imports, its Java package
or its PHP namespace says which class it is; where Python could pick another
ancestor, or a parent is outside the graph, the call stays unresolved. An
update that adds or removes the method checks the edge again, so it matches a
rebuild. A Java or TypeScript receiver's declared type now picks the class
even when that class needs no import, and a call on another object no longer
lands on the caller's own method when nothing says it is that class. On
spring-petclinic, calls bound through a subclass rose from 2 to 86, on a
TypeScript DDD example from 0 to 18 and on its PHP twin from 97 to 134, with no
new invented targets; 28 self-calls the old rule made up were dropped or
moved to the right class. - The edit hook updates the graph of the repository that holds the edited
file, not the one the session started in (#311). From a folder of sibling
repos it updated nothing, from an outer repo it updated the outer graph and
never the nested one, and from a main checkout it never updated a linked
worktree's graph. The Claude Code, Qoder and CodeBuddy hooks (and the plugin
hooks/hooks.json) now pass the event JSON tohook-update, which reads
the file path from it. That repository is updated if it already has a graph
or is the session's own repository, which is still built on first edit as
before; an edit anywhere else on disk changes no graph, and a submodule with
no graph of its own passes the edit to its superproject. Without a file path
in the event, the hook uses the event'scwdas before. Upgrade the package
before runninginstallagain: an older CLI has nohook-update, so its
edit hook does nothing (it still never fails the edit).installreplaces
the hook earlier releases wrote, anduninstallremoves it, including the
v2.3.4 to v2.3.6 form it used to leave behind. detect-changesanddetect_changes_toolnow report what a diff that only
deletes code removed, and no longer miss it or blame the code around it. A
hunk such as@@ -430,42 +429,0 @@adds no lines, so it named only the
line before the gap: deleting a whole function reported 0 changed functions
and risk 0.00, with nothing about the callers it left behind, and when the
gap sat right after the previous function, that untouched function was
listed as changed. The old version of the file is now read: a function or
class that lost some lines is changed, and one that is gone is listed in
removed_symbolswith the callers, subclasses, importers and tests that
still use it, and named in the summary ("Removed but still used:
format_filename (risk 0.65, by FileError.init, File.convert, ...)").
The code around the gap is never marked. A removed symbol that is still
used breaks its callers, so it is scored like a change (the flows it was
in are traced again) and raises the overall risk, whichfail-on-risk
reads; one nothing uses adds nothing. Deleting click'sechowith 20
callers now scores 0.65, where changing it scores 0.40. The PR comment
gets a "Removed symbols" table. Up to 100 files are read per report (CRG_MAX_DELETION_FILES), and
the summary says when a file could not be read. A deleted file is still not
covered. Thanks to @decrypt-mm for reporting the case in #1167.- TypeScript and JavaScript imports resolve against
compilerOptions.baseUrl
when nopathspattern matches them, astscdoes. A tsconfig that sets
baseUrlwithoutpathsresolved no non-relative import at all: on a
NestJS monorepo whose services importsrc/..., 1,849 import edges stayed
bare strings,importers_offor one service found 14 of its 35 importers,
and calls on its injected services fell back to name matching. - A seeded
visualizeview (--seed-symbol,--seed-file,--seed-flow)
reads only the part of the graph around its seed (#1145). It used to load
every node, one query per file, and the whole edge table before cutting the
view down to--max-nodes, so a two-node view of a large graph cost as much
as the whole graph. It now reads a few columns of each node, which name
lookup needs, counts edges from the indexes, and loads only the edges that
touch the view. On this repository's graph a one-symbol view went from 380
SQL statements to 20 and from 120 MB to 13 MB at peak, with the same output
byte for byte. A path query still reads the whole graph, since a route can
run anywhere, and so does a view that reaches most of a large graph. - Translation files no longer flood the graph. A translation file repeats the
same keys once per language, and each key became aYamlPathnode: on sidekiq,
web/locales/*.ymlgave 3,147 of the 3,336YamlPathnodes. A YAML file in a
locale,locales,i18n,l10nortranslationsfolder whose name or
sub-folder is a language code, such asconfig/locales/devise.en.ymlor
locales/pt-BR/app.yml, now gets a file node only. Sidekiq's graph falls from
5,085 nodes to 1,935 and its database from 45 MB to 31 MB. A name alone is not
enough, soconfig/en.ymland.github/workflows/it.ymlkeep their paths, and
so doeslocales/deploy.yml. Runbuildto clear these nodes from a graph you
already have;updateonly re-reads files that changed. semantic_search_nodes_toolandcross_repo_search_toolapplykindbefore
they cut the hits, not after. Rows of another kind could fill the fetch limit, so
asking for functions could return few or none when many existed:deadwith
kind="Function"found 1 of them on sidekiq and now finds 9. If the exact
reading of a query matches only other kinds, the wider reading is now tried.tests_forgives the same answer every time on the same graph. On a large
class it follows at most 50 callees (CRG_MAX_TRANSITIVE_FRONTIER), and it
picked them in an order that changed with each new process, so an MCP
restart or a new CLI call gave a different list. On koel, 20 runs on one
graph gaveSongServiceanything from 43 to 90 tests, and every run gave a
different list. The 50 are now the first by name, with named callees before
bare ones, which are never followed. Thedetect_changesrisk score counts
the same tests, so it is steady too. Two builds of the same code already
gave the same graph; only the query moved.- Imports of sibling packages in npm, yarn and pnpm workspaces resolve to the
package's source files (#1030). A specifier such as@acme/sharedor
@acme/sdk/browsernames a package-manager link, not a
compilerOptions.pathsalias, so it was stored bare andimporters_of,
the impact radius andreferences_tostopped at every package boundary.
Calls through a package's barrel file now reach the defining file as well.
The newWorkspaceResolverfinds the nearestpnpm-workspace.yamlor
package.jsonwithworkspacesinside the repository, maps each member's
nameto its directory, and readsexports(subpaths, conditions,
fallback arrays and*patterns, with Node's key priority; anullentry
or an unlisted subpath stays unresolved), thensource,module,main,typesand
typings. A target underdist/,build/,out/orlib/is tried
undersrc/with source extensions first, because compiled output is
normally absent from a checkout. tsconfigpathsstill win when both
apply. Because a wrong edge is worse than a missing one, an unscoped name
resolves only when the importing package declares it as a dependency, Node
builtin names such aseventsnever resolve to a workspace member, a name
two members share resolves to neither, probes are case-exact on every
platform, and a target the build would not index keeps the bare specifier. - The Claude Code plugin's hooks load again.
hooks/hooks.jsonheld a bare
event map, which current Claude Code rejects with "hooks.json must have
hooks(the hook matchers) ormodules(hooks modules), or both", so the
SessionStart and PostToolUse hooks never ran. The events now sit under a
top-level"hooks"key, the shape the plugins reference documents. The hooks
carry the same guards as the onesinstallwrites, so a missing
code-review-graph, a folder outside Git or a failed command shows no hook
error, and the update hook matchesEdit|Writeonly, so a Bash command can no
longer start a full build in a repository with no graph. - The OpenCode installer now detects the installed OpenCode major version and
writes the matching plugin API: returned hooks for OpenCode 1.x and the
id+setupdefinition for OpenCode 2.x. Both templates use bounded
node:child_processargv execution. The 1.x template runs from the OpenCode
worktree/current directory instead of passing that directory as--repo, so
starting OpenCode in a repository subdirectory still resolves the graph root.
The 2.x template keeps its plugin package import type-only, so OpenCode
2.0.0–2.0.24 can load it without a runtime@opencode/plugindependency.
This fixes the V2 loader errorPlugin must export a default definition with an id and an effect or setup functionwithout dropping 1.x support (#1038,
#947). Each plugin instance runs one graph update at a time, and edits made
during a run share one follow-up run, because an overlapping update reads the
first run's in-progress marker as a crashed build and does a full rebuild. - Codex hooks act on the repository named by the event's
cwdinstead of the
directory Codex was launched from, so they cannot update the wrong graph. The
generated hooks now run the newhook-updateandhook-statuscommands, which
ignore a missing, relative or non-repositorycwd. Upgrade the package before
runninginstall --platform codexagain;installanduninstallboth
recognise the hooks earlier releases wrote. **/vendor/**applies only when a root manifest showsvendor/holds
dependencies (composer.json,go.mod,go.work,Gemfile,Rakefile,
Cargo.tomlor*.gemspec), and a nested one excludes its sibling
vendor/. A source package namedvendorin any other repository stays in
the graph; addvendor/to.code-review-graphignoreto exclude it (#1056).from module import name as aliasfollowed byalias(...)is recorded as
a call tomodule.py::name. It used to qualify the alias,
module.py::alias, a target that matches no node.- A call through a Python module receiver whose module only re-exports the
name (pkg.helper()wherepkg/__init__.pydoes
from .impl import helper) resolves to the defining file. The resolver
required the name at the top level of the receiver's own module, so the
call stayed unresolved. _strip_plsql_noisetracked string-literal state per line and stripped
/* */comments over the raw text in a separate first pass, so a
string literal spanning multiple lines (or containing an unterminated
/*) could desynchronize the rest of the file: anEND name;-shaped
substring inside a multi-line string terminated a block early, an
Oracleq'[...]'/Q'<...>'quoted string (which allows unescaped'
inside it) confused the plain'...'state machine the same way, and
an unterminated/*inside one string literal could pair up with an
unrelated*/inside a later string and blank out everything between
them — including a wholeCREATE PROCEDUREstatement that happened to
fall in that span, which also regressed plain (non-Oracle) SQL files.
It now tracks string and comment state in one pass over the whole file.
_plsql_block_end's terminator search also ran unbounded to
end-of-file, so a package body member using a bareEND;could match
an unrelated, later package body's same-named member'sEND name;
instead of its own, and the unbounded search was quadratic on a file
with many members (~4s for 4,000). It now takes alimit, bounding a
member's search to its own enclosing package body (PR #785 review).- An incremental update now keeps flows and communities in step with the
graph. A new entry point gets its flow, a re-parsed file's flows follow its
nodes to their new IDs, and a deleted file leaves no empty community behind.
Before, relative changed paths never matched the stored absolute ones, so
flows and communities were silently skipped (#569, #1058). - Overlapping graph writes no longer stack their work (#1073).
build,
updateandpostprocesstake a per-repositoryflockon
<data dir>/write.lockbefore parsing or rewriting derived data, so a second
invocation reportsstatus: contended(CLI:Skipped: another code-review-graph ... is already running for this repository (PID n)) and
exits75(EX_TEMPFAIL) instead of repeating the work. Commit hooks,
editor-save hooks and agent tool hooks fire in bursts, and until now the only
cross-process protection was SQLite's busy timeout, which is not reached
until the first write — every duplicate run paid for a complete parse first,
and apostprocesscould rewrite flows/communities/FTS for a graph another
process was still storing. Ten concurrentupdateruns of one repository
used to mean ten process pools on the same files.--wait
(wait_for_lock) blocks until the in-flight write finishes, for callers that
want the work done rather than skipped; two repositories never block each
other; and the fixed lock path lets external tooling take the same lock. The
lock covers schema initialization, legacy migration and CLI recovery too.
Forked parse workers close inherited lock handles, so killing their parent
releases the lock even while workers remain alive. A filesystem without
flocksupport logs a warning and runs unlocked rather than reporting a
competing writer. Invoke protected CRG commands directly; externalflock
wrappers are for other writers. - An explicit embedding refresh (
--embedding-providerwith
--embedding-model, or theembedding_providerandembedding_model
arguments ofbuild_or_update_graph_toolandrun_postprocess_tool) on a
graph with no embeddings yet now reports that it embedded nothing.build,
updateandpostprocessreturnembeddings_refresh_skipped: trueand a
warning to runcode-review-graph embedonce, and so does anupdate
that finds no changes.watchlogs the same warning after each update and
keeps running; a refresh that fails against an existing index still stops
it, as before. The refresh used to returnokwith no embedding field,
the same result as a build that never asked for one. It still loads no
model and makes no network call in that case (#1070). - A rename rewrites the import statements that name the symbol (#1107).
The preview looked for import edges that target the symbol, but Python
import edges target the imported module's file, sofrom pkg.util import helperwas never edited and the applied code raisedImportError. The
preview now reads the defining module and every module that imports it,
and follows Python's scope rules:from module import namein every
spelling (several names on a line, parenthesized over several lines with
comments,asaliases, relative imports), packages that re-export the
name and their__all__,import modulethenmodule.name, and
imports inside functions. Calls that no import statement names are still
found through the graph's call edges and resolved the same way:
pkg.util.name()afterimport pkg,sub.util.name()after
from pkg import sub, and a module bound byimportlib.import_module.
JavaScript and TypeScript get the same forimport { name },
import { name as x }, local export lists,export { name } fromand
export * frombarrels (also a public barrel nothing in the repository
imports from), andns.namethroughimport * as ns,import ns = require(),const ns = require(),await import()and
import().then(). A CommonJSmodule.exports = { name }keeps its key,
so its importers keep the old name. An alias keeps its own name; a
parameter, local, comprehension variable or class attribute that reuses
the name is left alone, and so are comments, strings and longer names
that contain it. A call inside a shell"$(...)"is code and is renamed. - Rename edits are exact. Each one carries the
columnwhere the old name
starts and thesiteit is (definition,call,reference,
import,export,attribute,__all__), and applying replaces the
name only there. Before, applying replaced the first substring match on
the line (helper_countbecamedouble_count) and, when the line had
none, the first match anywhere in the file. A method call chained over
several lines, or made inside an f-string, is renamed where its name is
rather than in a comment or the string's text. An edit that no longer
matches is skipped and counted inedits_skipped. CRLF line endings, a
byte-order mark and non-ASCII text survive, and a file that is not UTF-8
is no longer rewritten with replacement characters. refactor_tool(mode="rename")resolvesold_nameexactly. It accepts a
name,Class.methodor a qualified name such aspkg/util.py::helper.
When a bare name belongs to several symbols the first by file and line
is renamed, and the preview's newqualified_namesays which; pass a
qualified name to pick another. A name that matches no symbol is
not_foundinstead of falling back to a symbol whose name merely
contains it.- tsconfig and jsconfig path aliases inherited through a relative
extends
resolve against the config that declaresbaseUrl(orpaths, when no
config in the chain setsbaseUrl), as tsc does, instead of the nearest
config's directory. In an Nx or Turborepo layout every app extending a root
tsconfig.base.jsonlost its alias imports, soimporters_ofand the
impact radius of a shared library missed those apps (#1110). Also as in
tsc, a base config reached through a symlink (to the file or to a
directory above it) anchors where the link sits, and a..in its
baseUrlorpathsclimbs from there;"baseUrl": nullin an extending
config unsets the inheritedbaseUrl. - Python
@propertyreads produce edges, socallers_ofandfind_dead_codesee them (#1065, #1060). - Python and Ruby classes that share a leaf name in one file no longer overwrite each other's methods (#1087, #999).
- Terraform provider aliases keep distinct nodes, and references reach the right one (#1083, #985).
- PowerShell functions, classes, methods and calls are extracted again (#1079, #1067).
- Elixir
defwith awhenguard is indexed and short-form bodies are walked (#980, part of #979). - JavaScript and TypeScript generator functions are
Functionnodes that own their calls (#1131). - Components wrapped in
forwardRef,memoand similar calls areFunctionnodes (#972, #971). - TypeScript abstract classes are indexed, with nodes for their abstract methods (#1033, #1031).
- Compact Ruby
class Foo::Bardeclarations emitClassnodes with stable identity, and all nested Ruby methods now carry their full scope, such asAuth.User.to_s. This moves the schema to version 14, and the nextupdatere-keys Ruby files by itself (#933, #932). installno longer appends a shell block to apre-commithook written in another language, which made every commit fail.uninstallremoves the empty Codex skills folder it created.- Calls inside Rust
if falseblocks are left out of the graph (#1132). @Overridemethods and JUnit lifecycle methods count as entry points in dead-code analysis (#1035, #1034).- Spring calls on a concrete injected type stay on that type instead of moving to its only subclass (#1075, #1074).
tests_forand the test-gap report name tests reached through spec helpers (#1128, #1109).get_impact_radiusandget_review_contextreport unknown risk when a requested path is not in the graph (#1084, #983).- The real-absence claim is withdrawn while untracked source exists (#987).
- Review guidance applies the lifecycle test-gap exemption (#871, #865).
- A registered data directory that has gone missing no longer strands a local graph (#1080, #1057).
- Untracked files reach
buildandupdate(#1094, #998). - A no-op
updaterepairs stale FTS left by--skip-postprocess(#1105, #1104). - MCP auto-watch runs post-processing after updates (#1108, #1103) and stops and joins its watcher before closing the store (#1127, #1121).
- Partial graphs recover once and then stay incremental (#881, #803).
- Renames through re-exports match a full build, and importers keep their flows and communities (#1062).
- Watch mode does not report a refused directory as adopted (#910, #907) and does not watch ignored trees created after startup (#973, #908).
- Windows daemon logs accept Unicode and tolerate vanished PIDs (#1129, #866), and a Job Object ends watchers when the daemon dies (#958, part of #925).
tests_fornames PHP tests that reach a class through the Laravel container,
and says they are guesses (part of #850). Run a fullbuildto pick this up.- A PHP call on an expression, such as
$this->service->run()or
app(Service::class)->run(), was bound to a method of the same name in the
calling file. A test named after the method it tests was recorded as calling
itself: 113 such edges from tests on koel/koel. It now stays unresolved, as
the same call in Python does. - When more than one class the test imports defines the called method, the
test is listed withinferred_by: "ambiguous_call", unless the call is made
on one of those classes that the test builds, as in
(new UpdateBooks)->handle(). Building one class does not settle the
test's other calls. On koel, classes with a test file that names them but
notests_foranswer went from 17 to 12. - The summary says how many rows are guesses, and
detail_level="minimal"
keepsinferred_byontests_forrows. - A guess is never coverage: test gaps and risk scores do not count it. The
detect_changessummary says PHP gaps are a guess, since container and
HTTP calls are not traced (#851 is the full fix).
- A PHP call on an expression, such as
- Migration
upanddownmethods are not test gaps when the file is in a
migrationsormigratefolder inside the repository (#850).
Upgrade notes
-
Schema version: 13 → 15. Two migrations run by themselves the first time
any command opens the graph. Both are quick, as neither rewrites a table:- v14 clears the stored hash of every Ruby file, so the next
updateparses
those files again and gives Ruby classes and methods their full names, such
asAuth.User.to_s. Until that update, Ruby rows keep their old names. - v15 drops
community_summaries,flow_snapshotsandrisk_index, which
nothing read, and three indexes that other indexes cover. The file keeps its
size and reuses the space; to shrink it, delete it and runbuild, or run
VACUUM.
Rolling back needs a rebuild. Migrations only go forward. 2.3.9 refuses a v15
graph with "was written by a newer code-review-graph (schema v15; this build
understands v13)", so to go back, delete.code-review-graph/graph.dband
runcode-review-graph buildwith the older release. - v14 clears the stored hash of every Ruby file, so the next
-
The VS Code extension has to be updated as well. 0.3.0 accepts v13 and
answers a v15 database with "Database was created with a newer version
(schema v15). Update the extension." This repository now carries 0.4.0,
which accepts v15. The extension is published apart from the Python package,
so until a 0.4.0.vsixis built and installed, the Code Graph panel shows
that error once the CLI has moved the graph to v15. -
Run
code-review-graph buildonce.updateonly parses files that
changed, so most fixes in this release reach a file only when it is parsed
again: the new file types (HTML, XSLT, Liquid, JSON, plain YAML,.cjsand
.kts), translation files losing their key nodes,vendor/folders that
hold source, workspace and npm alias imports, tsconfigbaseUrland
extends,extra.import_contexton import edges, the call and test fixes
(#984, inherited methods,tests_forfor Java, Ruby and PHP) and the
confidence tiers. Ruby files (v14 above), files with nested classes and C#
files are the exception: the nextupdateparses them again by itself. If
the graph has embeddings, runcode-review-graph embedafter that, because
renamed C# nodes leave their old vectors behind. -
Upgrade the package, then run
code-review-graph installagain. The
hooks and instruction blocks an earlier release wrote do not update
themselves. Install now also writespost-checkout,post-mergeand
post-rewritegit hooks; the edit hook for Claude Code, Qoder and CodeBuddy,
and the Codex hooks, call the newhook-updateandhook-statuscommands;
and the instruction block has new search guidance. Upgrade first: an older
CLI has nohook-update, so the new edit hook would do nothing. Install and
uninstall still recognise everything 2.3.9 and earlier releases wrote. -
New settings are off by default. Nothing changes unless you turn them on:
--enrichorCRG_ENABLE_ENRICHMENT=1(needs theenrichmentextra),
install --steering,install --gitignore,--follow-symlinksor
CRG_FOLLOW_SYMLINKS=1,CRG_CONTEXT_HISTORY=1,CRG_VECTOR_CACHE=1,
serve --http --multi-repo,serve --multi-worktreeand the Action's
detailsinput. -
Output and dependency changes. Build results no longer carry
summaries_computedorsummaries_s. Onfind_large_functions_tool,
semantic_search_nodes_toolandtraverse_graph_tool,result_countis now
the count before the cut. The lowest supported versions ofmcp,anyio,
tree-sitter,tree-sitter-language-packandpyyamlare higher, and
fastmcp 4 is allowed (see Changed). -
GitHub Action cache. The cache key segment moves from
schema13to
schema15, so the first run after upgrading restores no graph cache and
builds from scratch. Self-hosted runners can delete the old
code-review-graph-schema13-*entries.