An architectural audit where a finding is admissible only once its cost has been measured. Not a linter: the cost is not in the import graph, it is in the git history, and here it is the admission rule rather than a decoration.
Every architecture linter can tell you that a module has fan-in 40 or that there is an import cycle. None can tell you what it costs. A finding that cannot be priced does not enter the report — that single rule is what makes the output short enough to act on.
The method itself is one page: DOCTRINE.md — what the audit requires at each step, the risk catalogue, the admission rule, and what the tool does when it cannot deliver.
The name is Russian: зодчий, a master builder.
Step one is a script. No model, no dependencies, and it is the whole measurement — everything after it is judgement built on top:
git clone --filter=blob:none https://github.com/Textualize/rich /tmp/rich
cd /tmp/rich && git checkout 9d8f9a372cc5
python3 zodchiy.py measure /tmp/rich --since "20 years ago" --out measure.json{
"calibration_passed": true,
"blocked_metrics": [],
"confidence_ceiling": "verdict",
"commits_with_code": 1942,
"files": 213,
"edges": 750,
"runtime_cycles": 1,
"type_only_cycles": 1,
"containment": 0.672,
"coupling_pairs": 289
}Read the first three fields before the rest. calibration_passed false means
the history is too thin to price anything, blocked_metrics names what could
not be measured, and confidence_ceiling is the strongest verdict this run is
allowed to reach. On a young repository the tool refuses most of its own
metrics rather than reporting weak ones — that refusal is the product.
Then the doctrine turns the measurement into findings. Here is one, taken verbatim from the ledger of that run rather than retold:
| Finding | console.py concentrates the widest fan-in and the highest rework rate in the package |
| Risk | R1, priority 1, confidence finding |
| Symptom | 2699 LOC, 116 functions, max nesting 11, fan-in 110 — the 100th percentile of this repository. 463 edits, more than any other file, and a Wilson lower bound on the rework rate of 0.50: half of the changes to this file come back as a fix within days, against a repository-wide 0.263. |
| Remedy | Extract the render pipeline (segment assembly and the render_* helpers) into its own module and leave Console a facade that delegates. An extraction with the public API unchanged, not a redesign. |
| Predicted gain | Half of the change traffic to console.py stops returning as a fix within the week. |
| Falsifiable as | behavior.stability.unstable_files[file=rich/console.py].rework_rate_lb <= 0.40 in the next measurement |
Where every one of those numbers came from:
structure.hubs[file=rich/console.py].fan_in
structure.complex_files[file=rich/console.py].max_nesting
behavior.hotspots[file=rich/console.py].edits
behavior.stability.unstable_files[file=rich/console.py].rework_rate_lb
behavior.stability.rework_rate
examples/rich/ is that run in full — measurement,
ledger, twelve refutation verdicts, report, SARIF. Read report.md first: the
largest number in that measurement, a 50-file dependency cycle, is the one
thing the report refuses to turn into a recommendation, because its cost could
not be established.
The canonical form is an agent skill. ./install.sh finds the harnesses
present on the machine and refuses to overwrite anything it did not write.
git clone https://github.com/Socialpranker/zodchiy && cd zodchiy && ./install.shPython 3.11+ and git. Nothing outside the standard library, by design: a
tool that audits other people's repositories should not add a supply chain to
do it.
python3 zodchiy.py snapshot .zodchiy/measure.json --out .zodchiy/baseline.json
python3 zodchiy.py gate .zodchiy/measure.json --baseline .zodchiy/baseline.json # exit 1 on regression
python3 zodchiy.py export --findings .zodchiy/findings.csv --measure .zodchiy/measure.json --format sarif| Axis | Source | Blind to |
|---|---|---|
| Intent | ADR, README, agent instructions, import-linter / ArchUnit / eslint-boundaries | lies once the docs fall behind the code |
| Structure | scripts/structure.py — import graph, cycles, fan-in/out, complexity |
DI, registries, reflection, string keys |
| Behaviour | scripts/behavior.py — churn, fix share, co-change, ownership |
code that has not changed yet |
A structural smell nobody ever pays for is not a finding here. Neither is a painful file with no structural cause — that one is a staffing problem.
The rule this tool applies to other people's code applies to its own claims.
Verified.
- 171 unit tests, green
- 14 frozen end-to-end cases against ground truth, each
dirtycase paired with acleanone — without the pair, "found N" cannot be told apart from "found N plus ten false ones" (method borrowed from OWASP Benchmark) - degenerate repositories: no commits, one commit, non-UTF-8 file, detached HEAD, not a git repository at all. Each must answer with an explicit field, not a traceback
- the test harness itself, by mutation: it goes red when it should
- the one live run over a foreign repository is held as a regression, not kept as an illustration: paths resolve, every finding passed step 4 and reached both the report and the exports
- the skill's own
description, against a catalogue of twelve competing tools over 20 cases, half of them phrased without the words "architecture" or "tech debt". Split train/test, edited ontrainonly. After the edit:testrecall 0.83, false positives 0.25 — the price of widening it, stated rather than hidden
Not verified, stated plainly because a tool that hides its own gaps has no business auditing yours.
- no adapter has ever been run inside its target harness. Built is not working
- the trigger evaluation ran once; variance between repeats is unmeasured. Its off-domain control failed outright: swap the description for one about code formatting and three of four architectural requests still land here. The floor is set by the catalogue and by the skill's name — what the description demonstrably moves is 6 cases out of 20
- step 4, the refutation pass, has never run end to end with independent lenses — only the mechanics of recording its verdict are covered
reconmode has never been run- thresholds were calibrated on two repositories and will drift on a third. Percentiles travel between projects; absolute numbers do not — CALIBRATION.md
Six steps. The first is a script; the rest are a doctrine an agent executes, and each has something mechanical holding it honest.
- Measure. Cycles (type-only ones kept apart — they exist to break cycles), hubs and fan-in through barrels, per-function complexity, temporal coupling, hotspots by fix share, change spread, rework within days, ownership. Every absolute number arrives with its percentile, and the calibration — blocked metrics, regex fallbacks, history window — is part of the output rather than a footnote.
- Map, without verdicts. The tops of the rankings, never the tree of
directory names; every claim marked
OBSERVED,INFERREDorUNKNOWN. A verdict issued before the map drags the whole analysis behind it. - Judge. A risk catalogue (R1–R6) behind a materiality gate, Pain × Spread. Each entry carries its own what not to flag: a composition root with high fan-out is a design, not a mess.
- Refute. Every high-priority finding is attacked through four lenses. What survives is marked as surviving; what does not is dropped, not softened. Without subagents the lenses run sequentially — and then confidence is capped and the degradation is printed in the report.
- Report. Opens with
Checklist: X/Y, closes with blind spots assembled mechanically: blocked metrics, regex-parsed files, the history window, links no import graph can see, and how step 4 actually ran. Silently truncating a top-N list is forbidden — say how many were dropped. - Artefacts. Current-state document, ADRs, migration plan, and a findings ledger the next run compares itself against.
Three modes: audit (your own repository), plan (findings into decisions and
their order), recon (a repository you have just been handed).
Every finding must predict a gain, and verify compares that prediction
against the next measurement. An audit nobody can be wrong about is not an
audit.
Numbers come from a script. Judgement comes from the model. Never the reverse.
No metric is eyeballed — not fan-in, not complexity, not a cycle.
No finding is issued before the full map is built.
Prose cannot be told apart from a number recalled from memory; a path can be
checked, and zodchiy.py selfcheck refuses a report whose numbers do not
resolve.
| Path | What it is |
|---|---|
SKILL.md |
the doctrine itself — the audit an agent executes. Russian, 23 KiB |
DOCTRINE.md |
the same method in one English page |
zodchiy.py |
the CLI: measure, snapshot, diff, gate, add, refute, selfcheck, verify, export |
scripts/ |
the measurement — structure.py (graph), behavior.py (history), ledger.py (findings, gate, export) |
references/ |
what the doctrine consults: risk catalogue, materiality gate, axis disagreement, refutation lenses, remedies, artefact templates |
examples/rich/ |
one complete run over a foreign repository, held as a regression |
evals/ |
the regression suite — unit, frozen end-to-end, degenerate repositories, the live run, the description eval |
schema/, dist/ |
findings JSON schema; adapters generated from SKILL.md |
SPEC.md |
why each rule is what it is — sources, calibration decisions, rejected alternatives |
One text under four names — each harness looks for a different filename.
| File | Goes to |
|---|---|
AGENTS.md |
Codex (~/.codex/AGENTS.md, or the repository root), Grok Build, Cursor, Zed |
GEMINI.md |
Gemini CLI (~/.gemini/GEMINI.md, or the repository root) |
QWEN.md |
Qwen Code (~/.qwen/QWEN.md) |
IFLOW.md |
iFlow CLI (~/.iflow/IFLOW.md) |
| Slash command | Goes to |
|---|---|
gemini/commands/zodchiy.toml |
~/.gemini/commands/zodchiy.toml |
iflow/commands/zodchiy.toml |
~/.iflow/commands/zodchiy.toml |
Claude Code and Grok Build need no adapter — the whole directory goes to
~/.claude/skills/zodchiy/ or ~/.grok/skills/zodchiy/. Adapters are generated
from SKILL.md by scripts/build_adapters.py; do not edit dist/ by hand.
The doctrine — SKILL.md, SPEC.md and references/ — is in Russian. It is
read by a model, and models read Russian. Translating 280 KiB would create a
second copy that drifts from the first within a month, which is the exact
failure mode this repository exists to detect. English is the interface:
README, installer, CLI help of the public documents.
MIT. See LICENSE.