CLI¶
The codingest command builds a .kgl code graph from a checkout or from git
revision(s), checks whether an existing graph is stale, and runs one-shot Cypher
queries against it. It’s a port of KGLite’s former kglite code-tree
subcommand.
Two ways to install the exact same command:
pip install codingest # the Python wheel bundles the `codingest` command
# or
cargo install codingest-cli # pure-Rust binary, no Python
The wheel links the codingest-cli Rust library into its extension and exposes
it through a console-script shim, so pip install codingest gives you the same
codingest build/status/query command as cargo install codingest-cli — no
separate install. Its KGLite dependency also installs the kglite command, so
the pip-only flow pip install codingest && codingest skill install is
self-sufficient.
codingest skill¶
Codingest owns and distributes the code-review Agent Skill because the skill drives Codingest’s builder lifecycle. Install it for Codex and Claude Code at user scope with:
codingest skill install
Use repeated --host codex / --host claude flags to select hosts, --project
for repository-local installation, and --dry-run to preview changes. Use
codingest skill uninstall to remove an installation managed by Codingest.
Installation migrates an old kglite-code-review directory only when it bears
KGLite’s managed marker. Codingest refuses to replace its own unmanaged target
and leaves unmanaged legacy directories untouched.
codingest build¶
Parse a directory into a graph and write it to a .kgl:
codingest build /path/to/repo
# → /path/to/repo/.kglite/code-review.kgl
Build committed content at specific git revisions — a multi-rev merged
graph with one node per entity across revs (each node carries revs: [str]
membership; scope a query with WHERE 'v2.0' IN n.revs):
codingest build /path/to/repo --revs v1.0 v2.0
Common options:
--output <path>— where to write the.kgl(default<repo>/.kglite/code-review.kgl).--rev <revspec>— build a single git revision instead of the working tree.--revs <r1> <r2> …— merge several revisions into one graph.--include-docs— also ingest markdown as:Docnodes linked to the code they mention.--no-tests— exclude test files/dirs from the graph.
Run codingest build --help for the full flag list.
codingest status¶
Report whether a previously-built graph is stale relative to the current tree (so a wrapper can decide whether to rebuild):
codingest status --output /path/to/repo/.kglite/code-review.kgl
codingest query¶
Run a read-only Cypher query against a saved graph and print the rows:
codingest query "MATCH (f:Function)-[:CALLS]->(g:Function) RETURN f.name, g.name LIMIT 20"
codingest cypher is a visible alias for the same subcommand.
It queries an artifact; it never builds one. Build-then-query is shell composition, which keeps each step’s flags and freshness semantics its own:
codingest build . && codingest query "MATCH (f:File) RETURN count(f)"
That is also why loading is the right default: a .kgl load is far cheaper
than a rebuild, and CI typically queries the same artifact many times. Reads
take no writer lease and writers replace the .kgl atomically, so query is
safe to run while a rebuild is in flight or while codingest-mcp --graph
serves the same file.
Options:
-g/--graph <path>— the artifact to query (default.kglite/code-review.kgl, relative to the current directory). Note the asymmetry withbuild/status, which spell the same file--output: forquerythe artifact is an input, and--graphmatches the vocabularycodingest-mcp --graphalready uses.--format human|csv|json— see below (defaulthuman).--timeout <secs>— abort the query after this long; a query that actually hits the deadline exits1. The value must be positive and finite (at most1e9seconds). Zero, a negative,nan/infand anything past that bound are rejected as usage errors (exit2) — “no timeout” is spelled by omitting the flag, so a--timeout=0can only be a mistake.--require-fresh— refuse to query a graph that is not provably fresh.--parallel— permit the engine’s parallel runtime for this query (default off). It is a permission, not an instruction: only operators that partition deterministically honour it, and each applies its own candidate-row gate, so a small graph runs single-threaded either way and the result is identical. Worth setting on a scan-dominated query over a large artifact; pointless — and mildly wasteful of cores a concurrent process may want — on a small one.-in place of the query reads the query text from stdin:codingest query - <<'CYPHER' MATCH (f:Function) WHERE f.branch_count > 10 RETURN f.qualified_name, f.branch_count ORDER BY f.branch_count DESC CYPHER
Output¶
Nothing is ever truncated. The MCP server’s 15-row inline preview is a
host-context budget; a pipe has no such constraint, so row budgeting here is
Cypher’s job — use LIMIT.
human(default) — a header line of column names, then one tab-separated row per result row, on stdout. String cells print raw with\t,\nand\rescaped; other values render as compact JSON. TheN row(s)summary goes to stderr, so stdout stays pure data and pipes cleanly.csv—CypherResult::to_csv()verbatim, byte-identical to what the MCP server exports for aFORMAT CSVquery.json— one compact{"columns": [...], "rows": [[...]]}object, the same single-line conventionstatus --format jsonuses.
A query that ends in FORMAT CSV is a parser-level output switch and
overrides --format, matching MCP behaviour so one query renders the same
on both interfaces. EXPLAIN works too — the plan rows print like data rows.
Mutation Cypher (CREATE/SET/DELETE/MERGE/…) is rejected by the engine’s
read path. The CLI adds no policy layer of its own.
Freshness¶
Before executing, query runs the same sidecar check as codingest status and
warns on stderr — it does not refuse:
warning: graph is stale: source changed since the graph was built
Three non-fresh outcomes all warn: the source changed, the .meta.json sidecar
is missing (a foreign or hand-copied .kgl), or freshness could not be
verified at all — the check itself fails when the recorded source directory is
unreadable or a recorded git revision no longer resolves, which is exactly what
a .kgl copied to another machine hits. Rows are still returned in every case;
refusing by default would break the copied-artifact and CI-cache workflows the
CLI exists for.
--require-fresh inverts that for CI: any non-fresh or unverifiable outcome
becomes a hard refusal with exit code 3, and no rows are printed.
Exit codes¶
Code |
Meaning |
|---|---|
|
success |
|
operational error — missing artifact, bad Cypher, timeout, I/O |
|
usage error (clap) |
|
|
Wheel caveat: the pip install codingest console script maps every error to
PyRuntimeError, so a stale refusal exits 1 there. Use the cargo binary
(cargo install codingest-cli) where the distinction matters.
Interpreting CALLS edges¶
Every CALLS edge the resolver produces carries three properties describing
how it was pinned, so a query can separate a confident edge from a guess:
Property |
Meaning |
|---|---|
|
the tier that pinned the edge, listed here best-evidence first — |
|
how many targets survived the tiers; |
|
whether the caller’s file is the callee’s file, or imports it |
When several call sites between the same pair disagree, the edge keeps the best-precision tier and the smallest candidate count. AGC control-transfer edges do not go through the tiers and leave all three null.
# high-confidence callers of `helper`
codingest query "MATCH (a)-[r:CALLS]->(b:Function {name: 'helper'})
WHERE r.import_backed AND r.candidates = 1
RETURN a.qualified_name"
Two limits on that idiom, both on import_backed:
It is a one-hop check. A caller that reaches the callee through a barrel that re-exports it reads as
falseeven though the call is real, so treatfalseas unconfirmed, not refuted — it is a filter, not a deletion criterion.On Python it covers absolute AND relative imports. Absolute imports (
from pkg.util import helper) resolve for a root-relative package tree (a package nested undersrc/is a known gap), and relative imports (from .util import helper,from ..pkg import y, aliased forms, and imports insideif TYPE_CHECKING:/try:/function bodies) resolve against the importing file’s own package — soimport_backedis meaningful on cross-file Python calls reached through either form. Multi-name from-imports expand per name:from pkg import sub, helperrecords one import per name, the resolver sends each to its submodule file when one exists and to the package otherwise. (Same-file Python calls are unaffected — they areimport_backed = trueon the same-file rule.) Through 0.1.7 no Python import resolved at all; absolute imports arrived in 0.2.0 and relative imports in this release.
Querying the result elsewhere¶
The output is an ordinary kglite .kgl, so any kglite surface reads it too:
kglite query /path/to/repo/.kglite/code-review.kgl \
"MATCH (f:Function)-[:CALLS]->(g:Function) RETURN f.name, g.name LIMIT 20"
or in Python, kglite.load("…/.kglite/code-review.kgl").
Accuracy harness¶
The codingest library crate also ships codingest_stats, a JSON accuracy
harness (CALLS-resolution stats + node/edge counts) used by the determinism
gate:
cargo run -p codingest --bin codingest_stats --release -- /path/to/repo