A queryable graph of a codebase: what it contains, what it touches, and what crosses its boundaries — and a CodeQuality Score derived from it.
cqx is repository-agnostic. It was built while auditing a large Rust workspace and is calibrated against ripgrep, tokio and deno, but nothing in it is specific to any project.
cqx scans source, emits a language-agnostic stream of facts, loads them into a
graph database, and serves a web explorer over the result.
It is two halves, and they are deliberately separable:
The index is useful with no UI at all. That is on purpose: the UI is how you explore a codebase, the index is how you defend one.
Every category starts at 100 and is degraded only by a named rule with a published weight and a cap, so a score is a list of findings rather than a curve fitted to an imaginary average codebase. No reference population is needed, and every deduction opens to the file and line that caused it.
The rules target the failure modes of code written fast by many hands — effects escaping their crate, lints switched off wholesale, the same concept spelled four ways, bodies copied rather than shared — not abstract elegance.
Thresholds are anchored on real projects rather than intuition, which has already
corrected two wrong assumptions: large files turn out to be normal in Rust
(ripgrep keeps 81% of its lines in files over 500), and raw unsafe counts
measure a project's domain rather than its discipline (tokio and deno carry an
order of magnitude more than a CLI does).
Zoom levels are not separate datasets. They are aggregations over a single node set joined by a containment spine:
system ⊃ binary ⊃ package ⊃ directory ⊃ file ⊃ symbol ⊃ span
A package→package arrow is not a primitive fact. It is a rollup of the file and symbol edges beneath it, so clicking one decomposes into the call sites that constitute it. Compute an edge twice by two different routes and the levels disagree; then nobody trusts the picture. So: compute once, aggregate up.
The spatial spine is the filesystem, not the module tree. Directories, files and symbols exist in every language; module systems do not.
node := (id, kind, attrs)
edge := (kind, from_id, to_id, evidence[], confidence)
evidence := (file, line_span, extractor, source: static | runtime, observed_at?)
An edge without a file and a line span does not exist. This is what separates a map from a diagram — every arrow is clickable down to the line that created it, and an edge nobody can justify is a bug in an extractor rather than a lie in the UI.
Structural edges are numerous and boring. Effect edges are few and dangerous. The default view shows effects; structure is a layer you turn on.
| kind | meaning |
|---|---|
contains |
the zoom spine |
depends_on |
package → package |
imports |
file → symbol |
calls |
symbol → symbol |
spawns |
symbol → external process |
reads_env |
symbol → environment variable |
crosses |
a declared boundary (language edge, client/server split, host call) |
effect_fs / effect_net / effect_exec |
symbol → capability |
Only contains and depends_on are required. An extractor emits what it can
prove and says so; missing edge kinds degrade the view, they never corrupt it.
Identifiers are path-addressed, never line-addressed:
pkg:grep_searcher
file:crates/searcher/src/sink.rs
sym:grep_searcher::sink::matched
Three properties follow, and none of them are available otherwise:
Edges carry validity in commit space (first_seen, last_seen). The graph at
any commit is a filter; "when did this edge appear" is a field read rather than
a bisect. With stable IDs the commit-to-commit delta is small, so CI appends
rather than snapshots.
The core knows nothing about any language, and there are two ways in.
Bundled extractors are crates. Each one owns a command and registers it with
the CLI registry from deka-cli-core; the cqx binary is composition only, so a
handler can exist nowhere but its owning crate (the rfd#61 pattern, which exists
precisely because implementations kept leaking into the core crate).
// crates/cqx-rust/src/lib.rs — the handler body lives with the language
pub fn register(registry: &mut Registry) {
registry.add_command(EXTRACT_COMMAND);
registry.add_flag(FlagSpec { name: "--quiet", .. });
}
// crates/cqx/src/main.rs — the binary's whole job
RegistryBuilder::new().with(cqx_rust::register)
Out-of-tree extractors are programs. Anything that writes the same facts to stdout participates without being in this repo:
cqx-extract-<lang> <path> > facts.ndjson
Either way the schema is identical. Language-specific vocabulary lives in a
lang: attribute namespace, never in core node or edge kinds.
cqx is for
relationships between things.crates/
cqx/ the binary — registry + dispatch, no handler bodies
cqx-schema/ fact schema + stable ids (the contract everything else depends on)
cqx-rust/ Rust extractor: owns the `extract` command
fixtures/basic/ a workspace with deliberately planted facts
The web explorer lives in cqx-web, so
this repository stays Rust. deka explore in the deka toolchain is a downstream
consumer of cqx, not a part of it.
Defaults are calibrated against ripgrep, tokio and deno. Override any of them in
a cqx.json, searched for upward from the scanned path:
{
"version": 1,
"min_score": 70,
"rules": {
"exit-in-library": { "weight": 10 },
"duplicated-bodies": { "enabled": false }
}
}
A file need only mention the rules it changes. Every field can also be set from the environment, which is how CI usually wants to do it:
CQX_RULE_EXIT_IN_LIBRARY_WEIGHT=10
CQX_MIN_SCORE=70
CQX_CONFIG=/path/to/cqx.json
Precedence is defaults, then file, then environment, then flags.
cqx score --explain prints every rule with its thresholds and where each one
came from. --min-score exits non-zero when any category falls below it, which
is the CI gate.
cargo run -p cqx -- extract fixtures/basic
cargo run -p cqx -- extract /path/to/a/workspace --out facts.ndjson
fixtures/basic exists so output can be checked against a known answer instead of
eyeballed: a planted process spawn, two env reads, an unsafe block, a static mut, filesystem and network effects, and a library that calls process::exit.
cqx history /path/to/repo --commits 20
Materialises each commit with git archive — the working tree is never touched,
so this is safe against a repository somebody is using — extracts, scores, and
writes history.json with a per-category score and the change against the
previous commit.
A commit's score can never change, so each one is written once and reread thereafter: twelve commits of a 75k-line workspace take 15 seconds cold and half a second warm.
Nothing here is a fact about good code; some of it is a house standard. File length is the clearest case — measured across ripgrep, tokio, deno, deka and dsc, it tracks a project's habits rather than its quality, and the best-regarded codebase in that set has the most large files. So cqx ships a lenient default and makes the knob obvious.
cqx config show # or --json, for a reader that is not a person
cqx config set oversized-files.max_lines 2500
cqx config set oversized-line-share.enabled false
config set rewrites one field and leaves the rest of the file alone, checks the
rule and field exist, and names the accepted fields when one does not. The
effective configuration travels with every result — score --json and
history.json both carry it — so anything rendering those shows the standards
they were scored against rather than cqx's defaults.
cqx reads a workspace by parsing manifests rather than by running
cargo metadata, so cargo is the oracle: whatever it reports about packages,
versions and target roots is what the parser has to reproduce.
reference-repos.toml pins ripgrep, tokio and deno by commit, and CI fetches
them to check the parser against all three. They are here because our own
repositories were not enough — three cargo rules were found only when an
external project disagreed:
[[example]] targets do not replace discovery, they add to it[workspace.dependencies] makes a member even when nothing draws on itLocally the test skips when the checkouts are absent. Setting
CQX_REFERENCE_DIR asserts they are present, so a failed fetch fails the job
rather than quietly checking nothing.
A queryable graph of a codebase: what it contains, what it touches, and what crosses its boundaries — and a CodeQuality Score derived from it.
cqx is repository-agnostic. It was built while auditing a large Rust workspace and is calibrated against ripgrep, tokio and deno, but nothing in it is specific to any project.
cqx scans source, emits a language-agnostic stream of facts, loads them into a
graph database, and serves a web explorer over the result.
It is two halves, and they are deliberately separable:
The index is useful with no UI at all. That is on purpose: the UI is how you explore a codebase, the index is how you defend one.
Every category starts at 100 and is degraded only by a named rule with a published weight and a cap, so a score is a list of findings rather than a curve fitted to an imaginary average codebase. No reference population is needed, and every deduction opens to the file and line that caused it.
The rules target the failure modes of code written fast by many hands — effects escaping their crate, lints switched off wholesale, the same concept spelled four ways, bodies copied rather than shared — not abstract elegance.
Thresholds are anchored on real projects rather than intuition, which has already
corrected two wrong assumptions: large files turn out to be normal in Rust
(ripgrep keeps 81% of its lines in files over 500), and raw unsafe counts
measure a project's domain rather than its discipline (tokio and deno carry an
order of magnitude more than a CLI does).
Zoom levels are not separate datasets. They are aggregations over a single node set joined by a containment spine:
system ⊃ binary ⊃ package ⊃ directory ⊃ file ⊃ symbol ⊃ span
A package→package arrow is not a primitive fact. It is a rollup of the file and symbol edges beneath it, so clicking one decomposes into the call sites that constitute it. Compute an edge twice by two different routes and the levels disagree; then nobody trusts the picture. So: compute once, aggregate up.
The spatial spine is the filesystem, not the module tree. Directories, files and symbols exist in every language; module systems do not.
node := (id, kind, attrs)
edge := (kind, from_id, to_id, evidence[], confidence)
evidence := (file, line_span, extractor, source: static | runtime, observed_at?)
An edge without a file and a line span does not exist. This is what separates a map from a diagram — every arrow is clickable down to the line that created it, and an edge nobody can justify is a bug in an extractor rather than a lie in the UI.
Structural edges are numerous and boring. Effect edges are few and dangerous. The default view shows effects; structure is a layer you turn on.
| kind | meaning |
|---|---|
contains |
the zoom spine |
depends_on |
package → package |
imports |
file → symbol |
calls |
symbol → symbol |
spawns |
symbol → external process |
reads_env |
symbol → environment variable |
crosses |
a declared boundary (language edge, client/server split, host call) |
effect_fs / effect_net / effect_exec |
symbol → capability |
Only contains and depends_on are required. An extractor emits what it can
prove and says so; missing edge kinds degrade the view, they never corrupt it.
Identifiers are path-addressed, never line-addressed:
pkg:grep_searcher
file:crates/searcher/src/sink.rs
sym:grep_searcher::sink::matched
Three properties follow, and none of them are available otherwise:
Edges carry validity in commit space (first_seen, last_seen). The graph at
any commit is a filter; "when did this edge appear" is a field read rather than
a bisect. With stable IDs the commit-to-commit delta is small, so CI appends
rather than snapshots.
The core knows nothing about any language, and there are two ways in.
Bundled extractors are crates. Each one owns a command and registers it with
the CLI registry from deka-cli-core; the cqx binary is composition only, so a
handler can exist nowhere but its owning crate (the rfd#61 pattern, which exists
precisely because implementations kept leaking into the core crate).
// crates/cqx-rust/src/lib.rs — the handler body lives with the language
pub fn register(registry: &mut Registry) {
registry.add_command(EXTRACT_COMMAND);
registry.add_flag(FlagSpec { name: "--quiet", .. });
}
// crates/cqx/src/main.rs — the binary's whole job
RegistryBuilder::new().with(cqx_rust::register)
Out-of-tree extractors are programs. Anything that writes the same facts to stdout participates without being in this repo:
cqx-extract-<lang> <path> > facts.ndjson
Either way the schema is identical. Language-specific vocabulary lives in a
lang: attribute namespace, never in core node or edge kinds.
cqx is for
relationships between things.crates/
cqx/ the binary — registry + dispatch, no handler bodies
cqx-schema/ fact schema + stable ids (the contract everything else depends on)
cqx-rust/ Rust extractor: owns the `extract` command
fixtures/basic/ a workspace with deliberately planted facts
The web explorer lives in cqx-web, so
this repository stays Rust. deka explore in the deka toolchain is a downstream
consumer of cqx, not a part of it.
Defaults are calibrated against ripgrep, tokio and deno. Override any of them in
a cqx.json, searched for upward from the scanned path:
{
"version": 1,
"min_score": 70,
"rules": {
"exit-in-library": { "weight": 10 },
"duplicated-bodies": { "enabled": false }
}
}
A file need only mention the rules it changes. Every field can also be set from the environment, which is how CI usually wants to do it:
CQX_RULE_EXIT_IN_LIBRARY_WEIGHT=10
CQX_MIN_SCORE=70
CQX_CONFIG=/path/to/cqx.json
Precedence is defaults, then file, then environment, then flags.
cqx score --explain prints every rule with its thresholds and where each one
came from. --min-score exits non-zero when any category falls below it, which
is the CI gate.
cargo run -p cqx -- extract fixtures/basic
cargo run -p cqx -- extract /path/to/a/workspace --out facts.ndjson
fixtures/basic exists so output can be checked against a known answer instead of
eyeballed: a planted process spawn, two env reads, an unsafe block, a static mut, filesystem and network effects, and a library that calls process::exit.
cqx history /path/to/repo --commits 20
Materialises each commit with git archive — the working tree is never touched,
so this is safe against a repository somebody is using — extracts, scores, and
writes history.json with a per-category score and the change against the
previous commit.
A commit's score can never change, so each one is written once and reread thereafter: twelve commits of a 75k-line workspace take 15 seconds cold and half a second warm.
Nothing here is a fact about good code; some of it is a house standard. File length is the clearest case — measured across ripgrep, tokio, deno, deka and dsc, it tracks a project's habits rather than its quality, and the best-regarded codebase in that set has the most large files. So cqx ships a lenient default and makes the knob obvious.
cqx config show # or --json, for a reader that is not a person
cqx config set oversized-files.max_lines 2500
cqx config set oversized-line-share.enabled false
config set rewrites one field and leaves the rest of the file alone, checks the
rule and field exist, and names the accepted fields when one does not. The
effective configuration travels with every result — score --json and
history.json both carry it — so anything rendering those shows the standards
they were scored against rather than cqx's defaults.
cqx reads a workspace by parsing manifests rather than by running
cargo metadata, so cargo is the oracle: whatever it reports about packages,
versions and target roots is what the parser has to reproduce.
reference-repos.toml pins ripgrep, tokio and deno by commit, and CI fetches
them to check the parser against all three. They are here because our own
repositories were not enough — three cargo rules were found only when an
external project disagreed:
[[example]] targets do not replace discovery, they add to it[workspace.dependencies] makes a member even when nothing draws on itLocally the test skips when the checkouts are absent. Setting
CQX_REFERENCE_DIR asserts they are present, so a failed fetch fails the job
rather than quietly checking nothing.