Component 1 of 3 · the base

One typed graph, from whatever your repository is made of.

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

Nothing above this layer is required.

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 pointWhat it doesWho calls it
mezz analyzeOne-shot analysis to JSON, DOT, Mermaid or ASCII.You, a script, a CI job
mezz checkGrades a tree against the rules the repo declared. Exit 1 when one does not hold.A CI job
mezz mcpMCP server over stdio — 15 read-only tools.A coding agent
mezz watchRe-analyses on save; serves the graph API and an event stream.The visualizer, the extension
mezz serveSeveral already-analyzed repositories under one API.The visualizer, hosted
elevatorThe same engine pointed at .elv spec files.You, and --check in CI
mezz
# 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 mcp

mezz 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

Nine metrics per entity, one score to sort by.

MetricReads as
Cyclomatic complexityIndependent paths through a callable — the classic branch count.
Cognitive complexityHow hard it is to follow, weighting nesting over raw branching.
Max nesting depthThe deepest indentation level reached.
Fan-in / fan-outHow many entities depend on this, and how many it depends on.
Instabilityfan_out / (fan_in + fan_out) — 0 is a stable dependency, 1 is a leaf.
WMCWeighted Methods per Class: the summed complexity a container carries.
Chain depthLongest outbound call chain — flags Law-of-Demeter violations.
PageRankCentrality in the call/use graph: what the codebase actually revolves around.
Composite scoreThe single number that sorts a refactor list.

Signals, not gates

Five smells, computed from metric combinations.

God Class

A container that accumulated everything: too many methods, too much summed complexity, too many collaborators.

Dispatcher

A function that does nothing but route — high fan-out, no depth of its own.

Feature Envy

A method more interested in another class’s data than its own.

Shotgun Surgery

One concept whose every change forces edits across many files.

Data Bag

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

Four tiers per folder, over the graph the canvas draws.

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.

TierAdds
Cyclicnothing — this is the floor
Tangledacyclicity
Hierarchicallayering
Fractalbreadth, branching, doors, and children that hold the same shape
GateBar
Acyclicity1.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

Mezzanine ships no rules of its own.

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.

RuleCounts
max_entities_per_fileDeclarations a reader meets on opening the file, minus what a class-like entity owns.
max_elements_per_entityMembers of a class-like entity; parameters of a callable.
max_doors_per_folderFiles outside the folder land on, ties included — how many front doors it really has.
max_importers_per_fileOther 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

Every figure says which entities it counted.

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

An unmeasured number is not a zero.

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

15 dedicated parsers, in three capability tiers.

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.

Typed — Entities, call edges, and exact type edges from signatures and fields

Rustno flow entitiesJavaTypeScriptPythonGoDartnullable types ghostGroovyKotlinnullable types ghostSvelteno call edges

Structural — Entities and relationships, but no type edges

ImpexAnsible / KubernetesMarkdownopt-in onlySQLtopology, not logicElevatorJavaScriptCommonJS require is invisible

Fallback — A line-based regex pass, not a grammar

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

elevator: the same engine, pointed one floor up.

.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.

The spec language →

For agents

mezz mcp is this page, without the reading.

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

Where the graph is exact, and where it is a strong lead.

93.7%recall — real dependents found
98.6%precision — of those found, correct

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.

Read the numbers this way

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.

That measurement is one corpus in one language

Rust only, on Mezzanine itself, because rust-analyzer is the only oracle wired up. Assume the other parsers are worse rather than equal.

What is still missed, and why

  • Closure parameters, whose type is the iterator's item type.
  • Locals bound to non-constructor expressions — let c = spawn()?.
  • Chained calls — 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.

Other edges of the envelope

  • Type usage is exact only where UsesType is emitted — Rust, TypeScript, Svelte, Java, Go, Kotlin, Dart, Python, Groovy. Elsewhere impact approximates it via "used via members".
  • The first call after a source change pays a full re-analysis — seconds, growing with repo size. Subsequent calls come from the warm cache in milliseconds.
  • similar is identifier- and signature-based: it finds nameable duplication, not structural clones.
  • Elevator's descriptions and code pointers are authored, so they are the one layer that can lag. --drift verifies the anchors; a description that is wrong without naming anything checkable still needs a reader.

All of this is in the repository, in more detail →