Repository navigation
fix: metric value semantics, MI formula, and tool-bound doc comments - #1650
Merged
Merged
Conversation
The Python metrics= kwarg took only the 13 family names, while bca metrics --metrics also takes the list-metrics row names (sloc, dloc, ...) and the dotted threshold ids (cyclomatic.modified), so a name copied from the CLI raised ValueError in Python. Move the CLI's resolution into the library as metric_catalog::resolve_selector, with selector_names() for the advertised vocabulary and UnknownSelectorError carrying the input and the did-you-mean candidates. Both front ends now resolve through it; selection stays family-granular, so metrics=["dloc"] returns the whole loc block, as --metrics dloc does. The edit-distance suggester moves from the CLI into the library with its unit tests. The CLI reaches it through one #[doc(hidden)] format_suggestion re-export, and its --metrics error text is unchanged byte for byte (pinned by a new unit test). Python's unknown-name error keeps its "unknown metric: <name>" prefix and now ends with the CLI's hint and known-name list instead of "; valid: <families>". Docs, including the analyze() docstring, gain the alias vocabulary and the nom dependency of cognitive, nargs and nexits. Fixes #1614
The catalog recorded only a metric's direction, so every surface that
asks "breached, how much worse, how much changed" re-derived the
answer with ratio or percentage arithmetic. That assumes a true zero.
It holds for counts and fails for the Maintainability Index family,
which is interval-scale: mi.original and mi.sei have an arbitrary
zero and go negative, and mi.visual_studio is clamped to [0, 100]. A
0.0107-point MI drop rendered as "+1069%".
Add the value-semantics layer the callers will migrate onto:
- Scale { Ratio, Interval { min, max, unit } } on MetricInfo, with a
scale(id) lookup. The three mi.* rows are Interval; one Visual
Studio band (10 points) is the unit, 17.1 on the 171-point scale.
- MetricInfo::cmp_worse, a direction-oriented total order with NaN
sorting worst.
- Threshold, a validated limit. Threshold::check rejects an unknown
id, a non-finite limit and a negative limit on a non-interval
metric, and accepts an out-of-domain or unfireable interval limit
with a warning, since 2.x already parses those. It returns
Checked { threshold, warning }; Threshold::warning re-asks a
threshold in hand for the same advisory. Threshold provides
breaches, excess, one severity key (bitwise value / limit for
counts, saturating at a zero limit, 1 + excess / unit for interval
metrics), cmp_severity and cmp_breach, soft_limit with its
is_soft_ratio band rule, change (percent or points) and drift.
No caller is migrated here; the CLI, Code Climate and the Python
binding move onto Threshold in later commits.
Refs #1621
mi.original and mi.sei are unclamped and fall below zero on large functions, but the baseline reader and writer both dropped every negative value on the premise that extractors never emit one. A refreshed baseline therefore never covered those offenders: they reported [new] on every run and the suggested refresh could not quiet them. Both sides now go through one `recordable` helper that accepts every finite value and refuses only NaN and infinity, which cannot ratchet. A negative zero is written as 0.0 so the file never spells -0.0; the two compare equal, so no classification changes. The regression tag is now rendered from Threshold::change. Counts keep [regr +N%] byte for byte. The mi.* family reads in index points, [regr +N.N pts]: MI has an arbitrary zero, so a percentage of its record means nothing. Dividing by a signed record printed [regr +-100%] for a drop from -10 to -20, and a 0.01-point drop from a record of 0.001 read [regr +1069%]; they now read +10.0 pts and +0.0 pts. A violation that cannot form a Threshold keeps the old arithmetic. Also correct the comments that claimed values are never negative and that toml rejects nan/inf (it parses both), and the relative_drift doc, whose [0, 1] bound no longer holds once a record can cross zero. The --exit-codes help and the book name the tag shape-neutrally. Fixes #1611
Every surface that read a limit re-derived "breached, how badly, which is worse" with ratio arithmetic, which assumes a true zero. That fails for the interval-scale Maintainability Index. The CLI ranked any MI at or below zero as infinitely bad and broke the tie on the milder value, Code Climate banded the same breach as the mildest, the baseline tie-break preferred the stricter record for mi.*, and the soft tier divided an MI floor, which would loosen a negative one. Move them all onto the catalog's Threshold at once, since negative MI limits become legal here and a half-migrated tree would let the CLI and Code Climate disagree again: - Violation carries a Threshold instead of limit + lower_is_worse. Every layer validates through Threshold::check. mi.original and mi.sei floors may be negative; a mi.visual_studio limit that cannot fire or fires on everything still parses and warns once. The advisories are read off the resolved gate (ThresholdSet::gated_thresholds), so under --tier=soft they judge the raised floor that actually gates, not the configured one. - Ranking (footer, step-summary rollup, top-offenders table) uses Threshold::cmp_breach. The table heading now says "by severity". - Code Climate bands Threshold::severity with the same cutoffs; a zero-limit breach is blocker; unknown or non-finite records keep the declared floor. - The soft tier calls soft_limit: an MI floor rises by (1 - r) of one maintainability band, rounded up off the sig-fig grid. A limit the ratio does not move is used as written, so ratio 1 is the identity STABILITY.md promises, and a rounded one is held on the hard limit's side in the tier's direction. Count rounding is unchanged. --explain-threshold names the additive step the resolver took, and says "clamped" when the domain edge cut it short. - The baseline ratchet, stale tally and diff-baseline buckets use cmp_worse and drift; an equidistant tie keeps the more lenient record in the metric's direction. - --min-change rejects a non-finite value: inf hid every delta, and NaN and -inf reported every one. A negative one, which releases before 2.4 accepted, still reports every delta and now warns that it reads as 0. check --help says "breaches" rather than "at or over". Fixes #1617 Fixes #1616 Fixes #1620
Move the last direction-bool consumer, the Python binding's to_sarif(thresholds=), onto metric_catalog::Threshold. It copied the CLI's old rules: any negative limit was rejected, so no mi.original or mi.sei floor could sit under a large function's value, and the breach test was open-coded. Limits now validate and breach exactly as bca check does. A negative MI floor is accepted, and a limit that can never fire (mi.visual_studio=0) raises a UserWarning carrying the library's message. With no caller left, deprecate metric_catalog::lower_is_worse (kept working until 3.0) and ban it in a root clippy.toml, so a new direction encoding cannot creep back in. The CLI's test-only Violation::lower_is_worse goes too; its tests read the threshold's direction instead. Refs #1621
A space with no code to measure (zero Halstead volume or size term)
reports MI 0.0, an in-band sentinel. bca report folded it into its
SLOC-weighted average anyway, numerator and denominator alike, so an
count: arc.h alone read 11.9 (MODERATE), arc.h plus fstlib.h 8.4
(LOW).
Expose the predicate as mi::Stats::is_empty() (additive; the private
inputs_are_empty now calls it), project it into FunctionSummary, and
fold every report MI average through one MiAverage accumulator that
skips empty units in both its numerator and its own SLOC. All four
sites use it: the Markdown summary row and language overview, and the
HTML overview row and per-language summary. Displayed SLOC, file
counts and the comment ratio still count the empty file. The MI
hotspot keep reads the same flag instead of a hand-copied predicate.
A language whose every unit is empty has no average at all: the
summary reads n/a rather than rating a 0.0 LOW, and the overview
leaves its Avg MI cell empty, which the HTML sorter reads as no
value.
The mi.* gate skips such a space too. A bodiless Ruby block
(`lambda {}`, `[1, 2].each { }`) is a function space with zero
volume, and bca check reported its placeholder as the worst MI in
the file, banded critical in Code Climate. bca check and the Python
to_sarif(thresholds=) now leave it out, as bca report does.
mi_rating takes its 10/20 edges from mi.visual_studio's catalog unit,
so the report's bands and the gate's MI scale are one number.
The serialized MI of an empty space stays 0.0 in 2.x; emitting null
is deferred to 3.0.
Refs #1621
The HTML report carried a line-for-line copy of the Markdown per-language overview row, and each report summed a language's units and formed the comment ratio on its own, the Markdown global totals a third time. One copy of each now lives in markdown_report: lang_overview_row, LanguageTotals and comment_ratio_percent, so the two reports cannot drift apart. The rendered output is byte-identical; every sum is added in the same order as before.
Consolidate the [Unreleased] entries for the five commits that give the catalog a Scale and a validated Threshold and move the CLI gate, the baseline, Code Climate, the Python bindings and the report averages onto it: the additions, the changed limit and soft-tier rules, the deprecation of lower_is_worse, the MI null-on-the-wire change deferred to 3.0, and the five fixes it folds in. Fixes #1621
Three changes to the maintainability index, all in Mi::compute and Stats::sei: - Every variant sizes a space on ploc, the rows that carry code. Comment, documentation and blank rows no longer enter the size term, in either documentation mode; a row shared by documentation and code stays in. - mi.sei takes the comment share as a 0-1 ratio rather than a 0-100 percentage. On the percentage scale 50*sin(sqrt(2.4*CM)) swung between -50 and +50 several times over ordinary densities; on the ratio it rises monotonically to about 50, so sei >= original, with equality when a space has no comment. Blank rows stay in the denominator, so they still dilute a live comment share. - mi.sei uses ln, not log2, for volume and size; it is now original plus the comment term. mi_charges_docs therefore moves only mi.sei's comment share and can only raise it; original and visual_studio are identical in both modes. Every mi.* value moves for unchanged code, so a written baseline now stamps [provenance] mi_formula = 2. An unstamped file reads as revision 1, and bca check --baseline warns when a file holding any mi.* entry records another revision, in place of the mode warning; bca diff-baseline warns the same way when its two files disagree and either holds an mi.* entry. The mode warning itself now compares only mi.sei, the one metric the mode moves. No baseline version bump, so an older bca still reads the file but cannot know the stamp: STABILITY.md says to pin bca >= 2.4 wherever a 2.4-written baseline is read. visual_studio() is now original() rescaled rather than a second copy of the formula. Fixes #1607
Checker::holds_doc_comment_in_body carried a comment and the source only for Rust's inner-doc exception, so every other override bound them as unused parameters and the hook's contract depended on depth (the comment was passed for the two nearest ancestors, None above). The hook is now (ancestor, ancestors, below), and the exception moves to a dedicated Checker::doc_comment_opens_body, false by default, which documents_where_it_sits asks of the two nearest ancestors only. Both the //! and the /*! arm reach it through that one call. python_documentation_string_run walked siblings_before twice: once for the run's first string and once, inside python_is_documentation_string, for the statement before the run. One backward scan now finds both, since the first non-string, non-comment sibling ends the run and is the statement documented_by tests. No metric moves: loc JSON over the corpora and this tree is byte-identical between the pre- and post-change binaries. The baseline is refreshed for checker.rs's file-level loc and drops the six nargs entries the old hook signature held. Fixes #1615
CPython drops the parentheses around an expression, so ("""doc""")
as a body's first statement is its docstring. Every bare-string test
read the string's parent as the statement, and a
parenthesized_expression in between made the string, at any nesting
depth, a code row and an operand, and kept it out of dloc.
The bare-string predicates now climb through parenthesized_expression
to the statement, and the docstring-run scan unwraps a statement's
value the same way, skipping comments inside the parentheses. Both
walks stop at CPython's 200-level paren limit. The ( and ) rows are
billed as the string's comment rows, and as dloc only when the string
is documentation, so dloc stays within cloc. Halstead drops the ( as
it drops the string. A tuple, a parenthesized value and (f"x") keep
their old roles.
Every parenthesis of such a statement asks whether it wraps the
string, and answering by a climb and an unwrap per parenthesis made
a statement d deep cost O(d^2): 3.8 s for 80 KB at depth 200. A
walk that lends a FoldCache, now including the bca ops walk, reads
the answer off a memoised fold of the ancestry instead. Probes
loc/python-paren-bare-strings and ops/python-paren-bare-strings
hold it under the cap that halstead/python-nested-paren cannot
see.
Fixes #1610
The in-body test for a doc comment scanned the children of the function that holds it, past every other comment there, so the LOC walk was quadratic in doc comments in one C# accessor list or one PHP or Kotlin signature (exponent 1.93-1.98 on the new probes). Each hook now decides from the node on the comment's path: a C# property or indexer holds every comment but those in its accessor list, a PHP body is the compound_statement or an arrow function's last child, and a Kotlin body is the function_body or block child. csharp_member_has_accessors stops at the first accessor. The probes loc/csharp-accessor-docs, loc/php-signature-docs and loc/kotlin-signature-docs now fit 0.95-0.98. loc JSON over every C#, PHP, Kotlin, Java and Groovy file in the corpora (247 files) is byte-identical. One placement moves: a C# doc comment in the header or initializer of a property with accessors was dloc and is now a plain comment, as Roslyn reads it and as the same comment after a method's attribute already was. The baseline is refreshed for checker.rs's file-level loc. Fixes #1612
tree-sitter-perl holds everything after __END__ or __DATA__ as one data_not_for_compiler leaf, which the Perl LOC walk did not bill, so POD in the h2xs layout fell through to blank while the same POD before the marker was cloc and dloc. A one-pass scan of the leaf's bytes now reads POD blocks as Pod::Simple does (^=[a-zA-Z][a-zA-Z0-9]* opens, ^=cut closes, EOF ends an open block) and bills them as comment rows, and as documentation unless pod_comment_region says the block is only comment regions. Every other non-blank row is PLOC; blank rows stay blank. A bench-scaling probe, loc/perl-data-pod, fits 1.01. Splitting the first match arm removes every rustfmt bail in the file, so its rustfmt-bail baseline entry is dropped. The new arm moves the baselined halstead.effort of PerlCode::compute; the baseline is refreshed. Fixes #1608
A marker doc comment outside a function body counted as dloc wherever it sat, so a Javadoc licence header above `package`, a `///` above a macro call or an `extern` block, and the superseded first of two Javadocs all inflated dloc and the MI comment share. Rust, Java, Kotlin and Groovy now count a doc comment only when their own tool attaches it to the declaration after it, read from each tool's source and confirmed by running it: - Rust (rustc, rustdoc): every outer doc comment above an item, across blank rows, plain comments and attributes. An extern block, a macro call, a statement or a generic parameter takes none. `//!` and doc comments in macro token trees keep the old reading. - Java (javac `Token::docComment`, `-Xlint:dangling-doc-comments`): the last doc comment before a declaration's first token; consecutive `///` lines are one. The package takes one only in a file with no type after it, the shape of package-info.java. - Kotlin (`PrecedingDocCommentsBinder`): the last KDoc before a declaration; a KDoc inside its signature stays as #1612 decided, and one nested deeper, in an argument or a type parameter list, documents nothing, as javac's does. - Groovy (`GroovydocManager`): only the comment nearest the declaration, read past block comments the lexer hides. The choice is made once per stretch of comments, at its last member, which walks back over the stretch: O(k) for k members, held by the new loc/java-dangling-docs and loc/rust-attributed-docs probes. No committed corpus snapshot moves: the corpus snapshots cover C++, JavaScript, Rust (serde), C# and PHP only, and every serde doc comment is attached. Submodule unchanged at f34ec062. Refs #1613
C#, PHP and the JS family counted a doc comment as dloc wherever it sat outside a function body, so a /** */ above a call, a licence header above a plain comment, and a doc comment above a namespace or a `using` inflated dloc and the MI comment share. Each now counts only what its own tool attaches, read from the tool's source and confirmed by running it: - C# (Roslyn GetDocumentationCommentTriviaFromSyntaxNode, dotnet build): the nearest run of doc comments only blank rows separate, read back past plain comments and directives. A namespace, a `using`, a statement and an accessor take none (CS1587), which reverses #1612's accessor-list reading. - PHP (phpDocumentor 3): the last DocBlock before an element, and phpDocumentor's file DocBlock, which documents the file. - JavaScript (JSDoc 4): the nearest block comment before a declaration or an assignment; a virtual comment (@file, @module, a naming tag) stands alone. @license does not. - TypeScript and TSX (tsc, TypeDoc): the last /** before a declaration, past every plain comment; none after a decorator. @module and @packageDocumentation document the file. A new KeptDocs::Leading models the reverse trivia scan Roslyn, JSDoc and tsc share. In-body JSDoc stays documentation. Probes loc/csharp-dangling-docs, loc/php-file-docblock and loc/js-standalone-tags hold the decision at O(k). Three pdf.js corpus snapshots move, on loc.dloc* and mi.sei only, each matching jsdoc -X: submodule f34ec062 -> 9076c554. Refs #1613
C, C++, Mozilla C++ and Objective-C counted a Doxygen block as dloc wherever it sat outside a function body, so a block above the end of a file, a closing brace or a forward declaration inflated dloc and the MI comment share. Each now counts what Doxygen 1.13.2 documents, read from commentscan.l and confirmed by running it with EXTRACT_ALL=NO over ~200 fixtures: - Doxygen keeps every doc block before an entry and concatenates them: a declaration, definition, macro, typedef, alias, namespace, template, enumerator, macro call, or Objective-C container, property or method. It reads past plain comments, #include, #import, #pragma, an empty `;` and an empty conditional, and into a conditional that declares something. - A block above a forward declaration, `}`, `@end`, `@synthesize`, an explicit instantiation or the end of the file documents nothing. - A structural block (\file, \defgroup, \page, \name, or a naming command with a name: \class X, \def M) stands alone and counts wherever it sits; the doc blocks above a \file block count with it. - Member-after /**< blocks and in-body blocks keep counting (#1609). A new DocBinding::gathers models the \file case for a stretch that ends at no target. The rule lives in lang_helpers/doxygen.rs and one macro the four Checker impls share. Probes loc/c-dangling-docs and loc/cpp-structural-docs hold it at O(k). Two rules the other stretch bindings share change with it. A stretch reads past a parse error that is no doc target and holds none, as Doxygen's binding already does, so the error tree-sitter-rust leaves for a Rust 2024 `safe fn` no longer drops its doc comment; an error holding a declaration, such as a Kotlin class-body context(...), stands in for it. And KeptDocs::Leading asks whether a doc comment after code on its row still documents: tsc's leading-comment scan starts at the line break after the token before, so in TS and TSX one does only before a parameter, while Roslyn, JSDoc and PHP-Parser read it. No committed corpus snapshot moves: every C++ doc block in DeepSpeech native_client is attached. Submodule unchanged at 9076c554. Fixes #1613
Consolidate the [Unreleased] entries for the six LOC commits: the parenthesized Python docstring (#1610), Perl POD after __END__ (#1608), the linear doc-placement test and the C# header-comment reading (#1612), and the three-part doc-comment attachment rule with its per-tool behaviour and its probes (#1613). The #1615 refactor has no user-visible change; its entry was a rewording of the #1597 hook names made in place.
Lesson 99: a bench-scaling probe guards only the axis it grows, so a walk that descends needs a depth probe — the #1613 nested-conditional walk was O(depth²) under three green gates whose probes grew width. Lesson 100: a float tolerance is measured in the value's own ulps, never in f64::EPSILON, and a rounded limit is clamped to the hard limit's side — the soft-tier snap of #1621 was wrong twice before the clamp made the invariant hold regardless of the tolerance. Lesson 98 gains the #1613 sub-example: premises read from a tool's source and refuted only by running the tool. The interrupted-sweep case goes to .claude/rules/testing.md as a rule rather than a lesson: the driver writes its backup to disk before the first edit, and recovery is cmp against it. Refs #1613, #1621
This was referenced Oct 20, 2026
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #1650 +/- ##
==========================================
+ Coverage 98.22% 98.26% +0.03%
==========================================
Files 367 371 +4
Lines 104922 108453 +3531
Branches 104487 108018 +3531
==========================================
+ Hits 103055 106566 +3511
- Misses 1205 1211 +6
- Partials 662 676 +14
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
RUSTSEC-2026-0383 (webpki-root-certs < 1.0.10 bundles the emSign ECC Root CA - G3, whose distrusted intermediates' private keys are now public) and RUSTSEC-2026-0382 (rustls-webpki < 0.103.16 may accept a path through those intermediates). Both reach the workspace only through the dev-dependency chain jsonschema → reqwest → rustls, so no shipped binary changes. `cargo deny check` is clean again.
Codecov listed the expression-bodied arrow arm of the JS family's
holds_doc_comment_in_body, the InternalModule arm of tsdoc_target and
DocBinding::new as never run. Rows for a doc comment in an
expression-bodied arrow (JSDoc attaches it to nothing), in an arrow's
object literal (a member JSDoc documents, so a doc row where it sits)
and above a top-level `namespace N {}` (tsc's ModuleDeclaration carries
it), each verified against jsdoc 4 / tsc 5.9 and by deleting the
comment from the fixture. DocBinding::new is a const fn every language
evaluates at compile time, so one runtime call pins its defaults.
Refs #1613
This branch was successfully deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Two threads, 18 commits, each gate-clean on its own.
Metric value semantics (#1621 and its reports). Every
mi.*decision that used to be a scatteredlower_is_worse/starts_with("mi")branch now has one owner: a validatedmetric_catalog::Thresholdwith aScale { Ratio, Interval { min, max, unit } }on each catalog row. The CLI gate, baseline, Code Climate and SARIF writers, the Pythonto_sarif, and the reports all consume it. Negativemi.*baseline values round-trip, an MI regression reads in points ([regr +10.0 pts]) rather than a meaningless percentage, one severity key ranks mixed-metric offenders, the soft tier is additive for interval metrics and clamped so it is never looser than the hard tier, and a space with no code to measure is skipped by the gate as the report already skipped it.lower_is_worseis#[deprecated]and banned inclippy.toml. (#1611, #1614, #1616, #1617, #1620, #1621)MI formula (#1607). All three
mi.*variants size onploc;mi.seiisoriginal + 50·sin(√(2.4·CM))with CM the 0–1 comment ratio andlnthroughout.--write-baselinestampsmi_formula = 2; a baseline from another revision warns on every read. This movesmi.*for any space with comments or blank rows — reviewmi.*thresholds.Doc-comment attachment (#1608, #1610, #1612, #1613, #1615).
dlocnow counts the rows each language's documentation tool actually attaches, with every rule read from the tool's source and, where a toolchain could run, confirmed by running it: rustdoc, javac, Kotlin'sfindDocComment, Groovydoc, Roslyn, phpDocumentor + Zend, JSDoc, tsc, Doxygen (C/C++/ObjC/mozcpp), Pod::Simple. The mechanism is one declarativeDocBinding/KeptDocsper language and one generic stretch walker, memoised throughFoldLaneso each comment costsO(1);make bench-scalingpins the exponent on 16 new probes. Perl POD after__END__counts; a parenthesized Python docstring is one. The 23 deliberate tool-vs-bca divergences that remain are tracked in #1646.Closes
Fixes #1607, #1608, #1610, #1611, #1612, #1613, #1614, #1615, #1616, #1617, #1620, #1621.
Filed, not fixed: #1622, #1623, #1624, #1643, #1644, #1645, #1646, #1648, #1649.
Review guide
The two threads are independent; review them as two series. Commit order within each is dependency order.
Decisions worth a reviewer's eye, each recorded in the CHANGELOG and on its issue:
Threshold::checkreturnsChecked { threshold, warning }; a no-op or out-of-domain limit (mi.visual_studio=101) warns in 2.x rather than rejecting. Negativemi.*limits are legal.dloc1 was an accident ofis_func.ERRORcounts only when the error holds an entry; a stray}or@enddrops it. Residuals in fix(loc): tool-vs-bca doc-comment attachment divergences left by #1613 #1646.mi::Stats::is_empty()is the one predicate for "no code to measure"; the wire keeps0.0until 3.0 ((breaking)-deferred entry in the CHANGELOG).doc_comment.rsis one file for walker and classifiers (refactor(ast): split doc_comment.rs into the binding walker and the text classifiers #1649) — both filed from the architectural review, neither blocking.Verification
make pre-commit→BCA_GATE: pass; per-commit clippy-D warnings+ targeted tests sweep, 18/18.make bench-scaling(74 probes),make chain-audit,make check-test-lang-gates-compare COMPARE_REF=origin/mainall clean.big-code-analysis-outputat9076c554(two commits, pushed); gitlink bumped in the commits that moved metrics.Public API (STABILITY.md updated)
Additive, minor bump:
metric_catalog::{Threshold, Checked, LimitError, LimitWarning, Change, Scale},MetricInfo::scale,mi::Stats::is_empty,wire::Loc::dloc,MetricsOptions::with_mi_charges_docs, Pythonmetrics=accepts CLI names. Deprecated:metric_catalog::lower_is_worse. No removals.