Skip to content

fix(halstead): vocabulary and decision batch 2026-10-10 - #1605

Merged
dekobon merged 34 commits into
mainfrom
fix/batch-2026-10-10
Oct 15, 2026
Merged

dekobon merged 34 commits into
mainfrom
fix/batch-2026-10-10

Conversation

@dekobon

@dekobon dekobon commented Oct 15, 2026

Copy link
Copy Markdown
Owner

Summary

Halstead vocabulary and decision-counting batch: 28 signed commits, 16 issues fixed, one closed as won't-fix (#1583). Every fix went through investigate → simplify-rust → rust-optimize → review → audit-tests, then the branch had four cross-fix review passes (two whole-branch review runs, a max /code-review --fix, and a Fable-model gap review), each of which found something the earlier ones had not.

Halstead vocabulary

Decisions and walks

Gates

make pre-commit passes; 6,001 tests; 49/49 bench-scaling probes; chain-audit and fuzz-check green. Patch coverage 98.3% against a project figure of 96.76%, CI method on a clean target. Snapshot submodule at 3c754c5a (pdf.js, serde, one C# file, 884 DeepSpeech files; every moved value is halstead.* or mi.*, checked field by field).

Label

gate-narrowing-intended: tests/parity/main.rs deliberately drops feature = "kotlin" from the abc_halstead_bracket_parity mod gate, because the Kotlin super<A> row moved to the <> family (#1567). The compare gate did not previously see root mod gates at all; ec2feb72 fixes that, so it now reports this narrowing, correctly.

Follow-ups filed from the reviews

#1598 C-family kind table · #1599 is_primitive hook · #1600 import static vs C#'s using · #1601 non-optional SiblingCache lending · #1602 cognitive owner-list residual · #1603 fabricated chain in abc/perl.rs · #1604 -bareword outside key position.

Closes #1563, closes #1564, closes #1565, closes #1566, closes #1567, closes #1568, closes #1569, closes #1572, closes #1573, closes #1574, closes #1575, closes #1578, closes #1579, closes #1580, closes #1581, closes #1582, closes #1584, closes #1585.

TypeScript billed no class-member modifier but `declare`, while C# and
Kotlin bill theirs. Each modifier keyword leaf is now an operator in
the TS and TSX branch: `public`, `private`, `protected`, `override`,
`readonly`, `abstract` and `accessor`. The `accessibility_modifier` /
`override_modifier` wrappers stay unlisted, so each bills once. Every
word has one kind, so no alias is missed. The leaves bill outside a
class as well: parameter properties, `readonly T[]`, a mapped type's
`-readonly`, `abstract new () => T`.

`static` joins the shared JS-family arm, so JavaScript and MozJS bill
it too, in fields, methods and `static {}` blocks. Their one-token
`static get` (getter name on the next line) bills as one operator.

Used as names (`let readonly = 1`, `obj.static`, `{ public: 1 }`, a
field named `static`) the words parse as identifiers and stay
operands. The pinned TS grammar misreads a few valid name spellings as
the keyword (`abstract();` as a statement, a field `accessor = 1`, an
object-type member `abstract: T`); those now bill the operator where
they billed nothing, recorded in the getter comment. None occurs in
the corpora.

Snapshots: 60 pdf.js files move in halstead.* and mi.* only, operand
counts unchanged. Submodule 0708c3d9.

Fixes #1563
Kotlin billed none of `typealias`, `init`, `where`, `by`, `get` or
`set`, while it bills its other declaration keywords and every
modifier. Each is now an operator, as C# bills `get` / `set` / `init` /
`where` and TypeScript `type`. Each is the keyword leaf; the
`type_alias`, `anonymous_initializer`, `type_constraints`,
`property_delegate` / `explicit_delegation`, `getter` and `setter`
wrappers stay unlisted, so each bills once. Every word has one kind.

`get` and `set` bill only directly under `getter` / `setter`: the same
leaves are the `@get:` / `@set:` use-site targets, which bill nothing
like `@field:`, and kotlin-ng strands a `get(1)` call after a local
`val f = 6` in an ERROR. `by`, `where` and `init` used as names parse
as identifiers, and `typealias` is a hard keyword, so those four need
no guard. The accessor body's `field` is an identifier and stays an
operand.

No corpus snapshot moves: the one Kotlin corpus file uses none of the
words.

Fixes #1564
Java and Groovy billed `abstract`, `final`, `transient`,
`synchronized` and `default` but no other modifier, while C# and
Kotlin bill all of theirs. Each modifier keyword leaf is now an
operator in both: `public`, `private`, `protected`, `static`,
`native`, `strictfp`, `volatile`, `sealed` and `non-sealed`, plus the
`permits` clause keyword beside the already-billed `extends` and
`implements`. Java's `modifiers` wrapper stays unlisted and Groovy has
none, so each modifier bills once; an annotation inside `modifiers`
is unchanged. No word has a numeric-suffix alias. Java's `default` is
one kind for the switch label and the interface-method modifier and
already billed.

The `static` of `import static` and Java's `requires static` bills
nothing, as `import` and `requires` bill nothing. Java parses
`sealed` and `permits` used as names as identifiers, so they stay
operands. Groovy fails a statement that opens with one (`sealed = 1`)
into an ERROR holding the keyword, so its `sealed` and `permits` bill
only under their declaration; the gate joins the existing `trait` /
`record` one in a single (keyword, owner) table.

No corpus snapshot moves: the DeepSpeech corpus globs only C++, and
no Java or Groovy corpus is snapshotted.

Fixes #1565
A Java class literal billed its whole text as one operand while the
`.`, `[]` and primitive inside it also billed as operators, and the
`class` tail and type names were suppressed: `int.class` paid `int`
and `.` on top of the `int.class` operand that already spelled them.

JLS 15.8.2 defines the class literal as a primary expression, not a
literal (3.10 lists none), so its tokens are its vocabulary. The
`class_literal` kind leaves the operand arm, the #1560 type-name guard
is deleted so the type bills as everywhere else, and the `class` tail
becomes an operand, as Groovy's `Foo.class` and Kotlin's and PHP's
`Foo::class` bill theirs. The declaration keyword stays an operator.

`java.util.List.class` now bills three `.` and operands `java`,
`util`, `List`, `class`; `Foo[].class` bills `[]`, `.`, `Foo`,
`class`; `int.class` / `void.class` bill `int` / `void`, `.` and
`class`. The shared shapes are asserted against Groovy from one table.
No Java corpus file is snapshotted, so no integration snapshot moves.

Fixes #1566
#1559 billed a generic list's angle brackets as one `<>` pair. Three
bracket pairs that enclose types but are not list nodes still billed
the comparison `<` and `>`: a Rust qualified path's bracketed_type
(`<T as Tr>::f`, `<T>::f`), a C# function_pointer_type
(`delegate*<int, void>`) and a Kotlin super_expression (`super<A>`).
Each parent holds `<` / `>` only as its own brackets, so adding it to
the grammar's GENERIC_ANGLES.lists cannot capture a comparison.

The rule is now "angle brackets that enclose types are one bracket
pair". Kotlin's row leaves the ABC/Halstead bracket parity test, which
excludes generic brackets by design; ABC's half stays pinned by
kotlin_super_type_argument_is_not_a_condition. Nine serde corpus
snapshots move in halstead.* and mi.* only.

Fixes #1567
Groovy billed `int`, `boolean`, `void` and the other primitive type
names as operands while Java bills them as operators, so identical
declarations scored differently by language. The dekobon grammar has
no primitive node: a declared type is a `type_identifier`, and a class
literal's receiver (`int.class`) or an uninitialised local (`int t`,
parsed as two expression statements) is a plain `identifier`.

`get_op_type_with_code` now bills either kind as an operator when its
text is one of Java's eight primitive keywords or `void`, and
`is_primitive` keys it by that text. Groovy reserves these words, so a
name can spell one only as a member selector after a `.`-family
operator or as a map or named-argument label; those stay operands.

Java's `boolean` is now listed as `boolean` rather than its node kind
`boolean_type`, so `bca ops` reports both languages alike. The shared
Java/Groovy class-literal table takes the `int.class`, `int[].class`
and `void.class` rows.

No corpus snapshot moves: no Java or Groovy corpus is snapshotted.

Fixes #1568
An interpolated string's alignment `,` and format `:` (`$"{x,8:F2}"`)
were billed as Halstead operators. Both delimit the field like the
`{` / `}` braces #1314 left unbilled, and Python's format-spec `:`
went the same way in #1486. Parent-guard COMMA on
interpolation_alignment_clause and COLON on
interpolation_format_clause; the same tokens elsewhere, including a
ternary, named argument or argument list inside the hole, keep
billing.

The alignment value still walks as an ordinary expression: `8` is an
operand and `{x,-w}` bills `-` and `w`, since the spec makes it a
constant_expression that may name a const.

The csharp/strings.cs corpus snapshot moves by its two `:F2` colons
(Halstead and MI only).

Fixes #1573
Re-derive the halstead.effort default after the Halstead vocabulary
changes (#1486-#1573) on a rebuilt 41-repository, 20-language corpus,
measured with this branch's release bca and, for comparison, v2.3.0.

At 50,000 the median language flags 2.20% of its functions (v2.3.0:
2.23%), inside the 1-3% sizing band, so the default stays. Update its
rationale (per-language p90 now 3k Ruby to 60k C), regenerate the
book's per-language halstead.effort column, correct the two book
paragraphs the new figures contradict, and record the corpus with
commit SHAs in docs/development/threshold_corpus.md so the next
re-derivation can reproduce it.

Fixes #1569
Correct the CHANGELOG's #1567 n1 claim and name Mozjs and its
one-token `static get` under #1563. In the book, add `default` to the
modifiers Java and Groovy billed before #1565, document the `static
get` newline spelling, and scope the procedural-language threshold
advice to cognitive/cyclomatic, since Tcl, Bash and Perl effort sits
near the default (AGENTS.md likewise).

State `Checker::is_primitive`'s precondition: it is sound only for a
node the getter already billed as an operator, which Groovy's
kind-only impl relies on. Note that invalid C# interpolations
(`$"{x,}"`, `$"{:F2}"`) bill their delimiter, untested by design.

Fix the Groovy #1565 test doc that still called `void` an operand,
add a Java-to-Groovy twin oracle for #1565, and compare the #1573
test's vocabulary as returned instead of re-sorting it. Re-wrap
ragged comments.
C, C++ and Mozcpp billed no storage class, function specifier or type
qualifier, while every other class language bills its modifiers. Each
keyword leaf is now an operator: static, extern, register, inline,
thread_local, const, volatile, restrict, _Atomic, constexpr, alignas,
_Noreturn and their vendor spellings (__inline, __restrict__,
__thread, ...), C's storage class auto, and C++'s virtual, mutable,
constinit, consteval, explicit, friend, the access labels, override,
final and the noexcept of an exception specification.

The storage_class_specifier, type_qualifier, virtual_specifier,
access_specifier, explicit_function_specifier and noexcept wrappers
stay unlisted, so each modifier bills once; a member-function trailer
const and a pointer's const each bill where they appear. None of the
words has a numeric-suffix alias. override and final used as names
parse as identifiers and stay operands. C++'s auto is a placeholder
type and stays unbilled; attributes, MS calling conventions and the
Gecko MOZ_* macros are unchanged.

The DeepSpeech corpus moves in 884 snapshot files, Halstead operator
counts and MI only: operands and structure are unchanged.

Fixes #1578
Objective-C billed none of C's storage classes, function specifiers or
type qualifiers, so a .m scored a C declaration below a .c or .mm. The
ObjC getter now bills C's list exactly as c.rs does (#1578), plus
ObjC's own modifiers as keyword leaves: the @public / @Private /
@Protected / @Package ivar labels, @optional / @required, the ARC
ownership qualifiers and __bridge casts, the nullability qualifiers,
__kindof, __covariant / __contravariant, _Complex, and the in / out /
inout / bycopy / byref / oneway method-type qualifiers.

A property attribute has no keyword kind: it is the first identifier
of a property_attribute, which now bills as an operator keyed by its
text; a getter= / setter= value stays an operand. The nullable
qualifier is text-keyed too, so it and the nullable attribute share
one n1 entry. Apple SDK macros (NS_INLINE, FOUNDATION_EXPORT,
IBOutlet, __unused, ...) stay unbilled, as Mozcpp's MOZ_* do.

No corpus file is Objective-C, so no snapshot moves.

Fixes #1579
tree-sitter-perl 1.1.2 parses every auto-quoted bareword key as a
call: `(name => 1)` and `new(name => $x)` scored an ABC branch per key,
and `(-name => 1)` / `$h{-name}` also billed a Halstead `-` operator
and the unsigned operand `name`. Perl quotes a word before `=>`, and
alone in a hash subscript, so each holds a string.

lang_helpers::perl::perl_bareword_is_key now decides every bareword
key shape through one key-position test, which #1545's file-test keys
share. `=>` quotes the word right before it whatever expression it
closes, so the test climbs while the key is its parent's last child:
`(1 - name => 2)` quotes `name` too. ABC's bareword-call arm and the
Halstead getter consult it; a signed key's `-` bills nothing and the
key is spelled `-name`. Package-qualified words, words with arguments
and bareword values keep their call. Cyclomatic and cognitive were
never affected.

Adds the abc/perl-bareword-keys width probe to the scaling gate.

Fixes #1572
Two spellings of one Elixir program scored differently in five places.

A bracketed keyword list in last position (`if(c, [do: 1, else: 2])`)
is the same argument as the bare `do:` form, but every section reader
only recognised a bare `keywords` node. One lookup now finds the
trailing keyword list in either spelling, and quoted keys (`"do":`),
for `if` / `unless` else, `with`'s else tier, `cond`'s free `true ->`
and the `for ... reduce:` head skip.

`Kernel.||(a, b)` and `Kernel.&&(a, b)` now score ABC conditions and a
cognitive boolean sequence through the predicate cyclomatic already
used, and a nested call no longer pays a slot as an operand. A user
module's `Foo.||` still scores nothing.

Cognitive scored the keyword forms' `else:` / `rescue:` / `catch:`
nothing; they now score as their blocks do, for `if`, `unless`, `try`
and the `def` family. A `with`'s `else` adds nothing in either form:
its clauses are the arms of a `case`, already paid by the container.

Fixes #1574
Seven probes for the walks #1273, #1536 and #1540 made quadratic, which
the complexity-class gate could not see: a Perl `||` chain and a chain
a misparsed comparison absorbed (the spine climb), a block of `&&`
statements (the stop parent's operator lookup), a file whose tail
recovers into a flat ERROR root (perl_binding_rewrites), a Bash
`[ … -o … ]` run (cognitive), a valid `(( a = … = 0 ))` and a
`[[ … = … && … ]]` run (ABC). Each fails at 1.91-2.06 with its pre-fix
walk restored and holds at 0.98-1.19 clean.

The ERROR-tail probe is the first whose shape must not parse;
RECOVERY_PROBES exempts it from the parse check and pins its ERROR
root. Its body carries only `=~`: a bareword or chain under a flat
ERROR is still quadratic today (#1584).

Fixes #1575
A seek by byte reaches a child in O(log n) only where the grammar
stores the list as a repeat; an ERROR that recovery built by skipping
tokens is one flat array, and no tree-sitter call reaches a child of
one in better than its index. Perl ABC and Halstead ask for a sibling
in such an ERROR once per child (the bareword and dash key tests, the
misparse operator lookup, the key-sign test), so a file whose tail
fails to parse was quadratic in the tail: 2.15 s at 16 000 copies of
`{ last,`. Kotlin's `Ancestors::previous_sibling` scanned a string's
children from its start, making `"$a $a ..."` quadratic on valid input
(11.5 s at 8 000 names).

The metric and ops walks now record the children of every node wider
than 64 as they push them, in a SiblingCache they lend through
Ancestors. Ancestors::siblings_after / siblings_before and
previous_sibling binary-search that array; a narrow or unrecorded
parent falls back to the seek, so every answer is unchanged. A debug
assertion fails any walk that lends a cache without recording a wide
node. Ancestors moves to node/ancestors.rs, keeping node.rs under its
ploc limit.

New probes halstead/perl-error-tail-keys and
halstead/kotlin-short-interpolations, and abc/perl-error-tail widened
to carry a bareword and a chain, all trip bench-scaling with the cache
ignored (2.10, 1.74, 2.02) and hold at ~1.0 with it.

Fixes #1584
tree-sitter-perl parses a call's name as a bareword call nested in
the call, and an auto-quoted hash key as a bareword call too, so
`bca find --type call` reported `foo(1)` twice and every `name =>`
and `$h{-name}` key once. ABC already skipped both through a gate
that needs the ancestor chain, which the byte-less `is_call` lacks.

The gate moves to lang_helpers::perl::perl_bareword_is_call, which
ABC and a new Checker::is_call_with_code share; the `call` filter
now calls the chain-carrying spelling, whose default forwards to
`is_call`. The bareword is kept, gated, rather than dropped: it is
the only node for `foo;`, `&foo;` and the wrapper-less `foo(1` of
error recovery. Perl's `call` set now equals its ABC branches.

The key test reads the token after each bareword, a seek that is
quadratic under a flat ERROR (#1584), so act_on_node now records
and lends a SiblingCache as the metric and ops walks do. The new
count/perl-error-tail-calls probe fits 0.88 with it and 1.89 with
the cache left unlent.

Fixes #1580
A `receive` is a `case` over the mailbox, but modified cyclomatic and
cognitive did not know the construct: a `receive` with any number of
clauses sat at the base of both, and nothing inside it was nested.
It now pays the container decision and +nesting as `case` does, in
the block, keyword and bracketed spellings.

Its `after` clause is the timeout, the path taken when no message
clause matched in time. The compiler accepts exactly one, its head is
a timeout rather than a pattern, and `try`'s `after` takes no clauses,
so it is `receive`'s default arm and is free in every metric, as a
`case`'s `_ ->` is. Standard cyclomatic and ABC counted it before. A
`receive` holding only an `after` is a timed wait and scores nothing.

Fixes #1581
`Kernel.and(a, b)` and `Kernel.or(a, b)` expand the same short-circuit
macros as `a and b` / `a or b`, but scored no decision in any metric.
#1574 routed `Kernel.&&` / `Kernel.||` through the remote-call
predicate; the word forms never reached it, because the grammar names
them with a plain `identifier` rather than an operator token.

The `Kernel` lookup now returns that identifier when its bytes spell
`and` or `or`, and one helper keys every spelling of a short-circuit
operation to its symbol, so cognitive treats `a or Kernel.||(b, c)` as
one sequence. Cyclomatic, ABC and cognitive each read the identifier
beside the token. `Kernel.not` and `Kernel.!` stay non-decisions, and
a user module's `Foo.and` stays an ordinary call.

Fixes #1582
`f(x, rescue: (e -> e))` counts its clause as a decision because the
section gate does not ask which call owns the pair. The compiler
rejects that argument for any function, so only a user macro can take
it, and its clauses mean whatever the macro makes of them. Gating on
the owner would move no valid program's score in a defensible
direction, so the gap is documented rather than fixed and is not
pinned by a test, per grammar-dispatch section 6.

Also repoint the paragraph's stale `elixir_is_section_pair` link.

Refs #1583
The `Kernel` operator lookup repeated the `and` / `or` byte list that
`elixir_short_circuit_name` owns; it now asks that helper, so the two
cannot drift. The book still listed `after:` among the keys whose
clauses score, which #1581 removed.
tree-sitter-perl 1.1.2 parses an auto-quoted hash key as a bareword
call (upstream #1571), so `(exit => 1, die => 2)` scored two exits its
quoted twin does not. ABC and `find --type call` already skip such a
key through perl_bareword_is_key, which reads the token after the word.

Exit::compute now takes the walk's Ancestors chain, as every other
metric's compute does, and the Perl impl asks the key test after its
text test matches. The chain carries the walk's SiblingCache, so a flat
ERROR tail of keys stays linear. Exit is pub(crate), so the widened
signature changes no public API.

Fixes #1585
B1: a `receive` now gets exactly one free arm: its bare `_ ->` if the
do-section has one, otherwise its `after` timeout. Beside a catch-all
the timeout is a third path, so `receive do {:a, y} -> y; _ -> 0
after 10 -> :t end` scores what the three-clause `case` does in
standard cyclomatic and ABC; modified and cognitive pay the container
only and are unchanged. New parity rows in all three spellings.

T2: `WIDE_PARENT` is `pub` + `#[doc(hidden)]` and the three wide-parent
fixtures size themselves from it instead of a copied 64. T3: note that
`act_on_node_records_a_wide_seeded_ancestor` discriminates only in a
debug build (lesson #80).

T5: `nexits/perl-error-tail-keys` scaling probe (1.11 clean, 1.87 with
`Ancestors::lookup` ignoring the cache), listed in benchmarking.md.

T4/D2: C++ and C both parse `int restrict = 0;` / `int noreturn = 1;`
as a qualifier before a zero-width name; documented as grammar-decided
in `getter/c.rs`. D1, D3, N1: doc, changelog and comment fixes.
Add lessons 96 (a tree-sitter child seek is O(log n) only through a
grammar repetition, #1584) and 97 (a before/after on a rebuilt corpus
measures the corpus, #1569), and extend 53 (fixed-depth ancestor
climb, #1574), 57 (reserved word folded into the name kind, #1568),
64 (two candidate default arms, #1581), 82 (reuse the caller's chain,
#1580) and 95 (a real token wrapped in an extra ERROR, #1572).
@dekobon dekobon added the gate-narrowing-intended check-test-lang-gates --compare: a test legitimately stopped being built by a language leg label Oct 15, 2026
@codecov

codecov Bot commented Oct 15, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.57605% with 22 lines in your changes missing coverage. Please review.
✅ Project coverage is 98.20%. Comparing base (9e98be3) to head (f3aedfc).
⚠️ Report is 34 commits behind head on main.

Files with missing lines Patch % Lines
src/metrics/elixir_decisions.rs 96.29% 0 Missing and 6 partials ⚠️
big-code-analysis-ast/src/checker.rs 88.23% 4 Missing ⚠️
big-code-analysis-ast/src/lang_helpers/elixir.rs 96.19% 0 Missing and 4 partials ⚠️
big-code-analysis-ast/src/node/ancestors.rs 98.38% 1 Missing and 3 partials ⚠️
src/metrics/abc/rust.rs 88.23% 0 Missing and 2 partials ⚠️
big-code-analysis-ast/src/lang_helpers/perl.rs 98.98% 0 Missing and 1 partial ⚠️
big-code-analysis-cli/src/formats.rs 96.77% 0 Missing and 1 partial ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #1605      +/-   ##
==========================================
+ Coverage   98.18%   98.20%   +0.01%     
==========================================
  Files         364      365       +1     
  Lines       99890   101068    +1178     
  Branches    99459   100637    +1178     
==========================================
+ Hits        98077    99250    +1173     
+ Misses       1182     1173       -9     
- Partials      631      645      +14     
Flag Coverage Δ
python 100.00% <ø> (ø)
rust 98.19% <98.57%> (+0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
big-code-analysis-ast/src/checker/groovy.rs 100.00% <100.00%> (ø)
big-code-analysis-ast/src/checker/objc.rs 90.00% <100.00%> (+0.25%) ⬆️
big-code-analysis-ast/src/checker/perl.rs 100.00% <100.00%> (+18.18%) ⬆️
big-code-analysis-ast/src/getter.rs 96.06% <ø> (ø)
big-code-analysis-ast/src/getter/c.rs 85.71% <100.00%> (ø)
big-code-analysis-ast/src/getter/cpp.rs 100.00% <ø> (ø)
big-code-analysis-ast/src/getter/csharp.rs 96.34% <100.00%> (+0.81%) ⬆️
big-code-analysis-ast/src/getter/generic_angles.rs 100.00% <ø> (ø)
big-code-analysis-ast/src/getter/groovy.rs 97.61% <100.00%> (+3.07%) ⬆️
big-code-analysis-ast/src/getter/java.rs 98.41% <100.00%> (-0.03%) ⬇️
... and 40 more

... and 9 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Bill the vendor spelling `__volatile__` (Volatile2) in the C, C++,
Mozcpp and ObjC modifier arms, so `__asm__ __volatile__` scores like
`asm volatile`. Read an Elixir keyword pair's section off the ancestor
chain instead of rescanning `arguments` per pair or clause, which was
O(P*K) on wide calls (5.4 s -> 0.06 s at 16k arguments), bound the
bracketing-list climb to one step, and share one section-holder
helper across cyclomatic, ABC and cognitive. Pin that Perl's
is_call_with_code disagrees with the byte-less is_call on a key or
wrapper callee, pin the `(-foo, => 1)` and comment-in-slice shapes,
and record the ast-crate API changes and the cognitive scope of #1574
in the changelog. Add the cognitive/elixir-mixed-arguments probe.
`--compare` diffed per-test membership from each item's own cfg and
its enclosing items', but never the cfg on the `mod` line that loads
the file. A narrowed root gate in a `tests/*/main.rs` therefore
dropped a whole suite from a single-language leg with nothing
reporting it: #1567's deliberate drop of `kotlin` from
`abc_halstead_bracket_parity` read as OK against main.

Each file now inherits the conjoined cfg of the `mod` declarations
that reach it, and `--compare` intersects test membership with it.
The non-compare gate is unchanged.
Re-measure the C, C++ and Objective-C halstead.effort rows at
d361bc3: #1578, #1579 and `__volatile__` landed after the 18c28ca
measurement and raise C-family effort. The median-language rate stays
2.20% (2.78% / 1.76% at 40k / 60k), so the default stands.

Move the `(-foo, => 1)` row out of the "keeps its reading" test into a
pin of the grammar misparse carrying FIXME(#1571), restructure the
book's Halstead bullets so the declaration-modifier rule is stated
once, and correct lesson 53, the #1574 and #1578 changelog entries,
the vendor-spelling list and the probe table order.
@dekobon
dekobon force-pushed the fix/batch-2026-10-10 branch from 39acb28 to fe76014 Compare October 15, 2026 21:47
Upgrade rustls to 0.23.46 and replace serde_yaml with serde-saphyr to remove unsafe-libyaml. Preserve YAML data and round-trip behavior with updated tests and explicit string/float coverage.
Remove obsolete timeout-clause lookup and carry the recognized argument list into ABC. Add boundary tests for short source buffers, root nodes, empty arguments and standalone keyword lists, preserving metric values while reducing uncovered production paths.
@dekobon
dekobon merged commit f3aedfc into main Oct 15, 2026
59 checks passed
@dekobon
dekobon deleted the fix/batch-2026-10-10 branch October 15, 2026 23:07

This branch was successfully deployed

1 active deployment
github-pages — f3aedfc1 Deployed Oct 15, 2026 by dekobon via deploy to GitHub Pages #774
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment