maintainability-agent docs
Project

Changelog

All notable changes to Maintainability Agent will be documented here.

How to read this file. Entries are written for someone deciding whether a change affects them, so each one states the behaviour that moved before it explains why. Most are long because this project fixes a class rather than an instance, and the reasoning is the evidence that the class is closed — it is a track record, not an apology. If you want the short version of where the product stands, read the README; if you want what is coming, the roadmap. Neither requires this file.

Unreleased

5.1.2 - 2026-10-30

5.1.1 - 2026-10-30

5.1.0 - 2026-10-27

New, reported only: test honesty, after Ken Pugh's AlignThree Spectables (Decision 19, #313). No score, grade, gate or calibration constant moves; every published number is what 5.0.1 printed. The report gains a test_honesty block, and the Markdown and HTML reports and the remediation prompt show it:

The README's acknowledgements credit Ken Pugh, and this feature's own specification is written as a Spectables file at docs/specs/test-honesty.spectable.

5.0.1 - 2026-10-27

5.0.0 - 2026-10-26

Grades move: this is a recalibration. The analyzer pool was re-pinned and every corpus repository re-measured on it. On the 201 repositories, 2 change grade — go C→B, manim B→C — and 14 move at the published decimal. The grade counts are unchanged and the corpus median still scores exactly 4.0. If you track a grade over time, expect a step at this release; the trend already reports it as a break, because the calibration constant changed.

Changed — the analyzer pool is re-pinned (#296)

4.4.6 - 2026-10-26

An audit no longer leaves temp directories behind. No published number moves.

4.4.5 - 2026-10-26

The audit of main runs again, and a range git cannot resolve is refused by name. No published number moves.

4.4.4 - 2026-10-26

A production file whose name starts with test pairs with its test. Testability can rise for a repository that holds one.

4.4.3 - 2026-10-15

--update puts every MCP server on the new version itself. No published number moves.

4.4.2 - 2026-10-15

--update keeps the install current without ever moving it backwards. No published number moves.

4.4.1 - 2026-10-14

An audit can no longer rewrite the repository it audits. No published number moves.

4.4.0 - 2026-10-12

The analyzers your own pipeline already runs can now close a coverage gap, and a report says when an analyzer's findings do not move the grade. No published number moves.

Added — your pipeline's SARIF, read and never run (Decision 17)

Fixed — findings the grade does not use looked ignored (D228)

4.3.1 - 2026-10-09

This public repository no longer names its owner's private repositories, and a check now stops it happening again. Nothing an audit measures or reports changes.

Fixed — private repositories were named in a public one (D227)

Fixed — every CI job tests against the pinned lizard

4.3.0 - 2026-10-04

Code a change touches can be held to a stricter bar, and three analyzers that silently contributed nothing now run. No published number moves for a repository that sets no policy.

Fixed — three analyzers were lost to every audit (D226)

Added — code a change touches meets a stricter bar (Decision 15)

4.2.0 - 2026-10-03

A committed docs site no longer counts twice. Every repository is meant to commit an HTML site rendered from its Markdown, and the audit scored it as source on the default configuration: duplicate blocks doubled and debt markers were reported again in pages nobody may edit. No published number moves for a repository without one.

Added

Fixed

4.1.0 - 2026-10-03

No published number moves. The analyzers now read every file of a large repository, and the whole calibration corpus was re-measured to prove what that changes: ten of the 201 repositories read differently, every scoring constant came out identical to four decimals, and no repository's score moved at the published decimal or changed grade. Re-measuring took 61 minutes with --jobs 12, against about five hours one at a time.

Fixed

4.0.1 - 2026-10-02

Security — a repository could run code on the auditing machine (D222)

Fixed

4.0.0 - 2026-10-02

Grades move: this is a recalibration. Kotlin and Shell are anchored, so the reference every score is a multiple of is now drawn from 201 mature repositories in fifteen languages rather than 180 in thirteen. On the 180 repositories in both corpora, 9 change grade — 8 B→C and 1 C→B — and 52 move at the published decimal. The corpus median still scores exactly 4.0. If you track a grade over time, expect a step at this release; the trend already reports it as a break, because the calibration constant changed.

Changed — Kotlin and Shell are in the reference corpus

Fixed

3.12.0 - 2026-10-01

Added — missing security scanners are named before the run

Fixed

3.11.1 - 2026-09-28

Fixed — conformance and coverage edges an audit of 3.11.0 found (D218)

3.11.0 - 2026-09-27

The condition behind the first addition — a deterministic check is only evidence when the work under review could not author the record — is credited to Michael Eakins, who raised it in a public discussion of this project. Thank you, Michael.

Added — clean sees a weakened test (Decision 12)

--conformance said a diff was clean when it stayed in scope and added no suppression to a file the work order named. The test file the pairing rule lets in beside a fix could be skipped, aliased, emptied or made tautological, and clean stayed true while the suite went green. Now any test file in the diff made unable to fail — a skip or suppression, an assertion that cannot fail (assert True, expect(x).toBe(x)), the assertion API replaced (assertEqual = …, expect.extend over a built-in matcher, jest.mock('expect')), a failing assertion swallowed, or assertions deleted with nothing in their place — fails clean and is listed under oracle_weakened. The precision bar was frozen before the detector was written: zero false alarms on a labelled set of legitimate test edits, every labelled weakening caught. Still shape: whether an assertion expects the right value is not something this checks.

Added — --ask: read a diff against an ask this tool did not write (Decision 13)

--conformance main...HEAD --ask FILE reads the diff against the paths and globs the task was asked to change instead of the work order, for work this tool did not order. Every change outside the ask is out of scope, with production and test changes counted apart — so "every frozen test passed and the diff touched code nobody asked for" is something the record can say. The file is read from your path, never from the audited tree, and --ask without --conformance is refused.

Added — coverage from lcov

A suite that writes coverage/lcov.info or lcov.info — the JavaScript convention (c8, nyc, jest, vitest) — now scores test_effectiveness as coverage.xml does, under the same rule that only an artifact this run wrote is read.

3.10.0 - 2026-09-27

Fixed — a chosen report is saved, and chat gets the summary (D216)

On the chat door, choosing html returned the whole HTML report inline — on a real repository far over what a host accepts, so the host refused it and no report was delivered. The report is now saved to the location you choose when asked where to save (output_path), and chat gets the bounded view and the file's path. Choosing markdown or html without a location is refused before the audit runs, so your host asks. A saved HTML report is marked generated and is not audited as your code on the next run. The whole chat reply now has a size budget, tested for every format on a large repository.

Fixed — Shell reading (D217)

$((1 << n)) and (( y = x << 2 )) were read as heredocs, which hid every function after them from the audit. They are arithmetic now. A keyword after do, then or else in an argument list (echo do for it) no longer counts as a branch. The Shell page names every line shape that still over-counts.

Consent given while setting up one repository ran that repository's test command in every repository audited afterwards: auditing a private application executed this project's own pytest --cov=maintainability_audit against a JavaScript tree. Consent is now recorded against the repository it was given for, and a repository with none of its own runs nothing and says so. A new repository keeps your other answers and is asked only its own test command, on the chat door and at the terminal alike, and clearing the command declines for that repository only. If you opted in before 3.10.0, you will be asked your test command once per repository: a consent recorded before this is not run anywhere, because which repository it was given for is not known.

Fixed — behaviour suites are test code (D215)

acceptance/ and e2e/ are read as test directories, so a repository whose behaviour tests live there no longer has them graded as production code and its handlers reported untested. cypress/, playwright/, integration/ and features/ are not: each is production code somewhere, and one is a calibration-corpus repository. The two directory lists now share one, with testing/ kept as a declared discovery-only name until the next recalibration.

3.9.0 - 2026-09-24

Added — Shell is parsed

The sixteenth language with a scanner of its own, and the second of the five the roadmap schedules. .sh, .bash and .zsh produce a declaration population: functions in the POSIX name() form and bash's function name forms, whose body may open on the next line and may be a subshell ( … ).

Parsed by default, and that changes something for most users. Shell is in nearly every repository, so their scripts now join the graded population, and a grade can move because a repository's scripts were read for the first time. Shell is not in the calibration corpus, so every report says the grade is provisional for it, beside COBOL and Kotlin. The report's list of concerns an independent analyzer examined also narrows for any repository with scored scripts, because only jscpd reads shell: that list is the intersection over scored languages, and it is disclosure only — the grade does not read it. Each language's own coverage is still reported separately.

Three readings are shell's own:

A masker of its own. # starts a comment only at the start of a word, so $#, ${#list[@]} and ${path#prefix} are code. // and /* are not comments, so rm -rf "$dir"/* does not blank the rest of the line. Heredoc bodies are text, <<- terminators may be tab-indented, and a double-quoted string may span lines. Quote characters survive masking, so a case arm written "start"|"stop") is still recognisable as one.

A keyword counts only in command position. Shell scripts are full of unquoted prose, and echo waiting for the server has a for in it that is an argument. if, elif, while, until, for and select count at the start of a command; case counts at its arms, not its header, and *) is the default and not counted; && and || each count, because cmd || exit 1 is the guard clause scripts are built from.

Nesting is read from fi, done and esac. The brace reader would score a deeply nested script as flat. A construct written on one line opens and closes there and leaks no depth.

No second implementation reads shell complexity: lizard 1.24.0 has no shell reader and nothing adapted in the catalog measures it. The reader is checked against the grammar instead — every compound command and list operator in POSIX XCU 2.9.3–2.9.4 and the bash constructs in the Bash Reference Manual 3.2.5, one specimen each — and Shell is named beside COBOL in both the second-opinion and the external-complexity exemptions, each disclosed on the language-support page. What it misses is on its page.

The unanchored-language caveat now reads as a list — "COBOL, Kotlin and Shell" — rather than joining every name with "and", which was right only while there were two.

Maintenance — the ways a finished tool ages now open an issue

Three risks the roadmap named, each of which failed silently:

Documentation — every document in one readable HTML copy

docs/html/ now holds a reading copy of all the documentation: the root documents, all 60-odd under docs/, grouped by kind, and cards linking the standalone HTML pages already there. tools/render_docs.py produces it, adapted from another project's documentation renderer. The markdown stays the source of truth.

The set of documents is read from git rather than a list, so a new document is picked up automatically and an untracked file is never published. The output has no timestamps, so tests/test_rendered_docs.py can require the committed copy to match its markdown: a document edited without re-rendering fails the build. markdown joins the dev extra; the product never renders documentation.

Documentation — AI reviewing AI is still an opinion

The docs said a platform that generates code and grades it is producing a self-assessment, and said nothing about the more common case: a different model reviewing AI-written code, as AI pull-request reviewers such as Cursor's Bugbot do. docs/why-this-exists.md now covers it. Model review is a finder, not evidence, and a finding counts once something deterministic reproduces it. That is the loop ADR 013's hostile-audit brief already closes. docs/product-intent.md states the principle, and the roadmap lists model-judged code review among what this tool does not compete on, as a complement.

Fixed — a release cannot ship over one that never reached PyPI

3.7.2 was tagged and its release build failed — the suite ran without secure-code-agent installed — so it was never published. 3.7.3 fixed the build (D177) and shipped over the gap, and nothing recorded that a version was missing. The tag check added in 3.8.1 would not have caught it: the tag existed.

The release build now runs tools/check_previous_release_published.py before publishing, which asks PyPI whether the previous version is there and refuses otherwise — including when PyPI cannot be reached, because a check that passes when it cannot run is not a check. Run against the real index, it refuses 3.7.3 by name.

docs/release-plan.md now lists every version with a changelog section and no package — 0.3.0–0.6.0, 2.11.0, 3.7.2, 3.7.12 and 3.7.20–3.7.30 — with what happened and which release carried its changes, and says plainly that none is to be republished.

Documentation — the documents that describe today caught up with Kotlin

3.8.0 updated the README, the language-support page and the roadmap's schedule, and left six statements about the present describing the tree before it: the help index still said "the fourteen parsed languages", the architecture document's list of what DECLARATION_SUFFIXES gates stopped at Ruby, the roadmap's Shipped entry counted fourteen, its paragraph on unwritten languages said Kotlin had no declaration population, and its Sonar comparison counted nine free languages of fourteen — Sonar's Community Build covers Kotlin too, so it is ten of fifteen. Two code comments carried the old count.

test_current_state_docs_name_every_parsed_language now holds the help index's count to the one the report derives, and the architecture list to the language-support table, so the next language cannot leave them behind.

3.8.1 - 2026-09-23

Fixed — a merged version cannot sit untagged under the next one

3.7.20 through 3.7.30 merged over two days and none was tagged, so none reached PyPI. Among them was D211, the fix that stops an audited tree redirecting a read out of itself — merged, reviewed, and delivered to nobody who installed the package. 3.8.0 was the first release after 3.7.19 and carried all eleven.

Nothing noticed because the only check on tagging accepts the release plan's "Last tagged version" row if it names the newest tag or the version the tree is about to ship, and tools/stamp_version.py writes the second on every bump. The row always satisfied the check whether or not anything was tagged.

test_the_previous_release_was_tagged now requires the release before the one a tree ships to carry its tag, which stops a backlog on its first step: 3.7.21 could not have merged while 3.7.20 sat untagged. It deliberately does not look further back — forty-seven releases between 1.1.0 and 3.7.5 are on PyPI with no tag here, because the 2026-09-16 history rewrite left their tags pointing at commits that no longer exist. Those were removed on purpose and must not be re-created; a re-pushed tag would republish an old version over a newer one.

It fails rather than skips on a checkout with no tags, so the release build now fetches full history like every quality-gate job already did. This release is the first to run through both.

Fixed — pytest --cov could hide a failing test behind its own pass count

The test-execution tests run a repository's suite inside a temporary tree. Under pytest --cov that child wrote coverage data too, reading its settings from its own working directory — which has none, so it recorded statements while this repository records branches. combine then aborted with DataError after pytest had already printed "N passed", and the internal error consumed the failure summary. One run on 2026-09-23 reported a failure whose name was never recovered.

The suite now points every child at this repository's coverage settings. Nothing changes in CI, where the two already agreed; a local coverage run completes and reports its failures.

3.8.0 - 2026-09-23

Added — Kotlin is parsed

Kotlin is the fifteenth language with a scanner of its own, the largest real gap left on the roadmap, and the first of the five languages scheduled there one per minor release. .kt and .kts produce a declaration population: functions, constructors, init blocks, and the types — class, interface, object, and the data/sealed/enum/annotation/value/inner forms, which Kotlin writes as modifiers on class rather than as keywords of their own.

Three readings are Kotlin's own, and each is a place a shared default reached the wrong answer:

An expression body has no brace. fun area() = w * h is idiomatic Kotlin, not a corner. The shared end-finder bounds a body by its braces and falls back to indentation, and that fallback walks to the closing } of the enclosing class — a one-line function came back two lines long, with the brace ending its container counted as its last line. The shared bodyless check looks for =>, the arrow JavaScript and C# use for an expression member; Kotlin writes =, so every expression-bodied function in a tree would have been dropped as a signature with nothing behind it. Kotlin supplies its own end-finder and answers the bodyless question itself.

when is counted at its arms, the rule already shared with switch in Go, PHP, Ruby, Python, Swift and Fortran. Kotlin spells an arm ->, which is also how it spells a lambda's parameter list and a function type, so both are consumed before the arms are counted. when was in none of the cognitive vocabularies either, so a dispatch written the idiomatic way read as cognitively flat while the same dispatch written if/else if was charged in full — the Fortran do, Swift guard and C# foreach defect in a fifth language.

Kotlin has no ternary, because if is an expression there. The C-family ?…: alternative is therefore absent rather than merely unused: with it, fun find(id: Int?, name: String?) scores two, which is D115's defect in a sixth spelling after C#, TypeScript, PHP, Java and Swift. ?: (elvis) is counted as Kotlin's ??; ?. is not, because it yields null and carries on down the same path.

An extension function needs no second pass: fun Widget.draw() writes its own receiver, which is the one place Kotlin is easier than Swift.

Fixed — lizard reads Kotlin, and the catalog said it did not

The analyzer catalog's lizard row listed sixteen languages and Kotlin was not among them, while lizard 1.24.0 reads a .kt file without being asked to. This is the Fortran stale-row problem exactly: selection is gated on that list, so Kotlin would have come out not-applicable on every repository while the tool that measures it sat installed in the pool.

Two test fixtures carried the same wrong claim in prose — that no tool in the analyzer catalog measures Kotlin's complexity, offered as the reason Kotlin was a durable stand-in for an unparsed language. It was never checked against the tool. Both fixtures now use Elixir, on the narrower and checkable ground that the roadmap lists it under languages no scanner is scheduled for.

Changed — Kotlin is disclosed as unanchored

Kotlin is parsed but absent from the 180-repository reference corpus, so every grade skin now discloses that a Kotlin repository's grade is provisional — the caveat COBOL already carries. The two are there for different reasons and with different ends: COBOL is excluded from the corpus permanently, and Kotlin anchors at the next recalibration. The scanner shipped ahead of the corpus, and saying so is the whole job of that disclosure.

3.7.30 - 2026-09-22

Documentation — the hostile-audit brief is recorded as an unfinished feature (D202)

D202 described the --hostile-prompt-output brief appearing in no user-facing document and reaching no report field, and was filed Open pending a decision on what it should become. That decision was made — it is an unfinished feature rather than a defect — and the entry now records it as a decision closure with its decider and date, rather than as a gap someone is meant to close.

Nothing about the emitter changed. It remains deterministic, seeded from a real run, and connected to no scoring path.

3.7.29 - 2026-09-22

Fixed — the labor-rate questions accept the rate you type (D213)

The three rates published options: [90], [140], [210] — a one-item enumeration — and meant it as a type marker. The elicitation door reads a non-string option list as "any number", so that path worked. A host reading the published questions as data reads options as what the word says and offers the single value, so entering a real rate meant contradicting the contract the tool had just published.

The questions now carry type: "number", and default is a suggestion rather than the only permitted answer. Both readers ask the question what it is instead of inferring it from the shape of its options.

Menus are unchanged and still refuse what they do not offer.

3.7.28 - 2026-09-22

Fixed — C#'s foreach is counted as the loop it is (D212)

\bfor\b does not match foreach: the word boundary falls after for. C#'s primary loop therefore cost nothing at all. Two methods doing the same work scored foreach at complexity 1 and cognitive 0 against a counted for at 2 and 2 — the first contains a loop, and both metrics read it as straight-line code.

If you audit C#, your complexity and cognitive numbers will rise, and the new numbers are the ones that count the loops that were always there. Any threshold tuned against the old readings may need revisiting.

This is the fourth time a language's own keyword was missing from its pattern — Fortran's do, Swift's guard and repeat, PHP's elseif and foreach came before it, each fixed in one language. The check is now over every parsed language, and a language with no loop fixture fails rather than being skipped.

lizard, the independent implementation this project checks itself against, does not count foreach. The divergence is declared with its reasoning: the C# specification defines foreach (V v in x) by its expansion to a while (e.MoveNext()) loop, so the condition is in the language's own definition of the statement.

3.7.27 - 2026-09-21

Security — the audited tree cannot redirect a read out of itself (D211)

A configured path has always been bounded: resolved, refused if it lands outside the root, then walked component by component so an inward symlink cannot redirect it either. Three readers took their paths from the audited tree and never went through that boundary — practice detection, test-command detection and the generated-file banner. They were checked for what a path is, never for where it goes.

A repository whose pyproject.toml was a symlink to a file outside its root scored practice level 2 with a linter-config signal, where the same tree with an ordinary in-root file scored 1. The audited tree could raise its own reported level using a file this tool was never pointed at, and practice level feeds the pillar posture.

refuse_symlinked_route moved down to the module the reading primitive lives in, and practice and test-command detection now read through a door that applies both the containment check and the route walk. Either alone leaks: the route catches an inward link that resolves back inside, the containment check catches an absolute target the walk never meets.

The operator-named door is unchanged and still does not check: an operator names their own configuration and a symlinked config is an ordinary setup. What changed is the reads where the audited repository chose the path.

3.7.26 - 2026-09-21

Fixed — an unmeasured pillar reports nothing on either axis (D210)

Practice level is a fact about the repository, and the pillar section states it once above the table. Every row then repeated that one number, including the rows for pillars this tool does not measure — so the efficiency row read practice 4 for a pillar ADR 007 declares out of scope, and an unmeasured security row said the same, each beside a dash in the Condition column that said it had no reading.

posture had been nulled for exactly this reason when an earlier version printed "efficiency — healthy" from the practice axis alone; its input was left behind. Both axes are now absent for a pillar with no reading of its own.

A delegated pillar that came back with a document keeps its number, and it is a different one: secure-code-agent measures security practice and reports its own level.

3.7.25 - 2026-09-21

Fixed — a clone names every copy, and is not told to be removed (D209)

A duplicate block is a statement about two or more locations. The work order carries one path, so the item read "duplicated block in billing.py" with the target "remove the duplicated block" — and an agent handed that deletes one copy, which clears nothing and breaks the file it was deleted from.

Items for findings that occupy several places now carry all of them, and the target says what resolving it means: "extract the shared block so its 2 copies become one; editing one copy alone leaves the rest". Near-duplicates get the same treatment from the other side — their item never named what they nearly duplicate, while the near-duplicate section in the same prompt already did.

Both surfaces that print a location — the bounded prompt and the per-item copy-paste block that travels alone into an agent — read one shared helper, so one of them cannot be fixed without the other.

"Remove" is the verb D165 corrected for risk patterns, where it read as "delete the matched text" and cleared a finding without resolving it. Same verb, one class over.

3.7.24 - 2026-09-21

Changed — who gets handed what is its own module (D208)

No behaviour changed. _work_order.py had reached 745 lines against this repository's own 750-line gate, and the next fix needed ten more. The whole question of which work-order items an agent is handed — withheld_reason and the two clauses it applies, the bounded paste, the conformance targets — moved to _handoff. _work_order is 491 lines and decides what findings are worth and how they are ordered.

The boundary was already written down and enforced by nobody: architecture.md said presentation may import "_work_order's presentation-facing prompt selection", naming a module that did not exist. It exists now, so the rule is a module name that test_architecture holds rather than a sentence.

3.7.23 - 2026-09-21

Fixed — the prompt no longer lists what it just withheld (D207)

One prompt said two things. It printed "Not in scope for this change: N finding(s) need a design decision before code moves … Do not attempt them here", and that escalated findings were deliberately excluded — and then listed those same targets under "to inspect first", "to verify" and "to inspect".

withheld_reason is one rule with two clauses: a Major Project, and a finding fixed before that came back. The focus lists honoured only the second, and only on three of their seven categories — hotspots, large files and risk findings. Duplicate blocks, near-duplicates, dead code and competing libraries were filtered by nothing at all.

Every category now reads the same rule through the same locator, which also gained the competing-libraries shape so that finding class can be placed at all. An identified finding is matched by identity and an unidentified one by location, and the two are not interchangeable — a location is coarser than the identity it stands in for, and mixing them lets one class's withheld finding suppress another class's listed one.

A run that withholds nothing produces the same focus lists it always did.

3.7.22 - 2026-09-21

Fixed — a pillar's condition says what produced it (D206)

A pillar's condition is the mean of the aspects that carried a number, which is the only honest arithmetic — a None is not a zero — and it means an unmeasured aspect is dropped rather than counted. Maintainability's seven aspects with one bad one average 4.43; withhold that one aspect and the same pillar reads 5.0, in a row that still listed all seven and said nothing. Withholding evidence improved a reported value, and the value named nothing that produced it.

The overall grade was never exposed to this — it is banded from the evidence floor, where unknowns price at 0 — and flooring a pillar's condition too was rejected, because a delegated pillar's condition arrives already computed by the tool that owns it and the two would stop being comparable in the same column. So every pillar entry now carries condition_coverage and no skin prints a partial condition without saying how many of how many aspects made it and which are missing. "Could not measure" and "nothing to measure" are named separately, because collapsing them is how an unknown reads as clean.

Added — score.not_applicable (D206)

Which Nones in the aspect layer are a resolved absence rather than missing evidence. The scorer has always drawn that line for the grade and the report kept only the consequence, so a consumer had to infer it by subtracting the blocker text from the aspect layer. Additive and optional, like analyzer_scored_dimensions: no schema version moves and nothing reading version 2 breaks on it.

3.7.21 - 2026-09-20

Fixed — the written release process described the chore that had just been removed (D203)

3.7.17 reduced the version to one literal and 3.7.19 stamped the four documents that state it. Neither touched the instructions. release.yml's header still said to bump it in pyproject.toml, __init__.py and config.py, and CONTRIBUTING.md still said "all three places" and named config.py VERSION — so following the documented process reintroduced exactly the copies that were removed, and the suite would then refuse the result. Both now describe the process the gates actually enforce, including the stamping step, which is not optional.

Fixed — twelve releases published notes that said nothing (D204)

The GitHub Release job reads its notes from this file's ## X.Y.Z section and falls back to the sentence Release X.Y.Z. when there is none. This file stopped at 3.7.7 and twelve tags went out after it, each with that placeholder. The sections below are the backfill, and a test now fails when the shipping version has no section — so the fallback stays for a re-pointed tag rather than serving as the normal path.

Fixed — the version checks verified a proxy for what they claimed (D205)

Two checks tested something adjacent to their own claim. The release gate's comment said a tag is checked against "the literal the wheel will actually carry"; it read a file in the tree, which is the one thing that cannot say what a built artifact carries — and the build step installed the wheel, printed its version and compared it to nothing. The built wheel is now compared to the tag, and the comment says what each check does. Separately, test_only_one_module_writes_the_number searched for the current version's characters, so a module left holding VERSION = "3.7.16" after the package moved on was invisible — the check passed because the copy was stale. It now matches the shape of a version assignment.

3.7.20 - 2026-09-20

Added — json is offered wherever a presentation is chosen (D201)

--format json had been accepted on both doors for eleven releases and offered by no question. The terminal asked "chat, markdown, or html", the MCP default_format question listed the same three from a hand-written copy of the terminal's list, and the skill and server prompt were correct about a set that was wrong. It is now the fourth option at every door, with chat still what Enter selects; at a terminal it goes to stdout rather than writing a file. The four presentations read one report dictionary and none computes a score.

The option list lives in one place now, and the MCP door's format validator reads it too, so what a surface offers and what a door accepts cannot diverge again.

Known — the hostile-audit brief reaches no report and no document (D202, open, Low)

--hostile-prompt-output and the maintainability-hostile-audit prompt emit a run-derived brief for an adversarial review. It is in no README, neither SKILL.md, and nowhere in the product intent, and whatever a reviewer concludes from it reaches no report field and no history record. Nothing in it can affect a grade — no code path connects the emitter to the scorer — so it is recorded rather than held for. It may be an unfinished feature rather than a defect.

3.7.19 - 2026-09-20

Changed — pydocstyle's imperative-mood rule is off, with the reasoning recorded (D200)

D401 was 330 of 566 findings, each asking for """Returns the score.""" to become """Return the score.""". The docstrings here are deliberately narrative and most carry a defect's reasoning where the code that caused it lives; complying would flatten the sentences this project explains itself with, and a docstring's mood says nothing about whether code survives change. Turned off in configuration with the reason beside it rather than as 330 expiring suppressions. Every other rule stays on. Analyzer findings fell 832 to 502 and the documentation aspect did not move.

Added — the release version is stamped into the documents that state it (D200)

Four documents have to name the version and each went stale once: the README's version line, the README's GitHub Action pin (which is what a reader actually installs), the release plan's row and the SECURITY.md support table. tools/stamp_version.py writes all four from the single literal and exits non-zero if any pattern stops matching, rather than stamping three and reporting success — which is how v3.7.9 was tagged and lost its release. It does not improve change_coupling and says so: those files still move together because they must.

3.7.18 - 2026-09-20

Fixed — every skin says what examined test effectiveness (D199)

The bounded chat view reported test effectiveness: not measurable for a suite that had run and failed, because it never rendered the suite at all — a value with no attributable source, and a failed measurement misreported as an absent one. Chat now renders the suite section the complete report and HTML already carried.

3.7.17 - 2026-09-20

Changed — the version is written once (D198)

The version lived in five files, and this repository's own audit put __init__.py and config.py at the top of its change-coupling table at 0.985 confidence. v3.7.9 had already been tagged and failed its release because four of the five were updated and the fifth was not, and the sibling project had shipped a wheel stamped with one version while every report it produced carried another. maintainability_audit.__version__ holds the literal now, config.VERSION re-exports it, and pyproject.toml reads it through [tool.setuptools.dynamic].

The release gate had been reading pyproject's static version key, which this change removed, so v3.7.17 failed eleven seconds in — the fix for a release-breaking duplication broke a release, because the check protecting it was reading one of the copies. The gate now reads the module that holds it.

3.7.16 - 2026-09-19

Fixed — the HTML report draws the security work order (D197)

A reader who chose HTML got the delegate's work order wrapped in <pre>: raw Markdown inside a rendered page. secure-code-agent now emits the same work order as data, and this tool draws it in the delegate's own tiers, counts and caps. Requires secure-code-agent 0.12.8 or later.

3.7.15 - 2026-09-19

Fixed — the audit names the program it ran, not only the one configured (D196)

The same configured test command produced different results through the two doors: the CLI resolved the project's pytest and measured 96% coverage, the MCP server resolved another environment's and could not run the suite at all, and neither result said which program it had executed. The resolved path is now reported beside the configured command wherever the suite is shown.

3.7.14 - 2026-09-18

Fixed — a presentation override the user did not choose is disclosed (D194)

A per-call format silently overrode the presentation chosen during setup, so an agent's preference and the user's own choice were indistinguishable in the result. The override is now disclosed.

3.7.13 - 2026-09-18

Fixed — the data path is the same setup as elicitation (D193)

setup_answers was published but unfinished. Labor rates arrived as strings and were never converted, so the economic bounds stayed pending forever; an illegal value such as depth: "nuclear" persisted the default and reported success; and three surfaces still told a host to "call again" without naming the parameter it should call with. Submissions are now checked and converted against the questions the host was actually offered.

3.7.12 - 2026-09-18

Fixed — the setup instruction describes the reply it ships with (D192)

The instruction announced that the repository had never been set up, above three labor rates that exist only because it had been; recited the presentation options when the payload contained no such question; and never named setup_answers. It is derived from the questions in the reply now.

No tag was cut for this version.

3.7.11 - 2026-09-18

Fixed — both setup paths ask the same stage, and what setup writes validates (D190, D191)

Answering the first stage through the data path handed stage one back again, because the staging decision lived only in the elicitation schema. One stage decision now feeds both renderings.

Separately, the published config schema set additionalProperties: false and described none of presentation, history, test_execution or economic_context, so a completed first-run setup produced a file the tool's own contract rejected. The gate is now the round trip: write with apply_answers, validate what lands.

3.7.10 - 2026-09-18

Fixed — a question this agent asks is one a host can answer (D189)

The tool returned its seven setup questions, the user answered all seven, and nothing was written: setup was an elicitation-resolved parameter absent from the published schema, so a host that cannot be elicited had no argument to answer with and the documented fallback could not close. A published setup_answers carries them to the same writer, and the legal names derive from the questions so an unanswerable question is not expressible.

This also closed the dead end where consent to run the test suite, with no command recorded, produced a refusal on one surface and silence on the other (D185).

3.7.9 - 2026-09-18

Added — the agent reports when it is running stale code (D188)

The MCP server answered a call with agent_version 3.7.7 hours after 3.7.8 shipped: the version was a source constant, so a stale process reports its own staleness as fact and nothing can notice. The venv beneath it was an editable install frozen at 1.8.0 carrying secure-code-agent 0.2.0 against a floor of 0.12.7, which would have refused the delegate and left the Security pillar unmeasured. The agent now compares loaded code against installed metadata and names which of the two is stale — a process that needs restarting, or an install that needs reinstalling.

3.7.8 - 2026-09-18

Fixed — the security pillar asserts a scanner set, so it can be graded (D184)

The delegate config asserted no scanner set, so the security pillar reported unverified on complete coverage and could not be graded at all. gates.require_scanners now names the scanners the gate job installs.

Fixed — this tool's own capabilities are declared rather than scored against it (D186)

The security grade was held at B+ by 68 true, permanent reports about what this tool is: spawning analyzer subprocesses, reading analyzer XML, calling the SonarCloud API, seeding a calibration bootstrap. The delegate config now declares those four capabilities with a stated reason each, and the findings are reported on their own axes rather than scored, with the undeclared score disclosed beside the declared one.

Fixed — code scanning gets the filtered record, not the full one (D187)

Every required check passed and a merge was still refused, because the uploaded SARIF was the full record and code scanning had opened a blocking review thread for each of seventeen assert statements in the test files the branch added. Code scanning now receives the filtered document secure-code-agent writes for it, and the full record is kept as an artifact.

3.7.7 - 2026-09-16

Documentation and CI only; no product behaviour changes. Released so the package page carries the corrected README.

Added — the authorship check refuses an outside address or a forbidden term (D183)

Every pull request's commits must name only the repository's commit identity in their messages, merges included, so a Co-authored-by: trailer for any other address fails before it reaches history. A second step refuses terms kept in a repository secret rather than in the tree, reading identity fields, messages and every added line, and fails closed when the secret is unset. CI only; no product behaviour changes.

Documentation — synced with the build

3.7.6 - 2026-09-16

A documentation and CI release. No product behaviour changes.

Changed — the published repository and package describe the product, not how it is built

Source comments, test docstrings, the defect register, the changelog and the documentation no longer record which contributor found, wrote, tested or decided each change, and no longer quote anyone. Audits keep their commit and date, and every register entry keeps its defect, fix, closing test and mutation statement, so each is still traceable and still proven.

The source distribution on PyPI carries the test suite, so earlier releases include some of that text; this is the first release without it.

Changed — the authorship check verifies identity and signatures only

The pull-request workflow no longer requires a trailer naming the author of each commit. It still requires the repository's commit identity and a signature from a key on .github/allowed_signers, and a commit altered after signing still fails. The local commit-msg hook that required the trailer is removed.

Removed

3.7.5 - 2026-09-14

One fix. The security pillar now honours reviewed suppressions on any repository, not only this one. Scoring, ordering and policy are unchanged.

Fixed — this audit requires secure-code-agent 0.12.2, the first release that honours paths: suppressions (D182)

Before: the audit ran any secure-code-agent from 0.12.1 up. 0.12.1 matches .scignore.yaml paths: globs against absolute finding paths, and this audit always passes it an absolute root. Every reviewed paths: suppression was ignored, and its finding counted as live in the security pillar, which every skin shows since D179. 3.7.4 worked around that for this repository alone, by rewriting its own entry as file:.

Now: the supported range is >=0.12.2,<1. secure-code-agent fixed the defect in 0.12.2 (its D20): finding paths are repository-relative, and its fingerprints no longer depend on where the checkout lives. With 0.12.1 installed, the pillar states that the release is unsupported and the environment work order gives the install command, the same as for any release outside the range.

If you run the audit yourself: pip install -U 'secure-code-agent>=0.12.2'. secure-code-agent's own changelog covers what 0.12.2 changes for baselines. Existing ones keep matching.

3.7.4 - 2026-09-14

Three fixes from the audit of 3.7.3. Each one is a report that left out, or contradicted, what the same run had found. Scoring, ordering and policy are unchanged.

Fixed — the security pillar shows what secure-code-agent found, and its work order reaches you (D179)

An audit of this repository recorded 5 critical and 1 high security finding, and no surface mentioned them. The pillar row read "unverified: not graded — …". The remediation prompt that secure-code-agent wrote was deleted with the temporary directory it was written into.

Fixed — a copy-paste prompt no longer authorises what the remediation prompt withholds (D180)

The demo's remediation prompt withheld two duplicated blocks as needing a design decision. Their copy-paste blocks in the report still said "Task: remove the duplicated block." Every item the prompt withholds — a Major Project, or a finding fixed before that came back — now gets a block that asks for the design options and a stop, on the chat view and the Markdown and HTML reports. examples/demo/expected-prompt.md is regenerated.

Fixed — semantic findings respect exclusions on a full scan (D181)

Excluding tests/fixtures/ removed the fixture from every count, but a full scan still reported a design review candidate from it and named the file in the prompt. A semantic finding is now kept only when its file is one the audit read, the same rule changed-only runs already followed (D174). The semantic coverage counts are taken from the findings kept.

Repository — this project's own critical and high security findings are cleared

Once D179 made the counts visible, secure-code-agent's run on this repository showed 5 critical and 1 high. None was a leaked credential, and each is now fixed or acknowledged with a reason:

3.7.3 - 2026-09-13

3.7.2 was tagged and never published. Its release build tests the built wheel in a fresh environment, and that job never installed secure-code-agent, so the security pillar's tests read "not installed" and the build stopped before PyPI. 3.7.3 is 3.7.2 as intended: the same code, with the release job installing secure-code-agent 0.12.1 like every other job that runs the suite.

Everything described under 3.7.2 below ships in this release.

3.7.2 - 2026-09-13 (tagged, not published)

Changed — secure-code-agent is installed alongside, not a dependency, and 0.12.1 is required

3.7.0 and 3.7.1 declared secure-code-agent>=0.10,<0.11 as a dependency, which excluded the current release. The range was copied from this project's CI pin, not taken from the tool's published releases, which were at 0.12.1. Installing 3.7.x on a machine that had 0.12.1 downgraded it to 0.10.0. The dependency also put secure-code-agent's own mcp<2 pin into the same environment as this package's chat door, which needs mcp>=2.

It is no longer a package dependency. The two tools are independently releasable (secure-code-agent's D3, this project's ADR 008), so it is installed alongside, the way the analyzer pool is: pip install 'secure-code-agent>=0.12.1'. Every audit still runs it. The audit runs whichever release is installed if it falls within >=0.12.1,<1; a missing or out-of-range release is the pillar's stated reason, with the install command in the environment work order. CI installs 0.12.1, and a test keeps every CI pin inside the supported range.

0.12.1 writes the same security-pillar.json contract (schema v2), and this release reads it unchanged.

3.7.1 - 2026-09-13

Fixed — the chat door says what is installed when it cannot start (D178)

If maintainability-agent-mcp can't import mcp, the message now names the installed version, the range this server needs, and any other package that requires mcp — rather than "MCP support is not installed", which it said even when mcp was installed at an incompatible version.

If you followed 3.7.0's setup page: installing secure-code-agent[required-scanners] or [python-scanners] into this package's environment pulls in semgrep, which pins mcp<2 and replaces mcp 2.x, and the chat door stops starting. Restore it with pip install "mcp>=2,<3", remove the pip-installed semgrep, njsscan and checkov, and install semgrep and checkov from Homebrew. The setup page now says so.

3.7.0 - 2026-09-13

Reported from the ChatGPT desktop app: MA ran, and the security pillar was not in the report.

Changed — every audit runs secure-code-agent for the security pillar (D177)

Minor rather than patch: an audit now executes a second tool, installs it as a dependency, and can write one more file. Until now the security pillar was a handoff. secure-code-agent wrote security-pillar.json and MA read it if present. CI did that on every pull request; nothing else did, and the chat door never could. So the pillar was measured in CI and nowhere a person actually asked.

What the pillar reads depends on the scanners secure-code-agent can find. With none installed it runs its built-in rules and the pillar reads unverified: not graded — scanner coverage is partial. secure-code-agent --preflight . lists what to install.

Fixed — the chat view and the prompt show the pillars (D176)

The chat view printed nothing about security, measured or not, and neither did the remediation prompt, so a missing pillar looked exactly like a clean one. The chat view now carries the same pillar table as the complete report, and the prompt states the security pillar — who measured it and what it found, or why it wasn't measured.

An ungraded pillar is no longer described as "clean scan, but nothing prevents tomorrow's regression". It reads "not graded", with the reason.

3.6.3 - 2026-09-13

Fixed — a build no longer changes what an audit scores (D175)

Scores can move on a repository audited after a build, and move toward what the same commit scores on a clean checkout. Running mvn test in jsoup put its test resources in target/test-classes, and the next audit scored 29 more files — one of them failing — as jsoup's own code. The same commit audited differently before and after the build, and differently on a laptop than in CI.

Build output now leaves the scored population when the repository itself says it is not source: a path git does not track and the repository's own .gitignore excludes. It is counted in summary.generated_files, not silently dropped. Nothing is excluded by directory name, per ADR 010. Tracked files, and untracked files no rule matches, are scored as before, and a machine's global ignore file is never read.

Every file the scanner reads under a generated or vendored tree now leaves the score — its HTML, XML and JSON as well as its source. Before, only source-code files did.

3.6.2 - 2026-09-13

The hostile audit of 3.6.1 (77de21c). Four surfaces of one run told a different story from the report they belong to. No scoring, ordering, band or policy change.

Fixed — the pre-commit hook words a declaration the way the audit does (D171)

3.6.1 made a work item name the limit its kind is graded on, in the audit and the mid-edit check. A commit blocked by the hook still read currently 2876 lines; a class is graded on length alone where the audit read against the 300-line class limit. The hook now grades and words with the same thresholds. This gap was noticed while 3.6.1 was being made and not filed; the register records that.

Fixed — a withheld estimate is not attributed to anything (D172)

When no estimate was issued, the prompt still said "the maintainability estimate above uses the analyzer readings" (or "comes from the built-in detectors"). It now says no estimate was issued and that analyzer output is evidence about the code.

Fixed — "Nothing to do" means nothing to do in the prompt too (D173)

A run with no work order said Nothing to do in the chat report, while the remediation prompt — which a chat host treats as the work order — asked for "the smallest coherent patch" and listed warn-band hotspots the work order had refused. The prompt now carries the same sentence and no task, hotspot list or deliverable; the rules that bound an agent stay. A work order whose every item is withheld from the prompt (Major Projects, escalated findings) says so the same way.

Fixed — a changed-only run stays inside its change (D174)

--changed-only ran the semantic walk over the whole tree, so an audit of two commits nominated a TypeScript fixture neither touched. Semantic findings are now kept only where their source file is in the change. Full runs are unchanged.

3.6.1 - 2026-09-13

Found by running a Java repository's top work item through the chat door to verify that remediation works. Remediation worked; six things the report said about the work were not true, and one of them exposed an ordering that broke a rule the project already had. None changes scoring, bands or policy. D164–D169 correct what a report states; D170 makes the economic reorder obey ADR 007 §3, the only ordering change, and only with an economic context configured.

Fixed — a work item states its own finding, not its class's estimate of it (D164)

A declaration item now names the limit its kind is graded on. Before, a 2876-line Java class read currently 2876 lines, complexity 25 — a function's reading against no limit — with Why: "a long, branching function … extracting one is bounded, local work". It now reads currently 2876 lines against the 300-line class limit, and a function's item names lines, complexity and cognitive cost each against its own limit.

The Why no longer sizes the change. "Bounded, local work" is the class's judgment, published in standard.md beside its declared effort; printed on an item it claimed something the audit never measured, and was false for a class ten times its limit. unpaired-hotspot carried the same clause. The band is unchanged and still declared per class — standard.md now says so — so a reader sizing the work has the item's fact rather than the class's estimate.

Fixed — a risk-pattern item no longer asks for the match to be deleted (D165)

remove the configured risk pattern on a // todo read as "delete the comment", which clears the finding and the score while the debt stays. The target now names the rule and asks for what the match records: act on what the debt-marker rule matched here, or say why it stays; deleting the matched text alone hides the finding without resolving it.

The rationale said the rule was one "this project chose to enforce on itself", and the pre-commit hook said "this repository's own configuration asked". For a repository on the shipped defaults, neither is true. All three now attribute the rule to the active configuration.

Fixed — the verify command runs the interpreter that audited (D166)

Every item ended python -m maintainability_audit --root . --format json. Where no python exists it did not run; where python3 resolved to an older install it re-audited with a different release than the one that issued the item. The command is now bound to sys.executable, as D144 bound the environment work order's install commands. The demo's checked-in work order normalises the interpreter to python3 the way it already normalised the root.

tools/regen_demo_prompt.py also isolates the user configuration tier now: it had been reading the regenerating developer's own config, and a personal economic context reordered the checked-in demo.

Fixed — the COBOL caveat appears only where COBOL was scanned (D167)

Every grade skin printed "COBOL is parsed but is not in the reference corpus, so a grade reported for code in it is provisional" on every repository. The caveat now follows the run's own language census, summary.languages. A tree that contains COBOL still discloses on every skin, and a stored report without a census keeps the full disclosure.

Fixed — one report, one count of the corpus's languages (D168)

The prompt said "eight of the ten parsed languages" while the corpus note on the same report said thirteen of fourteen. Both now count the two lists the reference block publishes.

Fixed — the work order's heading states the order it is in (D169)

With an economic context configured, the exposure sort (ADR 004) replaces the band order, but the heading still said "ordered by what it costs to leave against what it costs to fix" and the prompt called its first items "the highest value for the least change". The table and the prompt led with different items and neither said why. The heading now names the sort that produced the list, and the prompt describes its own rule — severe findings first, one item per kind.

Fixed — the economic reorder no longer lifts a Fill-In above a Quick Win (D170)

The one ordering change in this release, and only with an economic context configured. The exposure sort (ADR 004) ranked the whole work order by recurrence and churn, so a Fill-In on a file that changes often could lead a Quick Win on a stable one — the nit-loop ADR 007 §3 rule 5 exists to prevent. Band is now the primary key and exposure orders items within each band. Without an economic context nothing moves. ADR 004 is amended to say so.

3.6.0 - 2026-09-12

A value-audit rerun on 3.5.0, which asked whether the first five minutes that release advertised actually work. Two of them did not.

Changed — the published starter config opens every language, not a web subset (D162)

Minor rather than patch, because the scanned population grows. A repository audited with maintainability-audit.example.json will now read files it previously skipped, so findings appear and a reported score can move. Nothing about the config format changed.

That file is what the README tells a stranger to curl and pass as --config. Its paths.include_extensions listed 10 suffixes — Python, the JS/TS family, HTML, CSS, Markdown. The shipped default opens 48. A config is a replacement, not a merge, so following the README on a Go, Java, Rust, Ruby, PHP, Swift, C#, C/C++, Fortran or COBOL tree scanned the README and little else.

Measured on a one-function Go tree, following the README command exactly:

before after
files scanned 1 (README.md) 2
.go Source Not Read applyPricing, complexity 31
work order absent one quick-win

The report named the miss — | .go | Go | 1 | — and told the reader to add the extension. The file they were told to download is what removed it.

The list is no longer hand-written. It is the shipped default's own set, so a language added to DECLARATION_SUFFIXES reaches the advertised config in the same change rather than whenever somebody remembers this file.

Changed — --prompt-output leads with the work order and issues no rates when unscored (D163)

The file the README calls the work order was a score sheet. Two halves, one entry.

render_ai_prompt opened with a preamble, seven rules and a nine-line audit summary carrying the estimate and the letter grade. The first thing to do arrived after all of it. That is the defect 3.5.0 fixed on the chat surface (D160), still standing on the file the CLI path hands a stranger. The work order now follows the rules that bound it, and everything describing the run rather than the task follows the task.

And it printed corpus multiples on a run whose overall the scorer had withheld. A multiple is a rate against a calibrated median — it is a score — so P7 governs it. On examples/demo the report said Not scored and the demo's own README said "only the rates are withheld", while the prompt said declarations at 3.9x and Start with declarations. The complete Markdown report on the same run printed no multiples. Only the advertised product file did.

It now prints none either. The grade blockers still render, because those say why no number was issued, which is what that reader needs.

The work order itself is unchanged and still names every finding. A finding is an observation, not a rate, and withholding those would leave an undersized repository with nothing at all.

3.5.0 - 2026-09-11

The first five minutes, rebuilt: - pipes the prompt into an agent, the chat reply opens with the work order instead of a metric table, and there is a demo tree that produces a real one. From an audit of what a stranger sees after cloning.

Added — - writes to stdout on every rendered output (D161)

Minor rather than patch, because a flag gains a meaning it never had.

- is the POSIX spelling for stdout. Every rendered output went straight to Path(name), so - was a filename:

maintainability-agent --config c.json --prompt-output - | claude

gave an empty pipe, a report on stdout nobody asked for, and the prompt on disk under a name a shell fights you over. Nothing errored.

That command shape is the five-minute path the audit recommends, and the flag it recommends did not work. All seven rendered outputs — --output, --prompt-output, --comment-output, --agent-instructions-output, --attestation-output, --hostile-prompt-output, --sarif-output — now stream through one seam, so a future output cannot be added with the old behaviour by accident.

The obvious fix introduces a second defect, handled here too. With no --output the report prints to stdout, so sending the prompt there as well hands a reader a prompt with a full report stapled to its front — worse than the file, because it looks like it worked. A secondary output claiming stdout now suppresses the implicit report; an explicit --output - still prints it.

--write-baseline and --html-output refuse - by name and with a reason: a baseline is read back by a later run, an HTML report is opened rather than piped. Refusing is the point — an output that accepts - and does something else is the defect being fixed.

Changed — the chat surface opens with the work order, not the grade (D160)

The behaviour that moves is what a chat reply shows first. No data changed and no format was removed; the bounded skin's sections are reordered.

The MCP chat format — the surface most people drive this tool through — built ## Summary into the shared prelude, so every reply opened with four metadata lines and a fourteen-row metric table before anything said there was work to do.

Measured on a tree with three fat modules:

before after
first line of ## Work Order 29 8
first pasteable prompt 51 36
lines before the letter grade 8 60

Hard gates still lead — a failed gate changes which item you start with. The summary, the capped-grade reasons and the score tables follow the work order as the evidence that aimed it. The complete report file is unchanged: it is read top to bottom and correctly opens with what it is.

A second half, found while measuring the first: a run with an empty backlog rendered no work-order section at all. This repository is that run — 511 files, 74 function warnings, and a chat view of 73 lines that never once mentioned a work order. A reader could not tell nothing met the bands from this tool does not produce work orders. It now says which.

Added — a demo tree that produces a real work order

examples/demo is a two-module order system with problems a reviewer would raise: a 72-line pricing function at complexity 27, the only money path with no test, and a pricing block duplicated between billing and invoicing so the invoice and the charge can disagree. Four items, three finding classes, a copy-paste prompt each, and it reads in under a minute.

The score is deliberately absent and the demo's README explains why: two files is below the calibration floor of 24, so no rate is issued. That is the posture in one output — the work order is first class and the score is second.

expected-prompt.md is checked in and a test regenerates and diffs it, so a renderer change shows its blast radius on real output rather than on a fixture nobody reads.

Added — the README names the analyzer pool, and says what you get without it

A first run with no analyzer installed falls back to the built-in tier, and the install path never said so — so the fallback read as the product rather than as a degraded reading of it. That is the commonest reason a first run looks thin.

The stanza names the exact set CI installs and calibrates against, and test_the_readme_names_the_analyzer_pool_ci_installs derives the expected set from the catalog and fails if the README drifts from it.

Fixed — the trend break named the wrong thing

The break reason read "a delegated pillar's producer or its version changed", while the key for a schema-2 document is scoring_model and the version is only the v1 fallback. A reader whose series broke went looking for a release change that in the common case had not happened.

Confirmed against the delegate rather than assumed: secure-code-agent 0.11.0 is a docs-only release with its scoring model unchanged, and it correctly does not break the series.

Changed — a register citation naming a test that does not exist is refused

prove_falsifiers already refused an entry where no citation resolved. It had nothing to say when some did, so an entry citing three tests and defining one proved the one and dropped the other two in silence.

That is D147's exact shape: the entry closed on opted_in_command, the citation read as evidence, and the function had never been written. The sibling citation that does resolve is what hides the fiction. Every cited name must now resolve, and the refusal names which did not.

Documentation

3.4.0 - 2026-09-11

Config excludes that held a glob start working, so a scanned population can shrink and a score can move; the delegated security pillar reports the delegate's scoring model; scan trends now break when a delegated producer changes.

Fixed — a directory exclude holding a glob matched nothing (D156)

Minor rather than patch, because an inert rule becomes live. A configured exclude that silently did nothing now excludes what it names, so the scanned population shrinks and a reported score can move. Nothing about the config format changed — the patterns simply start meaning what they always said.

Two forms were dead:

Three branches and both fell through all of them. The literal branch cannot see a *. fnmatch against the whole path wants a trailing slash no file path has. The containment branch — the one that makes a bare dir/ work at any depth — is skipped because the pattern holds glob characters.

So the two spellings of one intent differed by everything: dir/ excluded at every depth, **/dir/ excluded nothing. A config mixing the forms had half its rules working, and file patterns like **/*.min.js were unaffected, which made the failure harder to see rather than easier.

An inert exclude has no symptom of its own. Nothing errors, nothing is reported, and the only evidence is findings in files the operator believed were excluded — which reads as the audit being noisy rather than the config being dead.

If your config uses either form, expect fewer files scanned after this upgrade, and a score computed over that smaller population. That is the rule doing what it was written to do.

Changed — the delegated security pillar is read from secure-code-agent 0.10.0

The pinned version moves to 0.10.0, the first release emitting the v2 pillar contract. MA reads v1 and v2, keying a delegated trend on the producer's scoring_model where the document declares one and on its version where it does not.

0.10.0 carries a scoring change on the producer's side, so a delegated security trend breaks once at that boundary. That break is correct: the same repository scores differently under the new model, and joining across it would draw a line through a change in the instrument.

3.3.0 - 2026-09-11

Changed — a repository's documented test command no longer runs (D152)

Minor rather than patch, because a config key stops taking effect. If a repository's maintainability-agent.json documents expected_commands.test, that command is no longer what this tool runs when the suite is opted in. The command now comes from the person's tier — the one they were shown and accepted at setup.

D147 closed the half it named: load_config strips test_execution.requested from the repository tier, so a tree cannot opt the host in. It never stopped the tree choosing what runs.

expected_commands.test is deliberately not stripped — it is documentation, and require_test_command reads it, so removing it would fail a real gate on every repository that documents its command honestly. The comment guarding that decision ended: "their answer is what opted_in_command reads."

opted_in_command did not exist. Three docstrings named it as the user-tier reader that made keeping the key safe, and nothing implemented it. run_test_suite read the merged config, where the repository's document lands by design.

So a person consented to pytest -q, having been shown pytest -q, and the host ran whatever the tree documented instead. That is worse than an unclosed opt-in, because the audit trail says the person agreed.

test_execution.timeout_seconds took the same path unclamped: a documented 2147483647 made the host wait sixty-eight years for a child it was told to run. It now passes the same bound analyzers.timeout_seconds already had.

A repository that documents a command still satisfies require_test_command. It simply no longer chooses the program.

Added — --security-pillar, and the delegated pillar that now runs

ADR 007 delegates the security pillar: this tool reports what secure-code-agent measured rather than measuring it. The document is read from .maintainability/security-pillar.json, and that path was hardcoded — while secure-code-agent's own help said the file is "for maintainability-agent to ingest via --security-pillar", describing a flag that did not exist.

It exists now, and takes a path. The default is unchanged, so nothing moves for anyone not passing it.

Fixed — a delegated pillar's trend joined across the producer's scoring change (D155)

A delegated pillar's condition is stored in scan history like any other, and the comparability gate could not see when the tool that produced it changed its scoring. secure-code-agent altered its normalizer twice in one day without touching the document's schema: same shape, same fields, a different number for the same repository. The trend would have drawn one continuous line through it.

Scan records now carry delegated_producers — security:secure-code-agent 0.9.0 per delegated pillar — and it is a comparability field, so a producer or version change breaks the series and names itself.

History written before this release has no producer recorded, so the first scan after upgrading opens a new series. That break is real: the scans before it cannot be told apart from ones a different producer version made.

The trend keys on the producer's scoring model where its document declares one, and on its version where it does not. Keying on version alone over-breaks — a docs release of the delegate would reset the trend having changed no score — so the pillar contract gained a scoring_model field, agreed between the two tools before either released.

MA reads schema v1 and v2. v2 is v1 plus that field, so either tool can release first and there is no window in which documents are refused.

A v1 document is never collapsed onto a single model id: the delegate's releases before the field existed do not share one scoring model, so they keep the version as their key and fragment correctly.

authorize_config on the MCP seam checked that a caller's config_path resolves inside the repository. That catches a config outside the tree. It cannot catch an inward link — root/link -> root/src — which resolves to a path inside the repository and passes every containment check there is.

D34's rule is two halves: resolve and bound, then walk the route as written, because an inward link shows only on the lexical path. repository_path did both. This door did the first, so the two disagreed on the same tree — one refused, the other returned the target. The audited tree supplies the directory the link lives in.

Added — reports disclose that analyzer children are not isolated (D154)

Product intent has said since P1 was amended that this package "does not sandbox children", and lists "that third-party tools cannot use the network" among the things it does not promise. The scoping is correct and deliberate. The report never said it.

A reader holding an audit whose promise reads "the analysis itself performs no network access" and a coverage table naming the external tools that ran had nothing connecting the two. The coverage section now carries the note, once, when children actually ran.

P8's gap rather than P1's — an undisclosed promise, not a broken one.

Fixed — the HTML report carried none of the coverage gap notes

"One source only", "nothing examined", and a dimension the analyzer tier declined are the coverage section's prose about its own gaps. The HTML skin printed the table and none of the notes — including "Nothing examined", which the module itself calls the point of the whole section.

ADR 011 says the skins render one report dict and never disagree. That claim was resting on the prose being built as Markdown strings inside the Markdown renderer.

Fixed — the MCP presentation contract described itself wrongly

audit_repository's documentation said chat and markdown are "on the wire the two are the same text" while the code beside it already rendered chat as the bounded skin and markdown as the complete report. The behaviour was right; the sentence was not, and a reader who believed it would have taken the bounding for a rendering accident rather than the contract.

3.2.0 - 2026-09-10

Added — the security pillar reports what secure-code-agent measured

ADR 007's fifth pillar was never MA's to measure. It is now delegated rather than absent: if .maintainability/security-pillar.json exists and carries the secure-code-agent/security-pillar v1 schema, the pillar table names the producer, its practice level and its condition, and both the chat and HTML reports render it alongside the four MA computes.

The file is treated as a boundary, not as trusted input. A missing file, a directory, unparseable JSON, a non-object, an unknown schema, a mistyped field, or a posture that disagrees with the level and condition it was derived from all resolve to "no delegated pillar" — never to a partial one. The two axes are still never averaged.

No delegate, no change: a repository without the file reports exactly what it reported before.

Changed — the trend section reports the current series, not all of them

A repository that has been audited across many releases accumulates comparable segments: the series breaks whenever the instrument changed, which is correct and is why a spliced trend line is never drawn. This repository's own history reached 49 of them, and all 49 were rendered in full, in both skins — a section nobody reads.

The series in force is now given in full and the earlier ones become one table, newest first. Both skins, identically.

Two things that section said are also fixed. It rendered "1 scans" for every single-scan series. And it named the changed field as rubric_version, which holds the package version — so a reader was told the rubric moved when a patch release was all that happened, on 38 of this repository's 49 breaks. Break reasons now read in prose: "the tool version changed", "the configured thresholds changed".

The stored field name is unchanged, so no history migrates.

Added — ADR 014 settles the adversarial-properties scope question

ADR 013 deferred the detection half of that play to its own ADR because it brushes the secure-code-agent boundary. ADR 014 decides it: MA takes the evidence-integrity half — code that reads an absent, empty or failed measurement as a passing result — and leaves every exploitability class to the delegate. The boundary is a rule, not a list, and it is directional: where a finding is genuinely both, MA yields.

A scope decision only. Nothing is built, nothing gates, nothing scores, and implementation is gated on a precision bar frozen before any result exists.

Fixed — pairing read filenames, so 52 tested modules were called untested (D149)

describe_tdd paired a production module only when some test file's subject stem matched it — test_scoring.py covers scoring.py. That is one convention stated as if it were the only one, and every suite organised by behaviour was reported as having untested production code.

The tool did it to itself. The 3.1.0 self-audit raised 58 unpaired-hotspot findings against 146 production files; 52 of those modules were imported by at least one test, under a suite at 94.46% coverage with a single module below 82%.

Pairing now accepts a test that names or imports a unit. Python reads the imports from the AST rather than a regex; the regex path is retained for JS/TS, Java and Fortran.

Reported numbers move. A repository whose suite is behaviour-named will see unpaired-hotspot findings drop and its testability aspect rise. Nothing about the repository changed — the previous reading was wrong.

Changed — an interactive terminal asks all seven setup questions (D150)

docs/cli.md promised an interactive first run on a TTY is "the same setup questions as chat/MCP". It asked five of seven. default_format is legitimately absent (ADR 011 §3 asks it per invocation); economics was simply missing, and nothing was asked in its place, so a terminal user was never offered the economic scenario and got reports without money in them having declined nothing.

Minor rather than patch, because first-run behaviour changes. A terminal user who has not yet configured a repository is now asked one more question than before, and answering it changes what their reports contain.

Changed — a release build refuses to run while the ledger has an open entry (D151)

v3.1.0 was tagged at 17:13 with a known defect that was not filed until 20:16 — a violation of the project's own standing rule, caught by nothing because nothing checked. The release workflow now greps the defect register for ### D<n> — Open and fails before the build if it finds one. The gate is unconditional: it runs on every trigger, not only on tag pushes, so it cannot pass by never having run.

Fixed — four sentences said "COBOL are parsed" (D148)

Sentences built around a count agreed with the wrong noun when the count was one. A small _grammar module now supplies the verb, pronoun and possessive for a quantity, and the four callers take agreement from it rather than hard-coding the plural.

Fixed — the security job in this repository's own CI ran without its config

quality-gates.yml invoked secure-code-agent with no --config, so secure-code-agent.json sat in the tree unread and the job scanned under defaults. Repository hygiene rather than a shipped defect, recorded here because the audit that found it is the same one that produced the pillar integration above.

3.1.0 - 2026-09-09

Changed — a repository can no longer opt the host into running its own code

Minor rather than patch, because a config key stops taking effect. If a repository's maintainability-agent.json sets test_execution.requested: true, its suite will no longer run. The opt-in now comes from the user tier alone. A person who answered the setup question is unaffected; a repository that answered it for them is ignored.

Decision 9 says this agent does not execute the audited tree's code, amended by Class 5 to "unless the operator opted in and configured a command". suite_opted_in read both halves of that opt-in from the merged config, where a repository beats a person by design — so four lines in a pull request opted the host in and chose the program, with no reply from anybody:

"test_execution": {"requested": true},
"expected_commands": {"test": ["…"]}

That is D35's trust inversion on the one seam that runs an arbitrary command rather than installing a package, so it is the same defect with a shorter path to execution (D147).

load_config now strips test_execution.requested out of the repository tier before the merge. The check stays where it was, so suite_opted_in and run_test_suite remain pure functions of the config they are handed — what a caller, a test and a reader all expect — and the tier question lives in the one function whose job is deciding which tier wins.

expected_commands.test is deliberately not stripped. That key is documentation — "this is how you test me" — and the require_test_command hard gate reads it to require that a repository declares one. Removing it would fail that gate on every repository doing the right thing, to stop a threat the key does not carry: a command nobody opted in to running is inert. Authority is the opt-in; the command is documentation.

Migration: if you relied on a repository config to enable suite execution, run setup once (action: "reconfigure", or an interactive CLI run) and answer the test-execution question. Nothing else changes.

Fixed — two staged setup replies copied the audited tree into the user tier

_apply_bounds and _apply_command handed write_user_answers the repository's own document. So a repository declaring analyzers.acquire_tools, plus any stage-two reply — the labor rates, or the test command — copied that grant into the user tier, and acquisition_permitted read its own tier and answered true. The check was never wrong; it had been told the tree's answers were the person's.

Not a missed validation. Both writers built one dict for the repository write and reused it for the user write, and the tier boundary was carried by a variable name. The user tier is now built from the user tier, the way _persist_answers already built it from the setup payload (D147).

Fixed — repository-controlled reads bypassed the regular-file door

Nine readers took a path the audited tree chooses and read it by name: every package.json under the root, .gitmodules, every CI and toolchain file, every discovered file's banner, the checked-in TypeScript analysis, every TypeScript source, every manifest, coverage.xml, and the whole unread_source walk.

read_text on a FIFO waits for a writer that never comes, so a package.json named as a pipe stopped an audit dead — and no except OSError catches that, because there is no error to catch, only a process that does not return.

The quieter half is D141's other side: unread_source and recorded_type_analysis tested is_file() first, which is False for a FIFO, and read the path as absent. unread_source is the worst of the set because it reports what share of the repository the score describes — a source-suffixed FIFO went missing from both sides of that fraction, so the share read exactly as it would if the file had never existed (D145).

Fixed — --check read a plus-only diff fragment as file content

An agent copies the green lines out of a review pane and pipes them in. No @@, no ---/+++, so every shape check passed it through, the brace scanners found nothing in +function hello() {, and declarations_read came back true on all 33 declaration suffixes.

D133 closed this for a whole diff and D138 for a hunk-only paste, each written from the example reported. The rule is now stated instead: a body whose every non-blank line begins with + or - is a diff fragment. The mention-versus-assertion guard survives by construction — somebody writing about a diff writes something around the quoted line, and that line does not begin with a mark. Context lines carry a space, so a pasted excerpt still parses (D146).

Fixed — the unanchored caveat named five anchored languages

UNANCHORED_LANGUAGES still read ("Swift", "COBOL", "Go", "Rust", "PHP", "Ruby") two releases after 3.0.0 anchored five of the six. Every skin that prints a grade reads it, so Swift, Go, Rust, PHP and Ruby readers were told their grade was provisional against a corpus that had measured 14, 14, 14, 14 and 12 repositories of their language. A caveat naming an anchored language is not a caution; it is a false statement about the evidence (D143).

roadmap.md and both copies of standard.md carried the same claim in prose nothing checks, and are corrected with it.

Fixed — environment remedies addressed whichever pip PATH found

environment_work_order emitted pip install lizard and lizard --version. Neither names the interpreter that just reported the tool missing. This is 3.0.2's D142 seen from the other side: on the machine where that was found, the work order said pip install lizard to someone who had run exactly that. Pip-backed adapters now emit {sys.executable} -m pip install <dist> and verify with pip show (D144).

Changed — this repository's own security gate can now fail

Audit (maintainability + security) is a required check here and its security half could never trip: --fail-on-gate was passed with no secure-code-agent.json in the tree, so no gate existed. Measured on a three-line target with subprocess.run(cmd, shell=True) — two HIGH findings, grade F, gate PASS, exit 0.

Now configured rather than dropped, and proven both ways: exit 0 clean, exit 1 with a planted sink. The five outstanding criticals were all one false positive (curl -u "$SONAR_TOKEN:" is a variable, not a literal), acknowledged in .scignore.yaml with a reason and an expiry rather than silenced by lowering a threshold. secure-code-agent is pinned: unpinned, it changed the meaning of a green build between two runs four hours apart.

This is CI configuration for this repository, not product integration. The security pillar remains DELEGATED and this tool still never runs a security analyzer.

3.0.2 - 2026-09-09

Fixed — the analyzer pool was invisible to any system-Python install

A silently degraded audit scored better than a correct one. Found by running this agent against its own repository through the MCP server, the surface product intent calls primary. Nine analyzers reported "not installed or not on PATH" — the whole Python pool, lizard and complexipy among them — so declarations fell to the built-in tier.

Every one of them was installed. locate searched the interpreter's own bin and then PATH, on the assumption that "Python analyzers install alongside the agent". That holds in a virtualenv. On a system Python it does not: the framework directory is not writable, pip falls back to --user, and the console script lands in the per-user scripts directory — on macOS ~/Library/Python/3.11/bin, which is on neither list and not on the default PATH.

So the environment work order told the operator to run pip install lizard, they had already done exactly that, and the next audit reported it missing again. The remedy the product prescribes could not fix the problem the product reports — for any operator on a system Python, which is the install path the README itself offers first.

Measured on this repository, same commit and config:

pool invisible pool visible
tools contributed 0 of 13 9 of 13
verified grade B C

The estimate did not move; the grade did, and downward. A degradation that flatters the score is the worst direction for a silent failure, because nothing in the result asks to be looked at — the gate passed, the letter improved, and the only signal was a work order whose instruction had already been followed.

Both script directories are now derived from the running interpreter: sys.executable's parent, then sysconfig.get_path("scripts", ...) under the preferred user scheme. Neither is hardcoded, so this is not a macOS patch. PATH remains the last resort, so tools installed by homebrew or a system package manager — jscpd and pmd here — still resolve.

Filed as D142 with a falsifier and four guards written from the opposite direction, because the dangerous fix is one that widens the search until something is always found and names a tool that cannot run.

A note for MCP users: the server is a long-lived process and holds the module it imported at start-up. After upgrading, restart it — reloading the editor window is enough — or it will keep reporting the old result.

3.0.1 - 2026-09-08

Fixed — the post-2.11.0 audit, closed in full

Four defects. No scoring constants move, so no grade changes; one label in every report does.

A hunk-only diff read as clean (D138, High). --check refused a complete diff -u and nothing else, so the fragment an agent actually pastes — the hunk alone, because that is what a chat window shows — read as file content and every one of the 46 parsed suffixes reported declarations_read: true on it. D133 closed "a piped diff" on a detector that required the ---/+++ headers too. Either half of the shape now refuses. A hunk header quoted inside a string still parses: the check is anchored at line start, so writing about a diff is unaffected.

A published declarations rate mixed two counting conventions silently (D140, High). 3.0.0 completes an analyzer criterion set from the built-in tier per declaration, and published the result as an analyzer reading with no provenance. lizard reports cognitive complexity for no language at all, so on thirteen of the fourteen parsed languages that rate is two conventions wearing one label. Reports now say declarations (completed by built-in detectors). This changes the "Estimate source" line in every report where the fill applies — the number is unchanged, the attribution is not.

Scan reads hung on a FIFO, and skipped it silently (D141, Medium). The regular-file guard operator-named reads use is now shared with scan reads rather than reimplemented. That alone changed nothing, because the FIFO never reached a read: the walk tested is_file() first, which is false for a FIFO, so src/hang.py was dropped and the audit measured one file short without saying so. It refuses now, rather than hanging or quietly under-reporting.

The language-support lead out-claimed its own verification (D139, Medium). The page said every language's branch set is derived from its own grammar. Only Python's is — ast implements the grammar and CI enumerates from it. D129 corrected the verification section seventy lines down, where a reader of the table never reaches it.

3.0.0 - 2026-09-08

Changed — the recalibration, and the first release that re-grades

Every published grade may move. 33 of the 112 repositories present in both the 2.x and 3.0 corpora change grade — 15 B→C, 7 C→B, 5 A→B, 4 C→D and 2 B→A — and the corpus gains its first D grades. A 2.11.x re-measure taken the day before moved nothing at all and said so; this one moves a third of them, which is the difference between a patch and a major.

Dimension 2.x 3.0
file_size 0.0952 0.0938
declarations 0.0908 0.0962
duplication 0.3222 0.3144
risk 0.0826 0.0341
gates 0.05 0.05
CALIBRATION_C 8.7161 4.2565

The corpus median still rolls up to exactly 4.0000, which is the anchor's only job and the one number that must not move.

Three changes land together because each moves scanner_fingerprint and invalidates every stored measurement. Paying for them separately would mean measuring the corpus three times for an anchor obsolete at the next change.

The corpus grew from 112 repositories to 180, and from eight anchored languages to thirteen. Swift, Go, Rust, PHP and Ruby are anchored at fourteen repositories each, twelve for Ruby. The 112 existing rows keep the commits their measurements were taken at, so the move is attributable to what was added.

COBOL is not anchored, and will not be. Measured against the corpus criteria, GitHub holds one COBOL repository above 500 stars, and what is there is not COBOL: the most-starred is a programming course, proleap, che4z, koopa and opensourcecobol4j are Java projects that parse or compile COBOL, and node-cobol is a JavaScript wrapper. The one genuine COBOL codebase has 38 stars. Its real corpus is on private mainframes. COBOL stays parsed and disclosed as unanchored rather than anchored on teaching material.

The analyzer tier now drives declarations almost everywhere. No single tier measures the full criterion set: lizard reads sixteen languages for cyclomatic complexity and declaration lines and none for cognitive complexity, while the built-in scanner reads all three for fourteen. Completing one from the other per declaration, joined on (path, start_line), takes analyzer-driven declarations from 6 rows of 180 to 173. Where lizard reads the language its readings now drive the score, and the hand-written keyword sets supply only the concept no permissively-licensed tool measures.

risk more than halved, and the languages explain it. Risk patterns are configured policy and the defaults were written against Python and C. As a multiple of the new reference the added languages read Swift 0.00×, PHP 0.01×, Ruby 0.04×, Go 0.10× and Rust 0.26×, against C 5.82×, C++ 4.95× and Python 4.24×. The old reference was measuring the pattern set rather than the code.

Also in this release

Changed — the reference corpus was re-measured, and the anchor held

No behaviour changes and no constant changes. The measurements file is repository evidence under tools/ and is not packaged, so no published number moves.

The stored corpus had gone stale without anyone noticing: its rows were measured at tool versions 1.10.1 and 2.0.0 under scanner fingerprint 3b5e063e95316342, while HEAD is 2.11.2 under 1a902618a0600746 — eleven minor releases later, including four new language scanners and the D112–D136 fixes. All 112 repositories were re-measured --with-analyzers at the same pinned commits, so every move is attributable to the scanners and to nothing else.

92 of 112 declaration readings moved and the anchor did not.

stored measured
file_size 0.0952 0.0952
declarations 0.0908 0.0903
duplication 0.3222 0.3222
risk 0.0826 0.0811
gates 0.05 0.05
CALIBRATION_C 8.7161 8.7161

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 the 10% the suite tolerates, so they are left as they stand rather than edited by a margin smaller than the sampling error in them. No repository changes grade — A=5, B=52, C=55, median exactly 4.0000, before and after; two repositories move at the published decimal, by 0.1 each.

This is the first of the recalibration's four steps, and it falsified the reason the roadmap gave for shipping that work as a major release: that it "re-grades every repository, which is a breaking change to every published number". For scanner drift that is now measured and false. The prediction has been struck where it appeared and replaced with what was measured. It says nothing about extending the corpus to hold the six unanchored languages, which moves the population the medians are drawn from rather than the instrument reading it; that half is still untested, and the major still rests on it and on the two built-in changes.

2.11.2 - 2026-09-07

Changed — the documents caught up with the code

No behaviour changes. What ships is the packaged copy of the scoring standard, which had drifted from the document it is generated from.

The roadmap was the worst of it, and it is the one document a reader uses to decide what is coming. It said ## Next: 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. It counted "nine declaration languages" when there were fourteen. It described Swift, Go and Rust as examples of unwritten languages, in the releases that wrote them.

## Next now names the recalibration, which is the only scheduled work: six unanchored languages, plus deleting the per-language cyclomatic regexes and merging analyzer evidence per concept rather than per dimension. Everything that touches measurement queues behind it.

One of these was a false claim rather than a stale count. The corpus entry said the anchoring gap "closes for what ships today" — it does not. Swift, COBOL, Go, Rust, PHP and Ruby all ship parsed and unanchored, scored against an anchor holding none of them. That is the corpus policy working as decided, and it is the debt the recalibration pays; saying it had closed was the error.

Five more, found by sweeping for the same shape rather than waiting to trip over them:

Each correction states what it used to claim rather than being silently overwritten, because "Next: Swift" surviving seven releases is the part worth remembering.

Fixed — an audit of the surfaces, six defects and a decision

Three High, three Medium, filed as D130–D136. Each was reproduced before it was fixed and each is closed as a class rather than as the instance reported.

Changed — --check stays on the CLI

Decided 2026-09-06. D136 recorded chat as the primary surface and --check as CLI-only. It is closed as a non-goal, not left open: an open entry naming a gap reads as an instruction to close it. An MCP tool is not a port of a flag — it is a name, an argument contract, and a decision about the setup and consent flow every other MCP call goes through. If chat users ask for it, it reopens as a feature with its own contract rather than resuming as an unfinished bug.

The register's guards learned the category rather than being satisfied with a fake test: a defect closure names a falsifier, a decision closure names a decider and a date.

2.11.1 - 2026-09-06

2.11.0 was tagged and never published. Its release build failed on a stale row in docs/release-plan.md, before the publish job ran, so nothing reached PyPI and the version is skipped rather than yanked. This is the first release of that line, and it carries 2.11.0's whole contents plus the fixes below.

Fixed — four constructs no fixture contained, two of them High

An audit of the 2.11.0 tree found four defects, and none of their constructs were in any grammar fixture. Filed as D125–D129.

Added — Python's fixture coverage is derived from the grammar

ast is the module that implements the Python grammar, so the checklist is asked of it: every node whose fields carry test, orelse, handlers, ifs, cases or finalbody, plus BoolOp. No node names are written down, so a construct added to a future Python arrives in the checklist on its own rather than when somebody remembers it.

Run against the existing fixture it immediately named three constructs that had never been exercised: Assert, AsyncFor and TryStar.

Every other language's fixture is still a hand-written set, and the documentation now says so instead of implying coverage it does not have. Ruby's Ripper, Go's go/ast and Rust's syn can each do the same and are unwritten rather than refused.

Changed — documentation that contradicted itself

Known open defects

Seven, filed as D130–D136, two of them High: --sarif-input reads an operator-named path with no validation, and a unified diff piped to --check reads as clean for every brace language. Both are in surfaces already released in 2.9.0 and 2.10.0 — this release does not introduce them and fixes two High defects that would otherwise stay live.

2.11.0 - 2026-09-06

Fixed — every branch keyword now answers to a second implementation, and nine defects fell out

This leads, ahead of the four new languages, because it is the larger fact about every number this tool has ever printed.

Until this release every branch keyword set here was asserted rather than tested: a list written from somebody's knowledge of a language, checked against examples written by the same person from the same knowledge. That reads as evidence because the prose around it is confident, which is worse than reading as a guess.

lizard computes cyclomatic complexity from a separate codebase, by separate authors, with its own reading of each grammar. Measured against it on this repository's own source, this project agreed on 448 of 985 declarations — 45%. It is now 968 of 1000.

Nine defects, filed as D112–D120 with the mutation that reproduces each:

Correcting the measurement immediately exposed five real gate breaches in this repository's own source that the broken reading had been hiding, the worst a function at complexity 29 against a limit of 15. All five are decomposed rather than waived.

Added — the check that makes the above hard to repeat

Added — Go, Rust, PHP and Ruby are parsed

Four languages with declaration scanning, per-declaration complexity and budget enforcement, each with branch keywords derived from its own specification and checked against the oracle above.

None of the four is in the reference corpus, so a grade for code in them is provisional and the report says so on every run: the findings are as good as the parser, but the rate they are compared against was measured on other languages.

Changed — documentation split at the gates this tool enforces

2.10.0 - 2026-09-05

Added — --check, the in-loop door

Fixed — six defects, none of them found by the author

Every one came from somewhere other than the person who wrote the code: a field check that read no source, an adversarial audit, and the gate turning on itself.

Fixed — the falsifier gate had stopped proving an entire file

2.9.0 - 2026-09-05

The other end of the loop. This project has said in its own roadmap that it is end-of-loop heavy — strong where it is cheapest to be strong, a CI gate after the work is done, and thin during the loop where a constraint prevents rather than rejects. --staged is the pre-commit half of that gap.

Added — --staged, and the hook that runs it

Fixed — the falsifier gate could be talked out of proving things

Changed — run_git decodes what it cannot read instead of raising

Fixed — a suppression scan that reported markers it only read about

2.8.1 - 2026-09-05

Everything SonarCloud had to say, once the project was made public. The gate had only ever blocked on new code, so five findings in older code had never surfaced. Four closed by changing the code; two were resolved as false positives with the grounds recorded in the register rather than only in a dashboard.

Fixed — a file kept its own permissions, and a read stopped hanging

Added

Unchanged, deliberately

A symlinked config is still allowed: the operator named the path and controls it, and the audited tree's own default is a different question discovered_config already answers. Pinned by a test so it is not added by reflex.

The authorship gate still checks signatures over merge commits. Making GitHub's "Update branch" button pass would have meant skipping them, and that control exists because skipping them let unattested content through once already. Stale branches get rebased instead.

2.8.0 - 2026-09-04

An audit on f51fbbc found three classes, all shipped in the previous two days. Two of them were mine from 2.4.0 and 2.7.0.

Security — the audited tree chose what this process executed

_semantic_ts resolved tsc from the tree's own node_modules/.bin/tsc before falling back to PATH. A tsconfig.json beside a writable node_modules/.bin/tsc was therefore enough to make this agent execute the repository under audit — and the tree did not have to contain any TypeScript. SECURITY.md's claim that the opt-in test command is "the sole path by which anything from the audited tree runs" was false for as long as that resolution existed.

Deleted rather than guarded. There is no safe way to run a binary whose identity the audited tree controls. tsc is now invoked as a constant program name resolved on PATH.

Fixed — two scanners missed in the unsafe direction

Added — the grade says which languages the anchor omits

score.reference.corpus_note claimed the corpus spans "every language this scanner parses". True when written, false from 2.4.0, and it stayed that way for three releases across the report, docs/standard.md, the packaged standard and the README — while the remediation prompt still described a Python/TypeScript/JavaScript corpus.

Unanchored remains policy. What changes is that every surface printing a grade now names what the anchor does not hold:

A limit disclosed only in the JSON is disclosed nowhere that matters — the shape this project already shipped once as F1.

2.7.0 - 2026-09-04

COBOL is the tenth parsed language, and the first whose declarations have no end marker at all. C closes a body with }; Fortran closes one with end. A COBOL paragraph ends where the next paragraph begins — the boundary is the start of the next thing, and nothing in the source announces it. scan_bounded already took the bounding rule as an argument, so this needed no change to the shared walk: what is shared is that a range never runs past its own body, not the mechanism enforcing it.

Added

The judgments, because each is a defect taken the other way

Known and disclosed

No offline analyzer reads COBOL. lizard does not; jscpd reads duplication only, which is the half the complexity floor exists to reject. The mainframe tooling that would — IBM Developer for z/OS, ADDI, the analysis around Dependency Based Build — is licensed and host-resident. So the built-in scanner is the only reading COBOL gets, the analyzer tier reports it as unmeasured rather than pretending, and both the README and the language-support page say so.

A section whose statements sit outside any paragraph mints nothing, COPY members are not expanded, and REPLACE is not applied.

Not calibrated. Under the corpus policy decided with 2.4.1, COBOL ships parsed and unanchored: its findings are as good as its parser, and its grade is provisional until the single recalibration that follows the remaining scanners.

2.6.0 - 2026-09-04

Did run seven of this transformation land better than run six? The last of the five remediation-integrity items, and the only one that is a report rather than a gate. Where an agent performs one class of work repeatedly — a framework upgrade, a migration, a codemod across services — nobody measures whether the seventh run produced better code than the sixth. The generator cannot: its output is not reproducible and it keeps no memory across runs.

Added

What it refuses to claim

The value here is entirely in what it declines to say, because every temptation runs the other way.

Scope

One repository's history. The comparison joins the runs recorded in the tree being audited, which is the case the audiences in product-intent.md actually have: the same agent doing the same kind of work in one repository, over and over — a framework upgrade done in six passes, a migration re-run.

Corrected after release rather than quietly rewritten: this section originally announced a cross-repository version, over a fleet of services, as the case "where this is worth the most". No audience in product-intent.md is a platform team with a service fleet, and no such direction had been decided. It was an invented roadmap commitment and it is withdrawn.

2.5.0 - 2026-09-04

The repository answers the test-command question. expected_commands.test was hand-typed on every tree, and the prompt's example was pytest, so the question was hardest to answer exactly where this tool had just learned to parse the language: a Package.swift beside a Tests/ directory says swift test as plainly as a manifest says anything.

Added

What it refuses to guess

A command that cannot run is worse than no suggestion, so three cases are deliberately blank:

Unchanged, deliberately

Detection suggests and never decides. The require_test_command hard gate asks whether a human documented a command; a value the tool wrote into the config unasked would satisfy that gate on evidence it invented about itself. A test pins that a detectable command does not silence the gate.

2.4.1 - 2026-09-04

Swift shipped unreachable. .swift was routed to its scanner and absent from the default include_extensions, so no Swift file was ever opened and the language 2.4.0 announced did nothing. The guard that was supposed to catch this checked one direction only — that every included extension has a parser — and a language parsed but not included passed it silently. It now checks both, and the failure message says why the one-directional version was worthless: the language is claimed and unreachable.

Measured on vapor/vapor: 3 files and 0 declarations before, 253 files and 1,924 declarations after. Across the Swift candidates, 4 of 16 repositories met the qualification floor before the fix and 16 of 16 after — which is the shape of the bug, not a tuning change.

Documentation caught up with 2.2.0 and 2.3.0. The remediation-integrity flags (--conformance, --fail-on-out-of-scope, --fail-on-regression, --attestation-output) shipped two releases ago and appeared in no user-facing document. The README now has them; the roadmap's "remediation hole" section and four entries under architecture's Known debt are closed rather than still listed as future work; and product-intent.md grows the loop's fifth step, because "the work order bounds the agent by instruction and nothing verifies it" had become false. What is still true, and is now stated in all three places, is that these checks read a diff's shape and never its correctness.

2.4.0 - 2026-09-04

Swift is the ninth parsed language, and the first added since the corpus was extended — so it is scored against an anchor that does not hold it, which the roadmap already names as the cost every new language re-incurs.

Added

Fixed

Swift has no statement terminator, so the shared bare-signature rule — a signature that closes without opening a brace, which in C is a ; — could not see where a protocol requirement ended. It walked on and adopted the next line's brace, reporting func describe() -> String as two lines of body. A protocol of forty requirements is not forty declarations.

class is both a keyword and a modifier: class Widget declares a type, class func make() declares a type-level method. Stripping it with the other modifiers cost every final class Store its keyword — the type vanished and only its members were reported, which makes a declaration rate wrong rather than merely incomplete.

2.3.0 - 2026-09-04

The attestation artifact. The three remediation-integrity mechanisms now compose into the record they were built for. No score changes.

Added

Why an independent process has to produce it. A platform that generates code and then reports on its quality is producing a self-assessment — a property of the arrangement, not a criticism of any particular one, and no amount of engineering removes it. The question a reviewer asks is who checked this, and can they re-derive the check months from now, and an author cannot be the answer.

The value is in what it refuses to overstate, so that is what the tests hold:

It never opens the diff to judge it and it touches no score, both stated in the artifact rather than left to inference.

2.2.0 - 2026-09-03

The third remediation-integrity mechanism. No score changes.

Added

The judgment that matters is when subtracting is allowed at all. Two scans taken with different instruments cannot be differenced, and _scan_history.segments already owns that rule for the trend charts, so the ratchet refuses rather than reaching across a break. Without it, the day 2.0.0 moved CALIBRATION_C from 5.8843 to 8.7161 this gate would have reported every repository on earth as catastrophically regressed — and the gate would have been the thing that was wrong.

So there are three outcomes, not two: held, regressed, and not comparable. The third exits 2 and prints why, because a run that could not ask the question must not be read as one that asked and found nothing.

Two further limits, both tested: an aspect that becomes unmeasurable is not a drop — withheld evidence reading as a failure is the inversion P3 forbids — and aspects are reported beside the verdict but do not decide it, since failing on every aspect wobble makes a gate that gets switched off.

Changed

2.1.0 - 2026-09-03

The bounded work order becomes checkable. Fix exactly these findings and refactor nothing else was the product's central claim and was enforced socially: the prompt said it, and nothing compared the diff that came back. No score changes in this release.

Added

The two halves ship together because either alone is bypassable. Scope conformance answers "did it change only what it was asked to" — and is satisfied by touching the named file, adding # noqa to the flagged line, and changing nothing else. Suppression detection answers "and without switching a checker off in a file that was flagged". The record reports conformant for the first and clean for both; the gate reads clean.

Two judgments make it usable rather than noisy. A test pairing to a named file is in scope, by the convention _test_pairing already uses for coverage — a correct remediation nearly always touches the test that proves the fix, and flagging that would make every good remediation look disobedient. And only added lines are read, so a decade of accumulated directives already in the tree is not attributed to this change.

It never scores. Whether a diff was obedient is a fact about an agent's behaviour, not evidence about the code's condition, so _conformance is assembly and may not reach scoring — declared in architecture.md and held by test_architecture. The record carries a note saying it compared paths and added lines, so a green result cannot be read as "the change was correct".

Fixed

2.0.0 - 2026-09-03

Every score changes. The reference corpus every number is a multiple of now spans all eight parsed languages instead of three, so CALIBRATION_C and the per-dimension references move and every repository re-grades. Read the migration note before comparing a 2.0 score to a 1.x one.

Changed

The original 40 rows are unchanged and still pinned to the commits they were measured at, so what moved the constants is the languages added and nothing else. Selection is the same mechanical query — created before 2021, pushed since 2026, under the size cap — with one deliberate exception: Fortran entered at a 500-star threshold rather than 3,000, because the entire Fortran ecosystem has zero repositories above the bar the other seven use. Holding one number across ecosystems would have measured popularity rather than maturity and excluded the language outright. The per-language thresholds are recorded in corpus.json under selection.min_stars.

Added

Fixed

1.10.1 - 2026-09-03

Names a hole under the product's central claim, rather than leaving a reader to infer it is closed.

Changed

Prompted by What is agentic testing?, whose argument for where the model should stop describes the architecture this tool already has; its repair-step failure modes are the ones that still apply here.

Fixed

1.10.0 - 2026-09-03

ADR 013 ships. The adversarial audit now has an artifact.

Added

The hostile audit is the highest-leverage quality process this project has 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, with audit quality riding on whoever wrote the prompt that day. Not seeded, not repeatable, not comparable run to run.

The brief is emitted deterministically from one run and carries the commit under audit (and whether the tree was dirty), the evidence already computed — what ran, what did not, which concepts nothing measured, which have a single source — the eight published promises each with the shape of evidence that falsifies it, and the audit contract: reproduce rather than speculate, verify against the named commit, and turn every accepted finding into a test that fails without its fix.

The boundary is the one ADR 008 drew: the deterministic core seeds the hostile audit; it never performs it. An LLM red-team inside the audit would break P1 outright, so the reasoning happens outside exactly as the fixing does. It returns text — no gate, no score, nothing written, nothing sent — and its findings return to the falsifier-and-fix loop, never to the estimate, range or grade.

Fixed

1.9.0 - 2026-09-02

ADR 001 is finished. Stage 9 was the last unmigrated stage and the one with a shipped false result behind it.

Added

The coupling this replaces produced a real false number and took six audits to find. The oldest commit a shallow clone holds has no parent locally, so git diffs it against the empty tree and --numstat reports the whole tree as added: one fix commit measured (1 file, 75 lines) in a full clone and (2 files, 39 lines) at the shallow boundary. Each audit repaired the symptom it found because there was nothing to check a cache against. Now there is, and tests/test_history_manifest.py reproduces the boundary with real git repositories cloned shallow over file:// rather than mocking it.

Fixed

1.8.3 - 2026-09-02

The falsifier gate could not see a test added to a file that already existed, 1.8.2 walked through that hole, and closing it immediately caught 1.8.2's own guard.

Fixed

Changed

1.8.2 - 2026-09-02

The roadmap never mentioned the language work — not the five scanners shipped between 1.1.0 and 1.6.0, and not the ones still to come. The help docs never mentioned languages at all. Both are the front door for somebody deciding whether this tool reads their code.

Added

Fixed

1.8.1 - 2026-09-02

1.8.0 fixed the falsifier gate and could not prove it. Running that down found a second defect underneath the first, and both are now guarded by tests that fail without them.

Fixed

The line is now drawn at output, not exit code, because a collection error is a real signal here: a new test file naming a module the base does not have cannot be collected, and this tool already counts that as the weaker proof it is. Only a child that printed nothing at all never ran. - The 1.8.0 fix broke pytest for the child on some interpreters. Replacing PYTHONPATH with the worktree's src alone left the child unable to import pytest itself wherever pytest lives outside the default path. The worktree now goes first and the parent's path follows.

Added

End to end against v1.5.1, the gate now reports 19 of 20 added tests failing without their change, where before the 1.8.0 fix it reported 0 of 19.

1.8.0 - 2026-09-02

A retrospective audit of everything from v0.9.1 through v1.7.0 — 104 commits, benched against product intent, the architecture and the decision register — and the five findings it produced, fixed.

Fixed

Changed

Disclosed rather than corrected. Per-language references would remove the awkwardness by trading a stated limit for a silent breach of P2, and extending the corpus moves CALIBRATION_C and re-grades every repository — a deliberate release, not a patch. docs/standard.md carries the reasoning under "What the anchor does not cover". - Fixed-form Fortran tests state their strength, measured by mutation rather than claimed: swapping the card-column masker fails 1 of 9, removing labelled-DO tracking fails 3 of 9, and the rest are defensive regressions. A second test was added as a load-bearing case, measured, found not to discriminate, and is documented as defensive — the claim was removed rather than the test.

What the audit found holding: layering and acyclicity intact across 104 commits under 13 property tests; jscpd reporting 0 duplicate blocks across six sibling scanner modules; the setup question set unchanged through seven releases; and the tool ending its own audit at zero failures of every kind, having split four pieces of code that tripped its gates rather than shaving them to fit.

1.7.0 - 2026-09-02

Practice detection speaks the Fortran toolchain. Every list in _practice — linter configs, formatter configs, the lint command pattern, the gate manifests — was written from the Python, JS, JVM and Go ecosystems. Fortran appeared in none of them.

Fixed

It does not lift every Fortran repository, and that is the point. fortran-lang/stdlib has CI, an .editorconfig and no linter at all; it was level 2 before this change and is level 2 after. A detector that promoted every Fortran tree would be measuring the language rather than the practice, so that case is pinned as a test beside the fix.

1.6.0 - 2026-09-02

Fortran is measured with Fortran's reading. 1.4.0 found Fortran declarations correctly and then handed them to a C-family measurement, which produced numbers that were not approximate but wrong. This release fixes the measurement, gives Fortran the analyzer tier it was locked out of, and claims fixed-form.

Fixed

Added

Verification

Audited fortran-lang/stdlib — 640 Fortran files, 488 scanned. Ranges land exactly: add_log_unit is reported at lines 260-404, and the source has subroutine add_log_unit on 260 and end subroutine add_log_unit on 404. Complexity agrees with lizard on 4 of 5 procedures in a real file; the fifth is ours at 16 against lizard's 10, and all 15 decisions are enumerable in the source — 13 ifs, two dos and one .or. — so the difference is lizard under-counting single-line if (…) return.

1.5.1 - 2026-09-02

Documentation, and two guards so it cannot drift again.

Fixed

Documentation

1.5.0 - 2026-09-01

Fortran gets an analyzer. 1.4.0 shipped the scanner and noted that Fortran was the one claimed language with nothing external beside it — lizard does not read it. Fortitude is the answer: Rust-fast, pip install-able, 100+ rules across correctness, obsolescent features, modernisation, portability and style.

Added

It does not claim types. Fortitude's correctness rules are about declaring things explicitly — implicit none, intent, kind parameters — and a reader who saw types in a coverage record would take it to mean the code was type-checked. Nothing type-checks Fortran here. Only C191: unreachable-statement maps off style, to dead-code, because unreachable code is exactly that.

It is also a verdict emitter: it reports findings and cannot supply a declaration population. The built-in scanner remains the only path to a population for Fortran, which docs/language-support.md now says precisely rather than saying Fortran has no analyzer at all.

Notes

The recorded-output test pins the one way fortitude differs from ruff, whose JSON is otherwise the same shape: the location key is line, not row. Reading ruff's spelling here would have dropped every line number silently, leaving findings that name a file and no place in it.

1.4.0 - 2026-09-01

Free-form Fortran is a first-class language — and the first one here with no braces, which is why this release also generalises the walk that every other language shares.

Added

Fixed-form is deliberately not claimed. .f, .for and .ftn are column-sensitive — a comment is C in column 1, a continuation is any character in column 6 — which is a different scanner with different failure modes. Those suffixes stay out until they have one, and their own falsifiers.

There is no analyzer fallback here: lizard does not read Fortran, so the built-in scanner is the only path to a declaration population rather than a zero-install convenience.

Changed

Fixed

14 Fortran falsifiers cover keyword bounding against inner if/do/select blocks, bare end, interface blocks, typed and result-clause functions, derived types versus variables of them, case insensitivity, comments, contains, submodules, continuations, and the unclosed-unit fallback.

1.3.0 - 2026-09-01

C# is a first-class language, and the C family is complete. Third of the three increments — C in 1.1.0, C++ in 1.2.0, C# here. The architecture held to the end: a language is a module and a row.

Added

Properties are deliberately not declarations. public int Count { get; set; } has braces and would bound cleanly, and an ordinary C# tree holds thousands of them — each one line long, each nobody's maintenance burden. Counting them would dilute the population every declaration rate divides by. They are excluded by construction rather than by a rule: a property has no parameter list, and every pattern requires one. A method written public int Fast() => count_; is counted, because it has one.

Changed

Fixed

16 C# falsifiers cover properties, both namespace forms, attributes, constructor chains and destructors, generics with constraints, expression-bodied members, interface members, positional records, control flow inside bodies, and both unclosed-body shapes.

1.2.0 - 2026-09-01

C++ is a first-class language. Second of the three C-family increments (C shipped in 1.1.0; C# follows in 1.3.0). The architecture 1.1.0 built held: C++ is a module and a row, with no edit to the dispatcher and none to the shared core beyond the loop it now provides.

Added

.h stays C. It is the one extension both languages write, and the C reading under-reports a C++ header where the reverse would read no more and risk more.

Changed

Fixed

17 C++ falsifiers cover namespaces, templates, operator overloads, out-of-line definitions, initialiser lists, access specifiers, the macro case, control flow and casts inside bodies, and the indentation fallback.

1.1.0 - 2026-09-01

C is a first-class language. The first of three C-family increments (C, then C++ in 1.2.0, then C# in 1.3.0). A C repository now gets declaration-level findings from a pip install with no analyzer installed, instead of a withheld declaration rate.

Added

Two things are deliberately not declarations: a prototype has no body, so a header of 40 prototypes is not 40 declarations, and a preprocessor line is skipped whole, so #define MAX(a, b) … is not read as a function called MAX. K&R-style definitions and macro-generated declarations are missed, each costing one declaration and never a cascade. The full list is in docs/language-support.md.

The calibration is unchanged, and that was verified rather than assumed. Only 3 of the 40 pinned corpus repositories contain any C at all (27, 16 and 2 files in trees of 4008, 3085 and 878), all three are analyzer-primary, and re-deriving the constants with C included moves every reference and CALIBRATION_C by less than 0.0001. No repository is re-graded by this release for a language it does not contain.

Changed

Together these are the reason 1.1.0 is a structural release and not just a scanner: C++ and C# are now additive rather than another argument with a file-length gate.

Fixed

1.0.1 - 2026-09-01

A housekeeping release: no runtime behavior changed (the reconfigure and TypeScript-semantic fixes shipped in 1.0.0). This ships the release-pipeline fix and internal headroom, and refreshes the docs.

Changed

Documentation

1.0.0 - 2026-09-01

The 1.0 milestone. Not a rewrite — a line drawn under a long arc of subtraction. 1.0 is what remained after every claim this project could not stand behind was removed: the scoring engine that graded repo size was rebuilt into per-dimension rates calibrated against a query-selected corpus (0.5.0); the headline claim that near-duplication distinguishes AI-written code was retracted when a matched control erased the gap (0.6.0); the score learned to be withheld when the evidence cannot support one, and external analyzers became the primary evidence with disagreement widening the range rather than being averaged (0.7.0); the surfaces converged on one setup, three transports with chat as the primary door (0.8–0.9); and a real-repo acceptance round (Phase 8, on a private repository) hardened the last edges. What ships as 1.0 is the thing the whole project is for: a deterministic, offline, one-rubric audit whose output is a bounded work order — a copy-paste prompt per finding that tells an agent to fix exactly these and refactor nothing else.

1.0 — the bounded work order, delivered

Fixed

Changed

0.9.1 - 2026-08-21

Security

Added

Fixed

Added

0.9.0 - 2026-08-19

Added

Changed

0.8.1 - 2026-08-16

Changed

0.8.0 - 2026-08-16

Pre-1.0 test release: everything merged since 0.7.0, packaged for field testing. The known defects of the primary (chat/MCP) surface are recorded in docs/defect-register-chat-surface.md (D1-D15) and are the next fix cycle; this release ships them disclosed, not resolved.

Added

Changed

0.7.0 - 2026-08-13

Scores the evidence cannot support are withheld. --fail-on-new no longer fires on code that only moved. Two incompatibilities: report schema version 3 (maintainability_estimate is nullable) and baseline format version 2. See docs/migration-0.7.md.

Added

Fixed

Changed

Recalibration, and a retraction. The reference corpus is now chosen by a query instead of by the author's taste, which moved every constant in the scale — read the table before comparing a score to one from 0.6.x.

Added — local MCP server

Changed — ADR 001 stage 8 (breaking report contract)

Stage 8 moved the report schema from 1 to 2. The four ambiguous compatibility score fields left with no aliases. 0.7 then moved the schema to 3 when the estimate became nullable; that is the version this release writes. Version 1 is still rejected. There is no migration.

Removed Replacement
score.overall score.maintainability_estimate
score.overall_range score.maintainability_range
score.grade nothing — score.verified_grade is the only letter a report carries
score.grade_blockers score.verified_grade_blockers

Added — ADR 001 stage 7 (consumer migration)

Fixed — ADR 001 Stage 6 closure

Retracted

Fixed — second hostile-audit response

A second adversarial audit probed the rubric within hours of it landing. Its stale claims are answered in the tracker; its live findings are all fixed here:

Fixed — third hostile-audit response

Fixed — sixth hostile-audit response

The fifth round's response claimed all five findings were closed. Two were not, and the sixth audit proved it. Both were cases of fixing the demonstrated instance instead of the class — the exact failure the previous entry congratulated itself for avoiding.

Fixed — fifth hostile-audit response

The fifth audit accepted the fourth round's corrections and then found that two of them had introduced new defects, plus three older claims still weaker than stated. Every finding was reproduced against the pushed snapshot before being fixed; all five were true.

Fixed — fourth hostile-audit response

The fourth audit examined the pushed snapshot itself and caught this changelog telling two direct falsehoods, both corrected in place above with the correction named rather than silently rewritten:

Measured — the bounded prompt, tested: INCONCLUSIVE on the registered rule

Measured — fix breadth: a direction that did not survive pinning

Added

Changed

dimension 0.6.x (14 hand-picked) now (40 queried)
file_size 0.0779 0.0576
declarations 0.0243 0.0599
duplication 1.4659 3.7350
risk 0.0546 0.0726
CALIBRATION_C 5.2754 3.5466

Duplication is 2.5x more lenient: identical code that scored 5.0x now scores 2.0x. The hand-picked corpus was almost entirely libraries, which are built for reuse and have had years of review pressure to remove repetition; sorting by stars also returns applications and tools (n8n, excalidraw, playwright, transformers), which carry far more. The queried corpus describes widely-used code rather than well-factored libraries — the more honest reference for a tool that grades arbitrary repositories, but the bar is now set by a population that includes application code. - excluded one repository on what it is, not on how it scored. 33-js-concepts cleared verification — an index.js plus thirty concept-demo test files is enough declarations to look like a codebase — and landed as the corpus outlier on duplication (38.8x the median) and file size (3x the next repo), because parallel teaching examples are supposed to repeat. It is excluded as a teaching repo, alongside the tutorials and courses the name filter already removed. The distinction matters: filtering a corpus by its own measurements manufactures whatever reference the filter was aimed at. Removing it moved c 3.5724 → 3.5466, which is what a median is for. - fix(gates): the file, function and duplicate-block gates are opt-in. Measured across the corpus these fired on every single repository — duplicate counts of 33 to 5,325 against a default max_duplicate_blocks of 20 — so --fail-on-gate failed everywhere out of the box. A gate that always fails is not a gate; it trains people to pass the flag and ignore the result, and it gave the gates dimension zero variance. fail_on_file_failures, fail_on_function_failures and fail_on_duplicate_blocks now default to false. The findings are still reported — only whether they block CI changed, and a repo opts in to that. - the gates reference is now fixed at 0.05 rather than corpus-derived. Hard gates are discrete policy breaches a repository opts into, not a rate drawn from a population. Once gating became opt-in the corpus median went to zero, and dividing by zero would have made the dimension silently ignore real failures. One gate failure now reads as 1.0x. scoring._relative reports 0.0 for any zero reference instead of dividing. - fix(config): vendored third-party code is excluded by default. vendor/, third_party/, *.min.js and friends. Auditing vendored code measures someone else's decisions, and — worse for a reference corpus — constants drawn from a population containing it describe bundles rather than maintained source. lodash's corpus entry was 41% vendored. - fix(metrics): the README gate accepts any README. It required README.md exactly, so Django — which ships README.rst — was reported as having none. That is the class of finding that teaches people the tool does not know what it is looking at.

0.6.1 - 2026-08-08

Release plumbing and a performance consolidation. No behaviour changes.

0.6.0 - 2026-08-08

Adds four Tier-1 signals aimed directly at the "AI writes code humans cannot maintain" criticism. Reported as findings; deliberately not yet scored. No breaking changes.

0.5.0 - 2026-08-07

The scoring engine was rebuilt after being measured against real code and found to be wrong. Config is unchanged; the shape of report["score"] gains fields.

0.4.0 - 2026-08-06

Bug-fix release for TypeScript/JavaScript function detection, plus two threshold corrections. Config gains two optional keys; no breaking changes.

0.3.0 - 2026-05-23

Bug-fix release. Three independent false-positive sources observed during a downstream audit are eliminated; no breaking config changes.

0.2.0 - 2026-05-12

0.1.0 - 2026-05-11