Roadmap
Governed by product intent. Anything here that would require a claim the intent forbids is not on this list, however interesting.
This project should stay a thin orchestration and prompt layer, not a replacement for mature analyzers.
The bet is clone-to-work-order. One public bet, stated here so a reader does not have to infer it from what is missing: this tool is finished for the job it claims, and the remaining work makes the first five minutes better — more languages, a better first run — not a different product. There is no second track. A stranger who clones this is looking at a tool, not at a process that will announce something else next month.
3.5.0 is that bet's first instalment, from an audit of what a stranger
actually sees: - pipes the prompt into an agent on every rendered output
(D161), the chat reply opens with the work order rather than a metric table
(D160), examples/demo produces a real work order in under
a minute, and the README names the analyzer pool instead of letting a first run
silently fall back to the built-in tier. None of it makes the tool do a
different job.
3.6.0 through 3.7.6 finished the first five minutes and made the security
pillar real. The starter config opens every parsed language rather than a web
subset (D162), and --prompt-output leads with the work order and issues no
rates on a run whose estimate was withheld (D163). A work item states its own
finding and the limit its kind is graded on, the verify command runs the
interpreter that audited, and a changed-only run stays inside its change
(D164–D175). Every audit now runs secure-code-agent for the security pillar
(D177); its findings and its own work order reach the report and the prompt
(D176, D179), a copy-paste prompt never authorises work the remediation prompt
withholds (D180), and the supported delegate release honours reviewed
suppressions (D182). 3.7.6 changed no behaviour: the published repository and
package describe the product rather than how it is built.
3.7.8 lets the security pillar be graded. The delegate config asserted no
scanner set, so every audit reported a posture of unverified however complete
its coverage was — a grade withheld for want of a declaration rather than for
want of evidence (D184). gates.require_scanners now names the scanners the
gate job installs, and the delegate pin moves to 0.12.5, where osv-scanner
finding no package sources is no longer read as a scanner failure (that tool's
D23) — the other half of the same symptom, and the reason this one stayed
hidden.
It also closes a smaller one found by reading this tool's own report. A person who opted into running the test suite, with no command recorded in their tier, got a refusal that named no remedy and a chat door that said nothing at all, because the discovery line only fires for a configuration written before the opt-in existed. This repository had been in that state for weeks, reporting "it did not run" on every self-audit (D185). The security pillar's scanner floor now installs on main and on tags rather than on every pull request: grading matters where a release is cut, and a feature branch reports the partial coverage that is the truth about what ran.
Shipped
Dependency-light native scanner; Markdown, JSON, SARIF and PR-comment output; bounded AI remediation prompt; changed-only mode; baseline gating; agent instruction packs; ISO/IEC 25010-inspired rubric calibrated against a query-selected corpus (40 repositories of Python/TypeScript/JavaScript through 1.10.x; 112 from 2.0.0, across the eight languages parsed at that time); git-history aspects (churn, hotspots, coupling, ownership); 92% coverage gate; portable invokable skill for Claude Code, Codex and Copilot Chat; optional local MCP server (writes only its five disclosed config/state artifacts, never source or reports) for Codex and its VS Code extension.
Sixteen declaration languages, each shipped as its own minor release: Python and Java, JS/TS/JSX and HTML, then C (1.1.0), C++ (1.2.0), C# (1.3.0), free-form Fortran (1.4.0), fixed-form Fortran (1.6.0), Swift (2.4.0), COBOL (2.7.0), Go, Rust, PHP and Ruby (2.11.0), Kotlin (3.8.0), and Shell (3.9.0), over one shared walk in _ranges_core where a language is a module and a row. Fortran is the first with no braces, so the walk takes its bounding rule as an argument. Fortran also gained an analyzer adapter (fortitude, 1.5.0), its own complexity and cognitive readings rather than the C-family default (1.6.0), and toolchain practice detection (1.7.0). Per-language scope and misses are in language support.
Test honesty, after Ken Pugh (5.1.0, Decision 19). Skipped tests, Python tests that compute their own expected value, and three authoring checks on .spectable specifications, all reported and none scored. Not yet: unknown-type checks on Spectables files, which wait on a published list of its built-in types, and computed expectations in languages other than Python.
ADR 001 is finished
Stage 9 shipped in 1.9.0, and the migration is complete. History materialization is separated from measurement: tools/calibration/history_manifest.py fetches and writes down what it fetched — pinned head, selection rule, selected commit ids, required parent objects and tool version — and measure_fix_breadth reads that manifest and never touches the network. A cache that has drifted from the pin is refused with the difference named, rather than measured as if it were pinned.
The checked-in history_manifest.json pins 33 subjects, 6,120 commit ids and 293 required parent objects. It also records something the study could not previously see: five of the original 38 subjects no longer exist — deleted or made private since selection — which is an argument for pinning rather than against it.
That closes the stage this project spent six audit rounds circling. The measurement that produced a false result did so because the oldest commit in a shallow clone has no parent, so git diffs it against the empty tree; each audit repaired the symptom it found because there was nothing to check a cache against. Now there is.
The evidence model, its property tests, consumer migration and the version-2 contract were already done; ADR 002 stays rejected because it assumed --fail-on-gate consumes a grade when it only checks hard findings.
The hostile audit has an artifact
ADR 013 shipped in 1.10.0. The adversarial loop is the highest-leverage quality process here and was the only one with no artifact: it depended on a person hand-writing a fresh prompt into a fresh session, re-deriving context the report already held, so runs were neither seeded nor comparable.
render_hostile_audit_prompt is now the third emitter on the prompt seam beside render_ai_prompt and render_agent_instructions — CLI --hostile-prompt-output, MCP prompt maintainability-hostile-audit. It seeds the adversary from one run: the commit under audit, the evidence already computed (what ran, what did not, which concepts nothing measured), P1–P8 with the shape of evidence that falsifies each, and the audit contract this project already holds itself to. The boundary is ADR 008's: the deterministic core seeds the hostile audit; it never performs it. No gate, no score, nothing written or sent.
The fifth pillar is delegated, not absent
Shipped in 3.2.0; run by every audit since 3.7.0. ADR 007's security pillar
was always outside what a deterministic maintainability scanner can read,
so it reported nothing. It now reports what a delegate measured:
secure-code-agent,
whose producer, practice level and condition render beside the four pillars
MA computes itself.
This is the boundary the Then section already names — security scanning is a neighbouring tool's job, and the better it gets the more an independent maintainability reading is worth. Delegation is how the two compose without either claiming the other's measurement. The file is a boundary in the ADR 008 sense: absent, malformed, wrongly-typed or self-contradicting input resolves to no delegated pillar, never to a partial one, and the two axes are still never averaged.
In 3.2.0 MA only read a document another step had written, so the pillar was
measured wherever CI ran the delegate and unmeasured everywhere else, including
every audit through the chat door. Since 3.7.0 (D177) every audit runs
secure-code-agent itself, installed beside this package rather than as a
dependency. Without it, or with an unsupported release, the report says the
pillar was not measured and gives the install command. A security-pillar.json
in the audited tree is no longer trusted on its own authority; a pipeline hands
one over explicitly with --security-pillar PATH.
Next
The developer's own analyzers, read from their pipeline — decided
2026-10-12 (Decision 17); the first slice shipped in 4.4.0. Declaration,
setup on every surface, coverage and the pointer from every unexamined
concern are in; whether those findings should score is the next decision,
and is not taken. The catalog holds some 760
tools and this one cannot run them all; the developer runs theirs in their
own pipeline and this tool reads what they wrote. Found from a C# field
report: C# is read by lizard and jscpd only, and the tools that cover the
rest (Roslyn analyzers, Roslynator, dotnet format) run inside a .NET build
this tool will not run. --sarif-input already reads such output, but on
the CLI only, listed and never scored, and nothing points a reader to it.
The work: declare the files in maintainability-agent.json and ask for
them in first-run setup on every surface; let their findings count toward
the concepts they cover; say in the coverage section, beside every
unexamined concept, that a file from the developer's own pipeline would
close it; and state the model in product intent.
The recalibration, as this entry read on 2026-09-04, before it shipped. It was the only scheduled work, and everything that touched measurement queued behind it — six then-unanchored languages (Swift, COBOL, Go, Rust, PHP, Ruby), plus the two built-in changes recorded in corpus policy: deleting the per-language cyclomatic regexes, and merging analyzer evidence per concept rather than per dimension.
Shipped in 3.0.0 on 2026-09-08. All four steps: the corpus re-measured, the per-language criterion scope fixed (D137), the criterion set completed from the built-in tier per concept, and the corpus extended from 112 repositories to 180 and from eight anchored languages to thirteen. CALIBRATION_C moved 8.7161 to 4.2565 and 33 of the 112 repositories in both corpora change grade, which is what made it a major.
Two of the four did not land as written, and both are recorded above with the measurement that withdrew them: deleting the per-language cyclomatic regexes would have cost the bare install half its decision points, and the per-concept merge had no analyzer-to-analyzer content to merge. COBOL is not anchored and will not be.
Paused, 2026-10-01: no new language support until the foundation is stronger. The three languages left below stay listed and are not scheduled. The pause changes the corpus policy: the recalibration no longer waits for them. It runs next, to anchor Kotlin and Shell, and it runs after the analyzer pool is re-pinned, because a reference measured on a drifted pool would anchor the drift. The order is: re-pin the pool, then recalibrate, then the smaller foundation work.
Both ran, and 4.0.0 shipped on 2026-10-02. The pool was re-pinned in
3.12.0 (#284); the recalibration re-measured all 201 repositories on it and
anchored Kotlin and Shell — fifteen and six repositories. CALIBRATION_C
moved 4.2565 to 4.3092. On the 180 shared rows the new pins alone would
change 4 grades and adding the two languages changes 9 (8 B→C, 1 C→B), so
it is a major, on 3.0.0's reasoning. COBOL is now the only unanchored
language.
The pool was re-pinned again in 5.0.0 (2026-10-26, #296). The weekly
drift check found newer releases; all 201 repositories were re-measured on
them, with lizard held at 1.24.0 in constraints/held.txt because 1.24.3
cannot parse JavaScript that 1.24.0 reads. pylint and cohesion contribute to
the corpus for the first time since D226, which moved the declarations
reference 0.0929 → 0.0944 and CALIBRATION_C 4.3092 → 4.35; 2 of 201
repositories change grade, so it is a major.
The foundation work behind the pause is done — the re-pin (3.12.0), the recalibration (4.0.0), and the loose ends that came with them. Language support stays paused until the maintainer decides otherwise. What came next was found by using the tool on itself and on the corpus: analyzers handed a file list read only part of a large repository (D220), the calibration's scanner fingerprint missed most of the measurement path (D221), and cohesion was recorded as failed wherever there was no Python (D223). Those ship together with a corpus re-measure, because D220 changes the readings of the largest repositories.
Shipped as 4.1.0 and 4.2.0 on 2026-10-03. 4.1.0 carried D220, D221 and
D223 with the corpus re-measure: ten repositories read differently, every
constant re-derived identical, and no score or grade moved. 4.2.0 stopped a
committed docs site counting as source (D224, #292), added
paths.exclude_additional, fixed reads on Windows for want of a flag
(D225), and moved CI to secure-code-agent 0.13.1. Nothing is scheduled
next; the maintainer chooses it.
What was next is language support, and that was the whole list. The audit itself is finished for the job it claims: sixteen languages parsed, the rubric calibrated against 201 repositories, the evidence model migrated, the report contract stable. Nothing on this page proposes making it do a different job.
Five languages were listed, in the order they were worth doing — two shipped, one withdrawn, two left — each on the terms ADR 006 already sets — a scanner of its own, a documented list of what it misses, tests that pin them, one per minor release:
| Language | Why this one |
|---|---|
| ~~Kotlin~~ | ~~the largest real gap. Android and modern JVM services; the analyzer catalog already maps it~~ — shipped in 3.8.0. The catalog did not map it: lizard reads Kotlin and the row never said so, which is the Fortran stale-row defect again and was fixed alongside. Kotlin is parsed and, since 4.0.0, anchored in the corpus, and its page says what it misses |
| ~~Shell~~ | ~~in nearly every repository, audited by nobody, and where a great deal of operational risk actually lives~~ — shipped in 3.9.0, parsed by default. No analyzer in the catalog measures shell complexity, so the built-in reader is checked against the POSIX and bash grammars instead of a second implementation, and its page says what it misses |
| ~~Objective-C~~ | ~~completes iOS beside Swift, which shipped in 2.4.0~~ — withdrawn 2026-10-04, by the maintainer's decision. What exists is legacy iOS code being migrated to Swift, which is already parsed; few new users would arrive for it. Not deferred: off the list, so it is not re-proposed. .m and .mm files are still recognised as source, so a report names them as unread rather than staying silent |
| Scala | completes the JVM beside Java and Kotlin |
| PowerShell | completes the Windows and enterprise side, beside the COBOL and mainframe reading |
Below those the audience thins faster than the work shrinks — Dart, Elixir and Zig are real languages this tool would serve few people by parsing. The list stopped at five on purpose, and is four now that Objective-C is withdrawn; a language is not added to it because someone asked.
The rest of "next" is maintenance, and each piece now speaks for itself. A tool that stops being built still ages in three places, none of which announced themselves until 3.9.0:
- the analyzer pool is pinned, and a fresh install eventually stops resolving. The weekly drift job no longer relies on somebody reading it go red: while it fails it keeps one GitHub issue open, and closes it when it passes;
- the calibration describes the code of its moment. Every report now
says when the corpus was measured (
reference.corpus_measured), and a weekly check opens an issue once that date is more than a year old, asking for a recalibration or a stated reason to keep it; - Python 3.12 is pinned in the per-push gates. The package claims 3.11 to 3.13, so a weekly job runs the suite on 3.11, 3.13 and the newest release, and a test holds every claimed version to a CI job that exercises it.
None of those is feature work. They are the maintenance surface of a finished tool, and each now opens an issue when it needs a person.
This line said "Swift" for seven minor releases after Swift shipped in 2.4.0, and named the remediation-integrity checks as the other near-term block after they closed in 2.1.0 through 2.3.0. A roadmap whose "Next" is already done tells a reader nothing about what is coming, which is the failure the delivery entry below describes in the other direction.
Language adapters
The cadence is one language per minor release, never a batch — five were written that way between 1.1.0 and 1.6.0, and the reason is that each one has to earn its claim separately.
The price of admission, unchanged since ADR 006: a language is claimed only when it has a scanner of its own, a documented list of what it misses, and tests that pin them. The register's original wording said no further range detectors would be written; five were, and the rule that wording protected is what survived. A language does not get onto the table below by having its extension added to a config.
Swift shipped in 2.4.0. Braced, so it reuses the _ranges_core walk; what needed its own answer was narrower than this section predicted, and one of the four predictions was wrong.
Right: an extension adds methods to a type declared elsewhere, so a member is reported as Widget.draw rather than a bare draw; a protocol declares bodiless requirements that mint nothing; computed properties are the C# properties problem again and are not declarations.
Wrong: "string interpolation puts braces inside literals". Swift interpolates with \(expr) — parentheses, not braces — and string contents are masked before any counting regardless. The real hazards were different and neither was predicted here: Swift has no statement terminator, so the shared bare-signature rule could not see where a protocol requirement ended and reported it as two lines of body; and class is both a keyword and a modifier (class Widget versus class func make()), so stripping it with the other modifiers cost every final class Store its type. Both are pinned by tests.
Swift also needed its own reading of a branch: guard is the language's primary early exit and is absent from the C-family pattern, so a guard-heavy function would have read as branchless — the defect Fortran shipped with.
Go, Rust, PHP and Ruby shipped in 2.11.0, on those terms and at the cost of learning them: an audit of that release found four defects whose constructs were in no fixture, two of them High (D125–D129). ADR 006 named Go and Rust as unwritten for as long as they were, and the register entry carries the correction rather than being edited to look prescient.
What an unwritten language already gets today. Scala is classified by discovery and has practice signals wired (.scalafmt.conf). What it does not get is a declaration population: rates are withheld with the missing parser named, rather than approximated. That is the P7 boundary and it is the reason a language is a release rather than a config entry.
This paragraph named Swift, Go and Rust until 2.11.0 — all three are parsed now, and it went on describing them as unwritten for the releases that wrote them. It named Kotlin until the correction after 3.8.0, one release past the one that parsed it.
Two gaps that widen with every language added, and should be closed alongside rather than after:
- ~~Test-command detection stops at Python.~~ Closed in 2.5.0.
_test_commandsreads the tree's build manifests and offers the command as the setup question's default, on every surface:swift test,fpm test,cargo test,go test ./...,ctest,mvn test,gradle test(the wrapper when one is checked in),dotnet test,npm testandpytest. Two properties are the whole design. It suggests, never decides — therequire_test_commandhard gate asks whether a human documented a command, and a value the tool wrote unasked would satisfy that gate on evidence it invented. Andxcodebuild test, named in the original entry, is deliberately not shipped: barexcodebuild testneeds-schemeand usually-destination, so every suggestion it could make is a command that fails, and a suggestion that always fails teaches the operator to ignore suggestions. - A new language inherits whatever the corpus does not hold. This was the sharper of the two gaps until 2.0.0: the corpus was 40 JS/TS/Python repositories and LAPACK read 7.18x the declaration median against an anchor containing no Fortran. It is now 112 repositories — across the eight languages parsed when it was measured. The gap was open again, and this sentence claimed it closed "for what ships today" through six releases that reopened it: Swift, COBOL, Go, Rust, PHP and Ruby all shipped parsed and unanchored, scored against an anchor holding none of them. That was the corpus policy working as decided rather than a defect, and it was the debt 3.0.0 paid: the corpus was 180 repositories across thirteen languages, and since 4.0.0 is 201 across fifteen, with COBOL alone unanchored, permanently, for the reason measured below. Extending the corpus may move
CALIBRATION_Cand re-grade every repository, so it stays a release of its own rather than a patch bundled with a scanner. Stated as a prediction, not a measurement: the sibling claim about scanner changes was measured on 2026-09-07 and did not hold, and this one has not been tested.
Corpus policy: recalibrate once, after the remaining scanners land
Decided 2026-09-04. A language ships parsed and unanchored: its scanner lands, and the reference corpus is not extended to hold it in the same release. The corpus is re-measured and CALIBRATION_C re-derived once, after the remaining planned scanners are written — not once per language.
Swift (2.4.0) is the first language to ship under this policy and is unanchored today. It is measured by a parser and scored against a 112-repository anchor containing no Swift. COBOL (2.7.0) and then Go, Rust, PHP and Ruby (2.11.0) shipped the same way, so six languages awaited the one recalibration rather than each paying for its own. 3.0.0 anchored five of the six; COBOL is the permanent exception recorded below.
The reason is cost, and it is worth stating exactly rather than as "it's slow". Reuse (--reuse) makes a corpus change cheap: adding one repository re-measures one and reuses 111. It does nothing for a language change, because adding a scanner changes the measurement code, which moves scanner_fingerprint, which invalidates every stored row. So every new language costs a full re-measure of the whole corpus — ~~and each one also re-grades every repository this tool has ever scored, since CALIBRATION_C moves~~. Paying that per language means paying it in full, repeatedly, for an anchor that is obsolete again at the next scanner.
The struck clause was wrong, and 2.11.0 turned out to be its test case: four scanners were added, the corpus was re-measured on 2026-09-07, and CALIBRATION_C and every corpus grade came back unchanged. The cost of a new language is the compute, which is real and is reason enough for this policy. It is not the re-grading.
COBOL is a permanent exception, decided 2026-09-07 and measured. Five of the six unanchored languages are anchored by the recalibration; COBOL is not, because it cannot be. Against the corpus criteria — created before 2021, still pushed, and a codebase rather than a collection — GitHub holds one COBOL repository above 500 stars and six above 50, and what is there is not COBOL code. The most-starred by a wide margin is cobol-programming-course, which the selector already excludes as curriculum. proleap-cobol-parser, che-che4z-lsp-for-cobol, koopa and opensourcecobol4j are Java projects that parse or compile COBOL. node-cobol and node.cobol are JavaScript wrappers. The one genuine COBOL codebase, cics-genapp, has 38 stars.
The language's real corpus is on private mainframes and no query reaches it. Anchoring COBOL on what is public would build a reference out of teaching material and parsers written in another language — an anchor that looks like one, which is worse than the disclosed absence this policy already ships. So COBOL stays parsed and unanchored indefinitely, UNANCHORED_LANGUAGES keeps saying so in every report that grades COBOL, and this paragraph exists so the question is not re-derived as an unfinished task.
What this policy costs, stated so nobody has to discover it: an unanchored language is scored against code unlike it, and its grades are therefore less trustworthy than an anchored language's. That is the same defect as LAPACK's 7.18x reading, accepted deliberately and for a bounded time rather than encountered by surprise. The honest framing is that an unanchored language's findings are as good as its parser and its grade is provisional. That was written about Swift, which 3.0.0 anchored; it now describes COBOL and only COBOL.
What must happen at that recalibration, so it is a checklist and not a memory: re-measure the full corpus with the then-current scanners, re-derive CALIBRATION_C by bisection, re-run the calibration corpus tests, extend the corpus to hold the six unanchored languages, publish the study, and ship it as a major release. All of it ran, and 3.0.0 shipped on 2026-09-08 — 180 repositories, thirteen languages, CALIBRATION_C re-bisected 8.7161 to 4.2565, 33 of 112 shared rows re-graded. Five of the six were anchored; COBOL was not, and will not be.
The first step ran on 2026-09-07, and it falsified the reason given here for the major. This paragraph used to end "— it re-grades every repository, which is a breaking change to every published number", written on 2026-09-04 before anything was measured. All 112 repositories were then re-measured at the same pinned commits under the current scanners — eleven minor releases and four new language scanners newer than the stored rows. 92 of 112 declaration readings moved. CALIBRATION_C re-bisected to the same value to four decimals, the two references that moved did so by 0.55% and 1.8% against a 10% tolerance, and no repository changed grade: A=5, B=52, C=55, median exactly 4.0000, before and after. Two repositories moved at the published decimal, by 0.1 each.
So scanner drift does not re-grade anything, and the claim that it does was a guess. ~~What is still unmeasured is the other half:~~ the other half was measured on 2026-09-08 and it held. Extending the corpus to hold Swift, Go, Rust, PHP and Ruby changes the population the medians are drawn from rather than the instrument reading it, and that turned out to be the mechanism with the size: CALIBRATION_C moved 8.7161 to 4.2565 and 33 of the 112 shared repositories changed grade, where scanner drift alone had moved nothing. So the major release stood on that half, as predicted here, and not on scanner drift. Provenance and the full table are in _calibration.py.
Two built-in changes ride on that same release, decided 2026-09-06. Both move scanner_fingerprint, so neither can ship on its own — that is the whole reason this list exists rather than paying per change.
- ~~Delete the per-language cyclomatic regexes.~~ Withdrawn 2026-09-07, measured. The argument was sound — twelve of the fourteen built-in scanners read a language
lizardalready covers, and every defect from D112 to D129 lived in those keyword sets, which makes them high-risk rather than low-value. The deletion is still not executable, because this same section requires it to leave the built-in path intact rather than empty, and measurement says it would leave it intact and wrong.
Deleting them means the generic C-family pattern reads every language. On this package alone that finds 3,708 of 7,642 Python decision points — 49% — because Python spells its boolean operators and and or and the C-family pattern looks for && and ||. Rust, PHP and Ruby under-report by half or more on a single representative line. A bare install would get a silent ~50% under-report on its only cyclomatic reading, which is a worse defect than the ones being avoided: those were bugs that could be found and fixed, this would be a systematic bias that reads as a clean number.
What replaced it, shipped the same day. The keyword sets are proved against an independent enumeration rather than deleted. tests/test_grammar_token_coverage.py asks the pinned lizard which tokens it counts as decisions for each language, and fails when a grammar fixture never exercises one — the blind spot in the existing comparison, which could only ever check constructs somebody had already thought to write down. That blind spot has a track record: D120, D125, D126, D127 and D128 were each a keyword set wrong about a construct no fixture contained, found in production rather than by the comparison test that ran the whole time.
Two escapes, both requiring a written reason: a token absent from the language (catch reaches Go from lizard's C-like base, and Go has none), or one this project counts differently on purpose (finally and ensure always run, so they add no path; a Rust where clause decides nothing at run time).
It found three gaps immediately — Swift's catch, Ruby's word-form or, and Fortran's upper-case IF/DO/CASE/.AND., which matters because fixed-form legacy Fortran is routinely upper case. In all three the reader was already correct and nothing had ever checked it. The fixtures now exercise them and agree with lizard.
- ~~Merge analyzer evidence per concept rather than per dimension.~~ Shipped 2026-09-07, and not the merge this entry described. The stated reason was wrong twice over. complexipy supplies cognitive complexity for Python, so the original list of languages lizard "can never satisfy the gate" for wrongly named Python and wrongly omitted TypeScript. And PMD does not supply it at all in the sense that matters: it is a verdict emitter, reporting rule violations above a threshold rather than a reading per declaration, so it can never produce a rate over a population. Measured: of the 112 corpus repositories, zero satisfied the criterion set without complexipy, including the eight that ran PMD.
So there was no analyzer-to-analyzer merge available — nothing to merge. The version that works is analyzer-to-built-in, per concept: lizard supplies cyclomatic complexity and declaration lines for sixteen languages and cognitive complexity for none; the built-in scanner supplies all three for fourteen; neither tier is complete alone and the union per declaration is. Joined on (path, start_line), measured exact — all 1,008 lizard declarations on this package match a built-in declaration at the same start line.
This also delivers what the withdrawn regex-deletion item was reaching for. Where lizard reads the language, lizard's readings now drive the score and the hand-written keyword sets supply only the one concept no FOSS tool measures — the reduction in dependence on them that deletion was meant to buy, without costing the bare install its only reader.
Two defects found in the shipping of it, both worth recording. It went in inert: the whole suite passed while the join matched nothing, because three path vocabularies meet there (./src/x.py from lizard, absolute from complexipy, src/x.py from the scanner) and every test built both sides from one string. Only a real audit on a real repository found it. Then the report contradicted itself — "Analyzer readings for declarations" beside "declarations measured by built-in detectors" — because the disclosure was computed before the merge. The note now runs the same computation as the score, which is D137's lesson one layer up, and states the composition rather than crediting one tier for the other's work.
- Cognitive complexity: the gap is proven, and it is narrow (2026-09-07).
Checked because "no tool measures this" was being carried on a catalog tag search, and the catalog's measures field is a different vocabulary from the adapters' concepts — it tags complexipy as complexity while the adapter declares cognitive_complexity, which is how the only working cognitive source was nearly missed. By the right instrument, exactly two wired adapters declare it: complexipy (Python, full population) and pmd (Java, violations only).
Sonar already does this and it is not our invention. SonarSource defined the metric in 2016, and SonarQube Community Build is LGPL and computes it free for ten of the sixteen languages here — Python, JavaScript, TypeScript, Java, Kotlin, C#, Go, Rust, PHP and Ruby. Claiming a differentiator on the metric would be the unfounded "better way" ADR 006 exists to stop.
The genuine capability gap is six languages: C, C++, Swift and COBOL are commercial-only in Sonar (COBOL is Enterprise), and Fortran and Shell are unsupported at any price. For those the built-in reader is the only permissively-licensed source.
The larger gap is shape, and it covers all sixteen. SonarQube is a server plus scanner plus database; this product is a local stdio process. Requiring an operator to stand up a server before a score is available is a different product, not an integration. Recorded as the reason Sonar stays unwired despite an LGPL licence, so nobody re-derives the licensing question and concludes the opposite. Today an analyzer must supply cyclomatic complexity, declaration lines and cognitive complexity or the whole declarations dimension falls back to the built-ins — and the only wired tool reporting cognitive is PMD, which covers apex, java, javascript, scala and xml. So lizard can never satisfy the gate on its own for Go, Rust, PHP, Ruby, Swift, C, C++, C#, Fortran, COBOL or TypeScript. ~~or Python~~ — corrected 2026-09-07: complexipy supplies cognitive complexity for Python, which is why the Python corpus members satisfy the gate legitimately, as _calibration.py has recorded since 2026-08-14. The original list named Python and omitted TypeScript; enumerated from the adapters, the tools emitting these concepts are lizard (cyclomatic, lines), pmd (cyclomatic, cognitive) and complexipy (cognitive). Requiring all three from any combination of sources keeps the criterion set complete and comparable, which is the property the current rule exists to protect, while letting lizard supply what it measures well. The join is (file, start_line) and is already proven at 968 of 1000 declarations on this repository by tools/complexity_oracle.py.
This item now has a defect attached to it: D137. Looking for the evidence behind the claim above turned up that the completeness check is written at repository scope and applied to units that are scored individually — so a C repository containing one setup.py passes it and has its C declarations scored on two criteria of three. Reproduced: the same 50 C declarations read None alone and 0.2451 with one trivial Python function added. 47 of the 112 corpus repositories satisfy the gate today, including FFmpeg, git, bitcoin, cp2k and PowerToys. Fixing the scope and merging per concept are the same change to the same function.
This does not open a question about the bare install, and an earlier draft of this paragraph wrongly said it did. The analyzer pool help already answers it: the pool is the primary evidence source, and "built-in detectors always run as the fallback for concepts the selected analyzers cannot measure." A user who declines the pool, or picks a density that selects nothing for their language, gets the built-ins — that is what the fallback tier is, it is disclosed in every report that uses it, and deleting the regexes above must leave that path intact rather than empty.
Not scheduled, and the distinction matters. Scala, Elixir, Zig and the rest are classified by discovery and may be measured by adapters, but no scanner is scheduled for any of them. They are not refused — they are unwritten, on the terms above. A language moves onto this list when it is decided here, not by being named in an older register entry.
Go, Rust, PHP and Ruby sat on that list until 2.11.0, when scanners were written for all four. The sentence above went on naming them for the length of that release, which is why a test now holds this paragraph against the languages the scanner actually reads.
A known shape problem: this tool is end-of-loop heavy
Worth stating plainly because it aims several roadmap items. Addy Osmani frames agent constraints as back-pressure that should exist throughout the loop, not as a single review at the very end (Agentic Code Quality). Measured against that, this tool is strong where it is cheapest to be strong — a CI gate after the work is done — and thin during the loop, where a constraint is worth far more because it prevents rather than rejects. The shipped IDE and agent skills are a partial answer.
The pre-commit half closed in 2.9.0. --staged scans what the git index will commit — the index, not the working tree, so a half-staged file is measured as it will land — against the same thresholds the gate uses, and --install-precommit-hook writes the hook. It is deliberately not a small audit: it produces no score, because a diff has no population to draw a rate from, and it applies no repository gates, writes nothing and runs no tests. On this repository it costs 0.17 seconds against the full audit's 266, and that ratio is the feature — a hook slower than the author's patience is a hook they uninstall, at which point it constrains nothing.
The in-loop half closed in 2.10.0. --check PATH answers what a budget allows for content that has not been written yet: no repository, no git, no scan, and the path names the content rather than being opened. It reports breaches and, while there is room, how much is left — which is the part a gate cannot give, because a gate only speaks once it is already too late to be cheap. That is what "prevents rather than rejects" meant.
What this section no longer claims. Both moments the work stops are now covered, and so is the moment it does not. The shape problem as Addy Osmani framed it is closed; what is left is not another door but evidence that these are used — the headroom figure in particular is justified by argument and not yet by anybody's behaviour. A field check on 2.10.0 found the figure only changes an author's decision once it is already near the line, which is worth knowing before any more is built on it.
One door is still missing and it is not a shape problem: --check is CLI-only, while product intent names chat/MCP the primary surface. A feature built for agents that an agent cannot call over MCP is a gap in delivery, not in design.
He also names the trade-off available once generation outruns verification: scale verification, slow generation, lower standards, or relax constraints in low-risk areas while tightening them elsewhere. Only the last is a real engineering answer, and it is what the policy-as-code item below is for. This is a borrowed frame, not evidence — it tells us where to look, not what is true.
The remediation hole: closed in 2.1.0 through 2.3.0
The second known shape problem, and for most of this project's life the more serious one, because it sat under the product's central claim. The bounded work order told an agent to fix exactly these findings and refactor nothing else, and nothing checked that it did. It is now checked.
Prompted by What is agentic testing?, whose argument for where the model should stop — agent at authoring, model out of CI, frozen oracle in the gate — is the architecture this tool already has. The score is a rate computed by code, not a model's opinion, so the "right by construction becomes right most of the time" failure it warns about does not apply here. Its failure modes for the repair step did.
Three checks closed it, and they shipped in the order they were worth building — the first two in 2.1.0, the third in 2.2.0:
1. Scope conformance, verified against the work order — --conformance <revspec>, --fail-on-out-of-scope. The work order names its findings and their paths, so comparing a diff against them is mechanical. A remediation diff that touches files the work order never named is reported as out of scope. One rule is not mechanical and is worth knowing: a test added for a fix stays in scope even though the work order never named the test file, because demanding otherwise would make the check punish the behaviour the product asks for. _conformance.py.
2. A suppression is a finding, not a fix — the same check's second, separate verdict. A finding could be made to disappear by deleting the code, adding # noqa, # type: ignore, eslint-disable or pragma: no cover, or skipping the test; the gate went green and nothing noticed. Encoded now: a suppression added on a path the work order named is reported whether or not the diff stayed in scope. conformant and clean are two answers, never merged into one, because a change can obey the work order and still silence a finding inside it.
3. Per-dimension score regression fails the run (2.2.0) — --fail-on-regression. --fail-on-new plus a version-3 baseline and _finding_match already recognised a finding that cleared and returned. The ratchet on the dimensions is what was missing: a change that improved one dimension while quietly regressing another passed. It has three outcomes rather than two — held, regressed, and not comparable — because two scans taken under different calibration cannot be differenced, and reporting that as "held" would be the overclaim the check exists to prevent. _ratchet.py.
The umbrella: an attestation artifact — shipped in 2.3.0
The three checks are mechanisms. What they add up to is one thing worth naming: a per-change record that an independent process can produce and a generator cannot produce about itself. --attestation-output, _attestation.py.
Reproducible and derived from one run: what was measured, what the agent was told to change, whether it stayed inside the work order, whether it silenced anything, and what moved on each dimension. A code-generating agent has an audit trail of its own actions; that is a self-report. This is the second opinion, and it is the shape a regulated reviewer asks for — the question there is never "is the tool good", it is "who checked the vendor's output, and can they re-derive the check months later".
Two words it refuses. It is not signed — nothing here holds a key, and "signed" is exactly the word a reader reaches for with an attestation, so the document says the opposite in its own text. And a question nobody ran renders as not asked, never as passed; an incomparable ratchet renders as not established. The artifact's whole value is in what it declines to claim.
This is the one place where determinism stops being an engineering preference and becomes the product. A tool that routes across models cannot re-derive last quarter's verdict; this one can, byte for byte, which is why the attestation can be evidence rather than a log line.
What is still open here: the checks read the diff's shape — which paths it touched, what it silenced, which dimensions moved. Nothing reads whether the change was correct, and nothing will; that is a claim this tool does not make and the attestation says so in its own text.
~~The pairing-rule door, recorded 2026-09-26.~~ — shipped in 3.11.0. A test added for a fix stays in scope. clean fails on a suppression directive the diff added to a file the work order named, and — since 3.11.0 — on any test file in the diff made unable to fail: a skip or suppression, an assertion that cannot fail, the assertion API replaced, a failing assertion swallowed, or assertions deleted with nothing in their place (_oracle, reported as oracle_weakened). Held to a precision bar frozen before the detector was written. Decision 13 shipped with it: --ask FILE reads a diff against an operator's ask for work this tool did not order. Freeze, a fresh review seat, and a human on the test hunk sit outside this package. See product intent and Decision 12.
Two features that only a deterministic, unmetered checker can offer
Run-over-run comparison of generated output — shipped in 2.6.0, --transformation NAME. Where an agent performs the same class of work repeatedly — a migration, a framework upgrade, a codemod applied repeatedly in one repository — nobody measures whether run seven produced better code than run six. The generator cannot answer it: its output is not reproducible and it has no memory across runs. This tool already had pinned references, structured finding identity and scan history, so the comparison was a join rather than new machinery. The value is highest exactly where volume is highest and review capacity is lowest.
What it reports is each labelled run's movement in estimate since the previous recorded scan, and how the newest run's movement compares with the one before it. Three things it deliberately does not do. It never says one run was "better" — two runs of one codemod land on different code, so a larger movement can come from the tree as easily as from the run, and "moved further" is what the numbers support. It attributes nothing to the transformation: the label is the operator's claim about what happened in an interval, and everything else that happened in that interval is inside the number. And it refuses across an instrument change, naming how many runs it refused rather than dropping them silently — a reader who cannot see a refusal concludes there was no history.
The work not sent — decided against as a feature (2026-09-04); it is one sentence in the README instead. The idea: every finding this tool produces without a model call, and every diff its bounded work order keeps from becoming an open-ended rewrite, is work that never reaches a metered agent. Under per-action pricing that is a number with a currency attached, and the tool that produces it is the one with no incentive to inflate it.
It was dropped on inspection, and the reason is worth keeping. Every quantity it would report — files examined, declarations examined, findings produced, bounded work-item count — is already in every report. The feature added no measurement; it added a label. And the label is where it goes wrong: "work not sent" is heard as "money saved", and converting it to money needs a price per token, an assumption about what an agent would have done unprompted, and an assumption about how many turns it would have taken. All three are counterfactual and none is in the repository. Anything stronger would be the economic-ladder claim ADR 004 has not earned — and ADR 004, which ships, prices human labour against recurrence and churn, which is a different question from agent metering.
What survives is the true part, stated where a reader meets it: the report says what it examined and produced, and all of it is computed with no model call.
What this tool does not compete on
Named because the failure mode for a one-maintainer project is competing everywhere and winning nowhere. Code generation. Security scanning and compliance profiles. Comprehension of languages it does not parse. IDE experience. Model routing. Model-judged code review — AI pull-request reviewers such as Cursor's Bugbot find logic bugs this tool never looks for, and their findings are worth most when something deterministic reproduces them (why). Tools that do those things are the customer for what this produces, not the competition — and the better a generator gets, the more an independent check on its output is worth.
Related, and named separately because it is a detector rather than a gate: tests that assert implementation details instead of behaviour is listed under Additional detectors below. It is the direct answer to the review problem the same article raises — generated tests that look like yours and slide through review — and it is aspirational today.
Then
Additional analyzer adapters — Semgrep, pytest/coverage, and SonarQube export. Ingest output, preserve provenance, and do not pretend every analyzer has the same semantics. The fourteen shipped adapters — twelve native plus the declared pylint and mypy integrations — are listed in analyzer pool.
Deterministic semantic policy — prototype ADR 003 in TypeScript first: compiler-backed facts, explicit repository policies, and non-gating design-review candidates. Measure precision before adding any hard gate, and do not add a score weight during discovery.
Economic context and impact scenarios — prototype ADR 004 as an optional, separately labeled prioritization layer. Prefer repository-derived exposure data, ask only for business context the repository cannot contain, and keep all scenarios out of the standard grade.
Additional detectors. Worth building because the cost they name is real, and — per philosophy — worth detecting regardless of who wrote the code: docstring and signature drift, tests asserting private names, single-use abstractions, speculative generality, stale generated comments. Each becomes scored only by being given weight in the rubric.
Policy-as-code — new-code thresholds, changed-file thresholds, required tests for changed API files, architecture boundary rules by path.
Per-repository rubric overrides. Currently refused: the rubric is a standard, and a standard everyone edits stops being one. Any override mechanism must label its output a house variant so it cannot be compared to a standard score.
Delivery — GitLab and Azure DevOps adapters. The GitHub Action (action.yml), the PR-comment body (--comment-output) and historical trend reporting all ship; this line listed them as future work long after they landed, and an external evaluation of the tool got the CI story right by reading the README despite this roadmap. A document that under-claims is the same defect as one that over-claims — it just fails in the direction that costs adoption instead of credibility.
Claims without warrant — the family ADR 014 belongs to
Not built, not scheduled, and written down while the specimens are fresh. ADR 014 detects one class: code that reads an absent, empty or failed measurement as a passing result. That class is an instance of something larger, and the larger thing is the more interesting bet.
The thesis. Human code under-documents. Generated code over-asserts. A model produces the artifact and the claim about the artifact in the same breath — docstring, comment, test, config key, identifier — with identical confidence whether or not the thing behind the claim exists. Nothing in the ordinary toolchain checks that seam, because before code was generated at scale the seam was mostly trustworthy. Linters check shape, type checkers check types, SAST checks exploitability, coverage checks whether a line ran. None of them ask whether the code's own claims about itself are true.
Every specimen below is real and from this repository. That is the point: they are not hypothesised categories, they are defects this project shipped, and the register entry for each is the evidence.
1. The citation that names nothing
D152: three docstrings named
opted_in_command as the reader that made keeping a repository-supplied
key safe. The function did not exist. A reader checking the design
found the right rule stated, and a function name — and a function name
in a docstring is indistinguishable from a function.
Detection: resolve backticked identifiers, name() forms and module
paths found in docstrings and comments against the tree. Cheap, high
precision, and the purest signature in the family. The strongest
candidate here, stronger than ADR 014.
2. The comment that claims a mechanism the code lacks
D155: _SEQUENCE_FIELDS carried the
comment "derived from the dataclass rather than listed by hand so a new
one is handled the day it is added" and was a hand-written list of five
names. It cost precisely what it promised to prevent — the sixth field
was added and missed.
Detection: comments asserting derivation, enforcement, exhaustiveness or completeness adjacent to a literal collection. Narrower precision than 1, still real.
3. The test that defends nothing
A test added with a fix that passes without the fix. This project
already solved it for itself — tools/prove_falsifiers.py proves each
newly cited test fails at the base commit — and that tool is internal.
Generalised to audit a target repository's suite it is a capability
with no equivalent on the market: generated tests assert what the code
already does, which reads as coverage and is decoration.
Arguably a larger product than ADR 014.
Decided 2026-09-26: it stays this repository's gate. It does not
become a first-order step in the product loop. Customer-facing
generalization — new tests on a remediating revspec must fail on the
left side — remains unscheduled, still needs a labelled external corpus
and a frozen precision bar, and still must not license a claim that the
tests would catch a regression. The shipped MA increment on this seam
is the missing clean cases, not this prover. Decision 12.
4. The name that lies
The comparability field called rubric_version holds the package
version, so a report told its reader the rubric had moved when a patch
release was all that happened — on 38 of this repository's 49 series
breaks. Generated names describe what a thing is for rather than what
it holds.
Deliberately uncited: this one was never filed. It was corrected in
51e2060 by mapping the stored name to honest prose at the point the
report makes the claim, and the field keeps its name because renaming it
migrates every written line. The first draft of this section cited D155
for it — an entry that does not mention it — which is category 1
committed inside the paragraph describing category 1, caught by
resolving the citation instead of trusting it.
Detection: hardest of the five. Possible only in narrow shapes — a
*_version field whose sole writer is a differently-named version
constant.
5. Configuration that cannot fire
D156: **/dir/ matched nothing, at
any depth, while the bare dir/ matched at all of them. A config
written in the dialect the author had seen most rather than the one the
tool implements. An inert rule has no symptom of its own — it never
errors, and the only evidence is findings in files somebody believed
were excluded.
Detection: by execution rather than inspection — run each configured pattern against the tree and report those matching nothing. A liveness sweep, and it generalises well past exclude patterns.
Why this is not scheduled
The same reason ADR 014 is not: precision is unmeasured, and this repository is the wrong corpus to measure it on. Everything above was found here, which means this codebase has been scrubbed of the class and would score clean — the one tree available for free is the one that cannot validate the detector.
Building any of it needs a labelled external corpus and a frozen acceptance bar, the way ADR 003 and ADR 014 both require. Until there is a reason to build that corpus, this section is a record of specimens rather than a plan.
Distant future
Cross-repository transformation comparison — not built, low priority. --transformation (2.6.0) compares runs within one repository. The fleet version — many repositories' histories joined on one label, to report how a single codemod fared across an org's services — is wanted eventually and is not scheduled.
The reason it is low priority is not appetite, it is that the hard part is not the join. COMPARABILITY_FIELDS names what makes two scans differenceable: rubric_version, calibration, thresholds_digest, analyzers, scored_languages, scope. Two scans differing on any of them came from different instruments, and a trend across them measures the instrument rather than the code. Across repositories every one of those differs by default — each repository has its own thresholds file, its own installed analyzer set, its own language mix. So the feature is not "join the histories"; it is prove a fleet shares one instrument and refuse the members that do not, reusing segments. A version that skipped that proof would report an org's variation in configuration as variation in its code, confidently.
The honest first step is smaller than the feature and worth building alone: export a history, then compare exported histories that declare the same instrument. Nothing collects them today — a history lives at .maintainability/history.jsonl inside each repository, and this is a local stdio process with no fleet surface.
Two clarifications, because this item has been misread twice. The corpus is a calibration instrument, not a usage pattern: the repositories in it exist to fit the scale, and their number says nothing about how many a user points this at. And the resolved debt entry "nothing compares two runs of the same transformation" closed a single-repository gap — it is not the first half of a larger cross-repository item.
Deterministic adversarial-properties detection. The second play behind ADR 013: a detection dimension that audits a target repository for the hardening classes this agent enforces on itself — a write that follows a symlinked route, an empty tool run read as clean, a caller argument reaching a filesystem or shell sink unvalidated, absence read as a pass. Deterministic and AST/pattern-derivable, so it fits the suite rather than the prompt seam; distinct from ADR 013's emitter. The scope decision it was waiting on is made: ADR 014 takes the evidence-integrity half only — code that reads an absent, empty or failed measurement as a passing result — and leaves every exploitability class to secure-code-agent. Two of the four classes named above are therefore not MA's: the symlinked write and the unvalidated sink belong to the delegate. Non-gating and non-scoring, and nothing is built until a precision bar frozen before any result is met. A future release, not a next step.
Studies that would earn a claim
Listed with the bar each must clear, because the project has retracted one empirical claim already:
- Outcome tuning. Score a repository at a past commit, measure the following year's fix churn on held-out repositories. This is the only study that could license "the score predicts anything", and until it runs, the rubric is a standard and not a predictor.
- Economic-impact validation. Pre-register a model, estimate maintenance effort from information available before the work, and compare it with held-out task effort or cost. Organization-specific calibration comes before any cross-organization claim; configured assumptions alone license only scenario language.
- Fix breadth, done properly. A diff-content fix detector, a registered primary outcome, and commit-level authorship. The current result is a consistent direction across three specifications that straddle p = 0.05 and fails Holm correction — exploratory, and labeled so.
- Bounded-prompt effectiveness. The pre-registered experiment returned INCONCLUSIVE: far more findings closed, no measurable narrowing. A better-powered design with more subjects would settle it.
Non-goals
Replace SonarQube, Semgrep, Qlty, Code Climate, ESLint, Ruff, Radon or language-native tooling. Automatically rewrite repositories. Send code to an LLM by default. Treat maintainability as purely numeric. Claim to detect AI authorship — tested, retracted, and not revivable without a pre-registered design.