God Class
A container that accumulated everything: too many methods, too much summed complexity, too many collaborators.
Component 1 of 3 · the base
Per-language parsers feed a single shared model: Entities (functions, classes, modules, files) joined by typed Relationships (calls, contains, uses-type, imports, implements). Every metric, smell, panel and agent tool anywhere in this project is a question asked of that one graph.
On its own
The engine is two Rust binaries and no service. It reads a directory and writes a graph — to a file, to stdout, to a socket, or to an agent over MCP. The visualizer and the VS Code extension are both clients of what this layer already emits; neither is needed for the analysis itself.
| Entry point | What it does | Who calls it |
|---|---|---|
| nao analyze | One-shot analysis to JSON, DOT, Mermaid or ASCII. | You, a script, a CI job |
| nao mcp | MCP server over stdio — ten read-only tools. | A coding agent |
| nao watch | Re-analyses on save; serves the graph API and an event stream. | The visualizer, the extension |
| nao serve | Several already-analyzed repositories under one API. | The visualizer, hosted |
| elevator | The same engine pointed at .elv spec files. | You, and --check in CI |
# One-shot analysis to JSON — no UI involved
nao analyze ./my-project -f json -o analysis.json
# Filter by language, limit traversal depth
nao analyze ./my-project -l rust -l python -d 5
# Pipe a diagram straight into a doc
nao analyze ./my-project -f mermaid > docs/graph.mmd
# Expose the graph to an agent over stdio
nao mcpnao watch and nao serve exist to feed a renderer, but they are
ordinary HTTP servers — anything can read them, and their
responses are the same JSON analyze writes.
Measurement
| Metric | Reads as |
|---|---|
| Cyclomatic complexity | Independent paths through a callable — the classic branch count. |
| Cognitive complexity | How hard it is to follow, weighting nesting over raw branching. |
| Max nesting depth | The deepest indentation level reached. |
| Fan-in / fan-out | How many entities depend on this, and how many it depends on. |
| Instability | fan_out / (fan_in + fan_out) — 0 is a stable dependency, 1 is a leaf. |
| WMC | Weighted Methods per Class: the summed complexity a container carries. |
| Chain depth | Longest outbound call chain — flags Law-of-Demeter violations. |
| PageRank | Centrality in the call/use graph: what the codebase actually revolves around. |
| Composite score | The single number that sorts a refactor list. |
Signals, not gates
A container that accumulated everything: too many methods, too much summed complexity, too many collaborators.
A function that does nothing but route — high fan-out, no depth of its own.
A method more interested in another class’s data than its own.
One concept whose every change forces edits across many files.
Fields with no behaviour — flagged separately, because Kotlin data classes and Python dataclasses are the idiomatic form of the same pattern.
Detected in a post-parse pass, then surfaced wherever you are looking: the quality MCP tool, the Quality panel in the visualizer, red rings on the graph. Nothing here fails a build —
the ranking exists to point at what to read next.
Coverage
The tiers are about what you get, not about first-class versus fallback. The upper tier
emits exact UsesType edges from signatures and fields — that is where impact
analysis is type-accurate rather than approximate.
Everything else — including Go — falls back to a generic parser: entities, but no reliable relationships.
The other binary
.elv spec files are parsed by the same machinery that parses source, which is
why --drift can compare the two. It ships alongside nao and is
installed by the same command.
For agents
Ten read-only tools over stdio, emitting compact ranked text rather than JSON — the same graph, shaped for a consumer that pays by the token.
The agent loop →Honest limitations
Against a rust-analyzer oracle on nao itself, Rust, tests included, 3,649 exactly-resolved call edges. Re-measurable from the repository with the benchmark script that ships with it.
Call resolution is name-based and locality-ranked, not type-resolved. An empty impact result is strong evidence, not proof: roughly one
real dependent in sixteen is still missing, so Used by (0) on something you
expected to be called is a reason to go and check.
Rust only, on nao itself, because rust-analyzer is the only oracle wired up. Assume the other parsers are worse rather than equal.
let c = spawn()?.a().b().All three need return-type inference, which nothing currently does. What is still mistargeted is a long tail of same-directory same-name ambiguity: locality prefers the nearest candidate, which is not always the one in scope.
UsesType is emitted — Rust, TypeScript,
Svelte, Java, Kotlin, Python. Elsewhere impact approximates it via
"used via members".similar is identifier- and signature-based: it finds nameable duplication,
not structural clones.--drift verifies the anchors; a description that is wrong without
naming anything checkable still needs a reader.