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 |
|---|---|---|
| mezz analyze | One-shot analysis to JSON, DOT, Mermaid or ASCII. | You, a script, a CI job |
| mezz check | Grades a tree against the rules the repo declared. Exit 1 when one does not hold. | A CI job |
| mezz mcp | MCP server over stdio — 15 read-only tools. | A coding agent |
| mezz watch | Re-analyses on save; serves the graph API and an event stream. | The visualizer, the extension |
| mezz 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
mezz analyze ./my-project -f json -o analysis.json
# Filter by language, limit traversal depth
mezz analyze ./my-project -l rust -l python -d 5
# Pipe a diagram straight into a doc
mezz analyze ./my-project -f mermaid > docs/graph.mmd
# Expose the graph to an agent over stdio
mezz mcpmezz watch and mezz 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.
The ranking needs complexity scores, and not every parser computes them. Where one does not, the quality report comes back empty rather than short — which languages can be ranked is worth checking before you read an empty result as a clean bill of health.
A different question of the same graph
Everything above answers “is this code hard to change?”. This answers “can this drawing be followed?” — a question about how the files are organised rather than what is inside them. The two never mix: shape is reported beside the refactor score and never folded into it, because one number moving for both reasons could be acted on for neither.
New in Mezzanine 1.3.0.
| Tier | Adds |
|---|---|
| Cyclic | nothing — this is the floor |
| Tangled | acyclicity |
| Hierarchical | layering |
| Fractal | breadth, branching, doors, and children that hold the same shape |
| Gate | Bar |
|---|---|
| Acyclicity | 1.00 |
| Layering | ≥ 0.70 |
| Branching | ≥ 0.70 |
| Breadth | ≤ 8 children |
| Entry concentration | ≥ 0.60 |
| Child compliance | ≥ 0.80 |
| Compliance | ≥ 0.85 |
The one thing that can fail a build
mezz check grades a tree against rules the project declared in .mezz/rules.json. Every failure it can produce traces to a bar somebody in the
checked repo wrote down, and a repo with no such file passes and says so. Four rules are
understood; the threshold on each is the project's.
| Rule | Counts |
|---|---|
| max_entities_per_file | Declarations a reader meets on opening the file, minus what a class-like entity owns. |
| max_elements_per_entity | Members of a class-like entity; parameters of a callable. |
| max_doors_per_folder | Files outside the folder land on, ties included — how many front doors it really has. |
| max_importers_per_file | Other files depending on this one, counted against the file that declares what they use rather than the one they named. |
Exit 0 when every declared rule holds or none was declared, 1 when one does not, 2 when no
verdict could be computed — the last covering both an unusable rules file and an analysis
that could not run, because a gate that passed because it did not understand its own
configuration is the failure the command exists to stop. min_shape: fractal is
deliberately not declarable: the tiers are Mezzanine's, and a tool may not fail your build with a
diagnosis you are free to disagree with.
Added in 1.2
A quality number is meaningless without its population, and the panel had one figure over the whole analysis sitting beside another over the selection with nothing distinguishing them. There is now a single picker — the whole scope, the scope tree's selection, what the canvas is drawing, the current selection, the file open in the editor, or the files a diff touched — and every number in the panel derives from it.
A population whose signal is missing is disabled rather than quietly falling back, because naming one population and showing another is the failure this was built to end. The aggregate is weighted by lines, so a cluster of small healthy getters cannot mask the large complex function beside them.
And the more interesting half
Coupling is counted over dependency edges. In a document graph there are none — every
folder reported fan-in 0, fan-out 0 while the canvas plainly drew
arrows between those same folders.
Zero is the flattering end of that scale, so an unmeasured folder read as a decoupled one:
the reading least likely to be double-checked. Those cells now print an em dash and the
reason, but only when the count is zero and references exist — a folder with real
coupling keeps its real number, and a folder with neither keeps its 0, which is
earned and has to stay distinguishable from the other two.
Coverage
The tiers are about which edges come out, not about first-class versus fallback. The top
tier emits exact UsesType edges from signatures and fields — that is where
impact analysis is type-accurate rather than approximate.
The fallback tier is empty of named languages for the first time: Go got a dedicated parser in the top tier, and JavaScript now reads through the TypeScript parser sitting next to it. What is worth knowing before you install is where support is thin rather than absent — Svelte emits no call edges, and Kotlin does not resolve a nullable type to the class in its own file.
Every language, what each parser emits, and where support stops →
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 mezz and is
installed by the same command.
For agents
15 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 Mezzanine 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 Mezzanine 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, Go, Kotlin, Dart, Python, Groovy. 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.