Skip to content

fix: batch resolution of #1090-#1151 (21 issues) - #1173

Merged
dekobon merged 52 commits into
mainfrom
fix/batch-1090-1151
Aug 1, 2026
Merged

fix: batch resolution of #1090-#1151 (21 issues)#1173
dekobon merged 52 commits into
mainfrom
fix/batch-1090-1151

Conversation

@dekobon

@dekobon dekobon commented Aug 1, 2026

Copy link
Copy Markdown
Owner

Batch fix for #1090#1151: 21 issues across 50 commits, developed in an isolated worktree with one agent branch per issue, plus a follow-on cursor-hoisting pass.

Closes #1090, #1102, #1103, #1106, #1117, #1124, #1125, #1126, #1127, #1130, #1131, #1132, #1133, #1141, #1142, #1145, #1146, #1147, #1149, #1150, #1151.

#1143 is deliberately not closed — step 2 landed, steps 3 and 4 remain open with re-measured counts (see below).

#1145 had to land first

cargo fmt --all could not run inside a git worktree at all, so make pre-commit died at stage one and the canonical gate was unavailable in the isolation mode four skills document as their default. Six manifests needed a [workspace] table, not the five the issue listed — enums fails identically and takes three more gate stages down with it.

Correctness

Issue Change
#1102 ABC counts a ternary's condition and branch operands in C, C++, ObjC, Mozcpp, JS, TS, TSX, MozJS, PHP and Perl — a ? !b : !c moves 1 → 4, matching Java/C#/Groovy. 266 snapshot files, all values upward, submodule pushed.
#1130 bca ops opens function spaces on the same source-aware predicate as bca metrics, so Elixir defmodule/def finally get per-function vocabularies instead of a file-level one. New exhaustive-match-on-LANG parity test between the two walks.
#1142 / #1147 nargs counts Elixir arguments and Perl signatures rather than reporting 0 for every function. NArgs::compute gains code: &[u8], the last metric that lacked it.
#1149 Python nested defs no longer inherit the enclosing conditional nesting, so a function in an if scores the same as its byte-equivalent elsewhere — the contract the book already documented.
#1131 An unlistable directory now fails the walk across seven subcommands. bca check previously reported clean on a tree it could not fully read.
#1132 Five commands exit 1 with a diagnostic instead of panicking on an unwritable stdout. bca dump | head was also already broken and now exits 0.
#1146 An explicitly-named path that overrides an exclude now says so, naming the glob.

Performance

Guards

#1133 gave the scaling gate a width axis, and it immediately earned it: #1106's regression fits 2.17 on a width ladder while every depth probe stays green. Both were falsified by reinstating the defect and watching the gate go red — #1133 against #1100's rejected fix (0.97 → 1.99).

#1141 adds [thresholds.lang.<slug>], with the soft tier derived from each language's resolved hard limit. Deriving it globally would put soft below hard for any loosened language, making exit code 5 silently reachable.

The issues were wrong more often than not

Every issue in the range contained at least one factual error found only by measurement, and several would have shipped a wrong fix taken at face value:

Corrections are recorded on each issue.

Sibling sweeps

Filed #1159#1166 for the same defect classes in languages and seams outside this range — notably #1159 (JS-family nesting reset, four languages) and #1161 (ABC ternary in Ruby/Tcl/iRules, with a warning that Python must not be fixed the same way or it double-counts).

Validation

make pre-commit green on the merged tree (4,979 tests), make chain-audit green, all 24 make bench-scaling probes within bound, no .snap.new drift, submodule SHA fed6e69e reachable on its remote.

dekobon added 30 commits July 31, 2026 22:27
The scaling harness is about to gain a width-sweeping probe family,
which makes "depth" wrong at every entry that does not nest: the
field, the per-cell record, and the report column a reader consults
when a probe fails all name the axis rather than the quantity.

Rename Probe.depths to sizes, Cell.depth to size, and SMOKE_DEPTHS to
SMOKE_SIZES, and reword the doc comments to name the probe's swept
parameter. No behaviour change; the report's first column header now
reads "size".
`exclude` denies workspace membership but does not terminate cargo's
upward search for a workspace root. In a git worktree under
`.claude/worktrees/` that search escapes the worktree and resolves
against the main checkout, where the crate's path is neither a member
nor excluded, so `cargo metadata` errors. That killed `cargo fmt --all`
and every `make pre-commit` stage chained behind it — the validation
gate was unavailable in the isolation mode four skills use by default.

Six manifests need the table, not the five vendored grammars the issue
listed: `enums` fails identically and takes `enums-check` and both
`enums-codegen-drift` stages down with it.

Also excludes `.claude/worktrees` from the root workspace, so the outer
workspace cannot adopt a crate belonging to a worktree checkout, and
adds `utils/check-excluded-manifests.py` (wired into `make lint`,
pre-commit and CI) so a future excluded crate cannot reintroduce this
silently — which is how it went unnoticed until now.

Fixes #1145
`AGENTS.md` requires external grammar crates to carry `=X.Y.Z` pins, so
a plain `cargo update` cannot move a grammar silently and a downstream
consumer of the published `bca-tree-sitter-*` crates cannot resolve one
freely. Two vendored manifests used caret ranges: `tree-sitter-cpp` in
mozcpp (the crate #86 exists to gate) and `tree-sitter-javascript` in
mozjs.

`tree-sitter-language` stays caret-ranged, against the issue's table.
It is the ecosystem's shared `LanguageFn` trait shim rather than a
grammar, so the rule does not reach it, and pinning it is unworkable in
both directions: `tree-sitter-irules 0.1.1` requires `^0.1.7` and cargo
unifies 0.1.x deps, so `=0.1.0` makes the workspace unresolvable; and
an `=` pin on a shim every grammar depends on would break resolution
for downstream consumers of these published crates. Recorded as an
explicit carve-out in AGENTS.md and the gate's PIN_EXEMPT_DEPS. The
issue also listed it for tcl alone — it is caret-ranged in all five
vendored manifests, four of them spelled without spaces around `=`.

`check-excluded-manifests.py` grows the pin check so a future vendored
grammar cannot reintroduce the drift. `check-grammar-marker-sync.py`
now strips a leading `=` before comparing: its baseline records which
upstream version the vendored sources were generated from, so
`=0.23.4` and `0.23.4` name the same thing and the literal comparison
reported drift for a change that touched no generated byte.

Fixes #1151
Review of the #1145 / #1151 commits found the new gate hand-rolling
regexes over TOML, and a set of smaller defects around it.

The regex parser silently skipped legal Cargo syntax rather than
failing loud. TOML literal strings were invisible: an `exclude` array
entry spelled `'enums'` dropped out of the check, and
`tree-sitter-cpp = '0.23.4'` read as no dependency at all, so an
unpinned grammar passed. A commented-out `exclude` entry came back as
a crate path and killed the run on a missing manifest — and that
commit had just added the array's first comment. An inline
`exclude = ["enums"]` swallowed the entries of any later multi-line
array, and an indented or trailing-nothing `exclude` reported "could
not locate". The workspace-table probe matched the text `[workspace]`
inside a multi-line string and missed `[ workspace ]`. All of it
falls out of parsing with `tomllib`, following the `tomli`-fallback
import that check-grammar-marker-sync.py already uses. (F3, F4, F6)

The stated invariant was also wrong. The comment claimed only a bare
`[workspace]` roots a workspace and the test called that "the failure
this gate exists to catch". Probed on cargo 1.95.0: a sub-package with
no workspace table under an unrelated root exits 101, while
`[workspace.package]`, `[workspace.dependencies]` and
`[workspace.lints]` each make `cargo metadata` exit 0. Cargo keys on
the `workspace` key, not the header. The gate, the comment and the
test now state that. (F5)

Remaining fixes:

- The `tree-sitter[\w-]*` anchor never reached
  `dekobon-tree-sitter-groovy`, a live entry in the root and `enums`
  manifests; matching the substring covers it and any future
  `bca-tree-sitter-*`. (F1)
- AGENTS.md claimed the gate enforced the root manifest's ~20 grammar
  pins; it only walked the excluded crates, so loosening
  `tree-sitter-python` there was invisible. The root is now scanned
  too, which makes the sentence true. (F2)
- check-grammar-marker-sync.py's `removeprefix("=")` left the space in
  `"= 0.25.0"` and reported false drift on a spelling the pin gate
  called compliant. Stripped now; a pin is defined as exactly
  `=X.Y.Z`, so a compound `=0.25.0, <0.26` is rejected by the pin gate
  and cannot reach the marker gate. (F7)
- `main()` had no test at all, and suggested `should be "=^0.23.4"` /
  `"=>=0.23, <0.24"` / `"=*"` — requirements cargo rejects. The
  suggestion is only offered for a bare version now, and both error
  branches, the multi-offender join and the message text are tested.
  (F8)
- `test_equals_pin_still_trips_on_a_real_version_bump` passed against
  both implementations, since `"=0.26.0"` contains `"0.26.0"`. It
  asserts the stripped form, and a spaced-pin sibling was added; both
  fail when `.strip()` is removed, and nothing else does. (F9)
- The `^\s*` dependency-line anchor was untested — removing it failed
  nothing. A commented-out dependency fixture now covers it, verified
  against the pre-rewrite script with the anchor removed. (F10)

Smaller items: the new Makefile targets had been spliced into the
middle of the man-page gate's comment block, splitting both (F11);
the pre-commit `files:` regex `tree-sitter-[a-z]+` could not match
`tree-sitter-c-sharp/Cargo.toml` (F12); the gate had no defensive twin
in the CI lint job, unlike its four siblings (F13); `tree-sitter` the
runtime is pin-required while the `tree-sitter-language` shim is
exempt, now with the measured reason recorded (F15); and `[workspace]`
sat last in all six excluded manifests, where an empty table adopts
any bare key appended after it — hoisted above `[package]` (F16).

CHANGELOG records #1145 and #1151, both downstream-visible. (F14)
Every emission path that reached stdout through `println!` panicked on
a write error instead of returning one. `count` and `preproc` render
after the walk on `main` and exited 101; `dump`, `find`, and
`strip-comments` crashed a worker and surfaced the I/O error as
`Receiver("A thread used to process a file panicked")` — accidentally
exit 1, but reported as a thread crash and never naming the path.

Worker paths now write through the held stdout guard with `?`, so a
failure routes into the `write_failures` tally #1115 installed and
`BrokenPipe` stays swallowed. `count` and `preproc` cannot reach that
per-file seam, so they go through a new `writeln_stdout_or_die`, which
shares `write_stdout_or_die`'s single BrokenPipe decision.

`bca dump | head` was itself broken before this: the second and later
banners met the closed pipe and panicked, so the run exited 1 with
backtrace notes. It is exit 0 again, and pinned.

Fixes #1132
Every generator in the harness was affine in nesting depth, and
`shapes_nest_proportionally_to_depth` required it, so a walk that is
linear in depth but quadratic in a parent's child count passed the
gate. That is not hypothetical: the fix #1100 originally proposed was
green on every depth probe while making a flat 2 000-item file 94x
slower, and only an ad-hoc microbenchmark caught it.

Probe now carries an Axis. The depth invariant applies to Axis::Depth
only; Axis::Width gets shapes_widen_without_deepening, which asserts
both that the widest node gains children and that the AST depth stays
constant — without the second half a width probe that quietly began
nesting would pass while silently measuring the depth axis. Each
invariant asserts its axis is populated, so deleting the last probe of
an axis fails rather than retiring that axis.

nom/wide-attributed-fn renders n top-level `#[inline] fn f() {}` items
at 500 / 1 000 / 2 000 under the same exclude_tests attribute scan as
its nesting sibling. It fits 0.97-1.00; reintroducing #1100's rejected
forward-always scan behind a local patch takes it to 1.99 (35.6 /
145.5 / 560.6 ms against 1.9 / 3.8 / 7.2) while every depth probe
stays inside its bound.

shapes_parse_without_error and probe_workload_is_exercised now render
at the probe's smallest declared size instead of a literal 8, which is
eight siblings on a width shape. The whole set costs ~0.4 s in a debug
build.

Fixes #1133
`python_apply_boolean_operator`'s doc comment described the enclosing
walk as counting control constructs up to the nearest lambda.
`count_specific_ancestors` is `(ancestors, check, stop)` and the call
passes `python_is_lambda` as `check`, so it does the reverse: it counts
lambdas and stops at `expression_list` / `if` / `for` / `while`.

Add `python_boolean_in_expression_list_under_lambda`, which
discriminates the `ExpressionList` stop arm — previously covered by no
test. Both grammar routes that put an `expression_list` under a lambda
are exercised: a parenthesised `yield` and an f-string interpolation.
Each independently scores 1 with the arm and 2 without, and deleting
only that arm makes this test the sole failure across the lib suite.

Keep the `if`/`for`/`while` arms with a comment recording why they
cannot change a count: they do fire, but no lambda can sit above an
`if`/`for`/`while` statement, so stopping there is indistinguishable
from running to the module root. The comment sits above the `matches!`
rather than inside the pattern, per .claude/rules/formatting.md.

No metric values move.

Fixes #1090
`Stdout::lock` has returned a `StdoutLock<'static>` since 1.61, so the
`let stdout = ...` anchor the dump/find banners kept is dead weight; the
rest of the crate, including the sibling hunk in the same function's
file, already writes `std::io::stdout().lock()` inline.

In the #1132 stdout tests, import `std::process::{Output, Stdio}` rather
than spelling the path at six call sites. No assertion, fixture, or
control flow changes.
Python's `FunctionDefinition` arm bumped the function-depth surcharge
but never reset `nesting.conditional`, so a `def` written inside an
`if` inherited the enclosing function's nesting on top of its own
depth. `def inner` two conditionals deep scored 4 where the
byte-equivalent Java local class scores 2.

Python was the only module with a syntactic function-definition node
missing the reset. The book documents the resetting behaviour as the
contract, so the published documentation described behaviour Python
did not implement.

The `Lambda` arm deliberately keeps its current shape. Every sibling
lambda arm is a bare `nesting.lambda += 1` that leaves `conditional`
alone, because a lambda amplifies the enclosing nesting rather than
replacing it; adding the reset there would break the parity the
function-boundary reset exists to preserve. The book sentence naming
lambdas alongside nested functions was wrong on its own terms and is
split into one bullet per rule.

The regression test nests the definition two conditionals deep. At one
level the Java half cannot discriminate — reset plus surcharge and
no-reset plus no-surcharge both yield 2.

No snapshot churn: the integration corpus contains no Python files.

Fixes #1149
#1098 made every walking subcommand exit 1 on an unreadable input
*file*. A directory the walk could not *list* was a different code
path: it warned, dropped its whole subtree before any file was
selected, and left the per-file tally at zero — so the run exited 0
over a tree it had not read. `bca check` is the worst case, since a
gate reporting clean is indistinguishable from a gate that passed, and
`diff --since` rendered the missing subtree as added or removed rather
than as the I/O failure it was.

`ResolvedFiles` now carries a `WalkErrors` tally that
`walk_directory_seed` populates from its `Err` arm, counting only
errors carrying an `io::Error`. Every variant still warns, so a
malformed ignore file stays visible without becoming fatal. The tally
threads through `run_walk_resolved_tallying` into
`enforce_complete_walk` ahead of the read and write guards, into
`run_walk_resolved` for `strip-comments`, into `vcs rank`, and into a
new `DiffError::UnwalkableInputs` for `diff --since`.

Two findings the book now records. `--exclude` cannot exempt an
unlistable directory — it filters the paths the walk yielded, and one
that could not be listed yields none — whereas an ignore-file entry
prunes it inside the walker. And a malformed `.gitignore` at or below
the walk root never reaches the error arm at all; `ignore` attaches it
to the `DirEntry`, so the non-I/O case is only reachable from an
ancestor, which is where the negative test stages it.

Fixes #1131
`std::io::Stdout` is a `LineWriter` over a 1 KiB buffer, so a document
containing no newline and shorter than that is accepted into the buffer
and only written by the exit-time cleanup flush, whose error is
discarded. `formats::write_text` wrote and never flushed, so `bca vcs
-O json`, `vcs commit`, and `vcs trend` — the three that emit compact
JSON — exited 0 with their reports silently dropped against /dev/full
(measured: 782 / 852 / 836 bytes). Every other `vcs` format carries a
newline and was already exiting 1, which is why #1132's sweep missed
these.

`path_io::write_stdout_parts_or_die` had the identical hole. No shipped
subcommand can reach it with a newline-free document — every one is
line-oriented or pretty-printed JSON, so the buffer spills on an
interior newline — so that half is latent and is pinned by a unit test
on the new `write_parts_flushed` seam rather than end-to-end. The
newline chunk in `writeln_stdout_or_die` made its robustness incidental;
the flush now makes it explicit, and its rationale no longer claims an
allocation saving that never existed.

`strip-comments`' non-UTF-8 stdout branch carried the same shape (no
newline, no flush, a second lock) and is folded into
`write_stripped_on_stdout` alongside its UTF-8 sibling.

Also adds a bench guard: flipping `exclude_tests` off on
`nom/wide-attributed-fn` deleted the whole `should_skip_subtree` scan
the probe exists to price and failed zero tests, because `nom.total()`
reads the same either way on an `#[inline]`-attributed shape. The new
check classifies each shape by walking it both ways and requires a
prunable one to be probed with the flag on — discriminating on the
shape, not on the flag, since selecting by the flag is vacuous against
exactly that flip.

The exit-1 claim in the book's command overview is now true for `vcs`
too, and says so. Corrects the Python cognitive comment that asserted
an unpinned coverage claim and then disclaimed it four lines later.
…7-31-2

# Conflicts:
#	big-code-analysis-cli/tests/read_failures.rs
`per_function` rebuilt everything it needed for each file: a
`to_thread_local()` repository handle, a re-read `.mailmap`, and a
commit-metadata map that died on return, so a commit touching 200
files was decoded, mailmap-resolved, and classified 200 times.

Worse, that handle carried no ODB object cache at all, and no
configuration on the engine could give it one: `into_sync()` discards
both cache layers, `to_thread_local()` rebuilds through
`gix_odb::Cache::from` with `object_cache: None`, and the
`setup_objects` that runs on rebuild re-applies a pack cache
unconditionally but an object cache only when
`gitoxide.objects.cacheLimit` is non-zero - which defaults to zero.

All of that collapses into one change. `BlameSession` (new, public)
holds the handle, its object cache, the mailmap, and the commit memo
for a thread's lifetime; the CLI keeps one per worker in a
thread-local, and the Python batch path keeps one per repository.
`PerFunctionBlame::per_function` is unchanged as the one-shot form.

Holding a handle for a thread rather than a file does not widen the
issue-#579 staleness window: the `ThreadSafeRepository` and its
`gix_odb::Store` are already fixed for the engine's lifetime, and a
handle refreshes its own pack-index snapshot on a miss before
reporting one, so the retry re-reads exactly what a fresh handle
would. `gix::Repository::reload` inside the retry would re-open from
disk to observe external writes the miss is not about.

`vcs commit` and `vcs trend` gained the object cache their sibling
walks already set; trend's comment about freeing a cache it never set
is now true.

Over this repository's `src/` (325 files) at `--jobs 1`, min of seven
interleaved runs: wall 2.94s -> 1.78s, CPU 2.82s -> 1.61s. Output is
byte-identical, as is `bca vcs`, `vcs commit`, and `vcs trend`.

The per-file `canonicalize` the issue lists first is left alone: it
measures 4ms over these 325 files, 0.25% of the run, and deriving
repo-relative paths lexically instead would change what a symlinked
in-tree directory resolves to.

Fixes #1117
`PerlCode` sat on the default `NArgs` impl, whose `compute_args` looks
up a `parameters` field. tree-sitter-perl emits a signature as an
unnamed `function_signature` child instead, so the lookup missed and
every Perl sub reported 0 arguments — a `nargs` limit passed
unconditionally on any Perl codebase, reading as "no offenders" rather
than "not measured".

Add `impl NArgs for PerlCode` backed by `compute_perl_args`, which
locates the signature and counts every child that is neither
punctuation nor a comment. The negative filter is what makes a
defaulted parameter count: `sub deflt($x, $y = 5, @rest)` wraps
`$y = 5` in a `binary_expression`, so a positive scalar/array/hash
variant list would report 2 where the answer is 3. It also needs the
comment exclusion, since a multi-line signature carries `comments`
children directly under `function_signature` — a documented
3-parameter sub read 6 without it.

`Perl::NormalComma` joins `is_non_arg`: the separator inside a
signature is `normal_comma` (369), and the bare `COMMA` (12) already
listed there is that node's child, not the separator.

Two forms measured and deliberately left at 0: a prototype
(`sub f($$)`) is a `function_prototype`, not a parameter list; and an
anonymous sub's signature parses inside an `ERROR` node upstream, so
closures stay uncounted rather than pinning us to a parse shape that
will change. Both are covered by tests so a grammar bump surfaces them.

Fixes #1147
The C family, the JS family, PHP, and Perl counted a ternary's `?` and
nothing else, so `a ? !b : !c` scored 1 where Java, C#, and Groovy scored
4. Each family now walks the ternary's condition and both branch operands
through its existing unary-condition helper, as `java_walk_ternary` has
always done. That is the paper-faithful reading: `src/metrics/abc.rs`
records GMetrics' rule that a value treated as boolean counts, "examples
include `if (x)` and `return !ready`", and a negated ternary branch is
exactly that case. It also makes each family's `<lang>_inspect_container`
boolean-context seed reachable — the seed was written for a ternary
parent the dispatcher could never hand it.

Slots are addressed by grammar field, not by child index. The issue
reported a uniform `0 / 2 / 4` layout; the C-family and PHP grammars in
fact mark the consequence OPTIONAL to admit the short ternary `a ?: b`,
which moves the alternative to child(3), and tree-sitter-perl names the
branches `true` / `false` rather than `consequence` / `alternative`. A
copied index or a copied C-family field name silently drops an operand in
both cases; regression tests pin each.

`(a > 0) ? b : -b` still reads 2 in all eleven languages. The `!` is a
type-free proxy for "this operand is boolean", so an unnegated branch
contributes nothing and the fix is purely additive.

Adding one arm pushed five already-at-limit ABC dispatchers past the
self-scan thresholds. Each is an exhaustive one-arm-per-grammar-kind
match, so they take an in-source `bca: suppress` marker with a rationale
in place of their previous baseline entries, rather than another ratchet.

Ruby, Python, Tcl, and iRules have the same gap and are measured and
filed as #1161; Python's is narrower (condition slot only).

Fixes #1102
`nargs` reported 0 for every Elixir function. The blocker was not the
`is_func` work the issue describes — #275 already taught
`is_func_with_code` to recognise `def` / `defp` / `defmacro` /
`defmacrop`, including the #310 rule that a `def` inside `quote do …
end` declares nothing. It was that `NArgs::compute` was the only metric
in `src/spaces/compute.rs` not handed the source bytes, so no Elixir
impl could reach that predicate.

Thread `code: &[u8]` through `NArgs::compute` (12 impls plus the single
dispatch site) and switch the trait's default from `is_func` to
`is_func_with_code`, mirroring `Nom::compute`. Elixir is the only
language that overrides that predicate, so no other count moves — the
point is that the next language whose declarations are only visible in
the source text does not silently report 0 here.

`impl NArgs for ElixirCode` then walks the two `arguments` levels a
`def` puts between the macro and its parameter list. Four shapes, all
measured against tree-sitter-elixir 0.3.5:

  * `call` has only a `target` field — contrary to the issue, there is
    no `arguments` field, so both levels are found by kind.
  * a guarded head interposes a `when` `binary_operator`; unwrapping it
    is what keeps guarded clauses from counting 0. The same alias
    appears as a guarded `fn` clause's `left`, where without the unwrap
    every closure counted the guard's fixed three children.
  * a bare `identifier` head (`def noargs, do: 1`) has no parameter
    list and stops there rather than falling through to the enclosing
    `arguments`, which holds the `do:` pair.
  * `def a + b` and `def -a` define `+/2` and `-/1`; their head is the
    operator node, with the operands as parameters.

Closures take the first `stab_clause` only, never a sum: every clause
of one `fn` has the same arity, so summing reports 2n.

Elixir comes off the "reads 0" list in the book and in the `bca init`
rationale, leaving Bash as the only entry. The `.bca-baseline.toml`
refresh is the one-token growth the extra parameter costs
`compute_per_node`'s halstead.effort.

Fixes #1142
`ops_inner` decided a node opened a function space with the byte-less
`is_func || is_func_space`, while `spaces::compute::metrics_inner` uses
`promotes_to_func_space_with_code`. Elixir's `defmodule` / `def` /
`defp` / `defmacro` are plain `Call` nodes distinguished only by their
target identifier text (#275), so `bca ops` opened no space for any of
them: an Elixir file came back with only its file-level vocabulary
where `bca metrics` returned a full module/function tree. The same
split existed for the space kind, where `classify_space_kind` called
`get_space_kind` against `open_func_space`'s `get_space_kind_with_code`.

Both call sites now match the metrics walk. Elixir is the only language
overriding a `*_with_code` predicate today, so every other language's
output is byte-identical; `make bench-scaling` is flat (ops/nested-fn
1.03 -> 1.10, inside run-to-run noise, all 25 probes within bound).

Adds tests/ops_metrics_space_parity.rs, which asserts the `ops()` and
`metrics()` space trees agree in shape, name, kind, and line span for
one fixture per language. Its fixture table is an exhaustive match on
`LANG`, so a new language cannot compile without one. Plus three Elixir
ops tests, including a `def` inside `quote do ... end` that must open
no space (#310).

FIXME(#1162) markers record the same divergence at two seams this fix
deliberately leaves alone: `bca functions` (src/function.rs) and
`bca find --type function` (src/parser.rs).

Fixes #1130
`finalize`'s pop arm re-derived the parent's Halstead `Stats` from its
occurrence maps after every child space merged into it. That is three
map traversals — one over the parent's whole accumulated operand
vocabulary — per popped child, for a result the parent's own
`finalize_state` recomputes and overwrites. `O(children x vocabulary)`
where `O(vocabulary)` suffices, so quadratic in a file's function count.

Split `compute_halstead_mi_and_wmc` into `compute_halstead_and_mi` and
`compute_wmc`. The pop arm keeps only the latter: `wmc::Stats::merge`
routes a child's cyclomatic on the parent's recorded `space_kind`, which
stays `Unknown` until `Wmc::compute` runs, so dropping that call fails
133 lib tests. Halstead and MI have no such dependency — both `merge`s
are no-ops on `Stats`, nothing else reads a parent's intermediate
values, and every state reaches `finalize_state` exactly once.

Guarded by a new width probe, `halstead/wide-distinct-fn`, on the axis
#1133 added. It needs a taller ladder than `LINEAR_WIDTHS`: the
redundant pass is a second visit to data the walk already touched, so
its share of the total is set by the sibling count alone and enriching
each function's vocabulary raises both terms together. At 500/1000/2000
the regression fits 1.37-1.39 — a real 2.1x slowdown the 1.5 bound
passes. At 4000/8000/16000 the gate reads 1.09 clean and 2.17 with the
per-child pass restored, 45.6 ms against 507.4 ms on the top cell, and
it is the only probe that fails.

Metric values are unchanged: `cargo test --workspace --all-features`
green from a clean tree with no `.snap.new` under the output submodule.
Real-code effect is modest, since it is quadratic in one parent's child
count: no measurable change on the corpus slice (files capped at 64 KB),
~10% CPU on the corpus's widest file (a 2.4 MB C++ source with 1,808
top-level spaces, 95.6 ms to 86.7 ms).

Also corrects `mi::Stats::merge`'s comment, which named this commit's
renamed function and the wrong module.

Fixes #1106
An explicitly named path overrides the walker's exclude deny-set by
design — the ripgrep convention that a path you named is a direct
request — but nothing said so. Any caller that names paths one at a
time therefore analyzed files the project had put out of scope and
reported them as offenders, which is what the shipped per-edit agent
hooks do on every edit.

Keep the override; make it visible and document it.

`expand_seed_paths` now emits, for a named seed the deny-set covers:

  bca: warning: <path> matches an exclude pattern (<glob>) but was
  named explicitly; analyzing anyway

naming the glob in the spelling the user configured, so the entry can
be found and moved. It is silent for a seed no language claims, since
`git diff --name-only | bca metrics --paths-from -` feeds in whole
changesets where such files are the majority and produce no output
either way. Naming the glob needs the pattern list alongside the
compiled set, so `ExcludeGlobs` pairs them and `mk_globset_retaining`
is the single place the compile-time pattern skip lives — the caller's
original list is not index-aligned with the set.

Fixes a second defect found while verifying the hooks' own invocation
shape: `anchor_against_seeds` left an absolute explicitly-named seed
unanchored, so a `./`-anchored `[check.exclude]` glob never matched it.
Both hooks pass absolute paths, so the one exclude surface meant to
survive an explicit path did not. It now falls back to
`file_seed_match_path`, the same CWD-relative form the walk's include
filter derives. The residual manifest-root-vs-cwd anchoring gap is
tracked in #1164.

Move this repo's own gate-scope exclusions out of `.bcaignore` and into
`bca.toml`'s `[check] exclude`: `.claude`, `.opencode`, `utils`,
`xtask`, `enums`, `generate-grammars`, and the benchmark harness. These
were never "do not analyze this" — they are dev tooling that ships
nothing and should not gate. Eleven of their files produced false
offenders when named explicitly; none do now. `.bcaignore` keeps only
walk-scope entries (build output, vendored trees, prose, the
third-party corpora) and its header states the split.

The book's "Explicit paths bypass the filter" section, the `--exclude`
/ `--exclude-from` / `--paths-from` long help, the agent-feedback
recipe, and both hook scripts now state the rule: walker excludes shape
what gets analyzed, check excludes shape what gets gated, and a
per-file invocation only respects the second.

`expand_seed_paths`'s three accumulators become a `SeedSet`, which
states the dedupe invariant once instead of at each branch that feeds
it; that also takes its halstead.effort from 46_430 to 30_844, clear of
both gate tiers.

`exemptions.rs`'s `run_json` now anchors its cwd at the fixture. It ran
from the inherited repo root, so manifest discovery unioned this
repository's new `[check] exclude` globs into its assertions (#491).

Fixes #1146
The #1146 exclude-override warning gated itself on a local
`extension()` + `get_from_ext` pair, which is the body of the
library's `get_language_for_file` minus the ASCII case-fold #1111
put inside it. A mixed-case extension therefore resolved to no
language and returned early, so `bca check SKIPME/A.RS` analyzed
the file and reported its offenders while saying nothing about the
override it had just performed -- the exact silent asymmetry #1146
exists to remove, surviving for every mixed-case extension.

Call `get_language_for_file` instead and drop the wrapper, whose
whole doc comment was the imprecise claim that it mirrored the
per-file dispatch (the dispatch uses `guess_language`, which also
falls back to modeline and shebang). The remaining rationale moves
onto `warn_exclude_overridden`, including why the extension half is
deliberate here: the walk has not read the file's bytes.

Verified by perturbation -- restoring the old spelling makes the new
test the only failure in the CLI suite, and its output shows the
defect directly (`B.RS` reported as an offender, only `a.rs`
warned).
dekobon added 20 commits August 1, 2026 06:53
Three follow-ups from a review of the whole batch branch.

The `write_text` flush added while closing #1132 made a closed consumer
pipe visible as an `io::Error`, and every `vcs` emit site died on it —
so `bca vcs --format json | head` exited 1 where `bca dump | head`
exits 0. That contradicts the policy the same batch documents on
`path_io::write_stdout_parts_or_die` and pins for `dump` alone.
`die_unless_broken_pipe` now routes all four emit sites through one
place. The regression test uses a 400-file fixture so the document
outgrows the 64 KiB pipe buffer and the close is actually observable.

The privileged-runner skip guards added for #1131 could never fire:
`deny_all_access` probes with `fs::read`, which returns `EISDIR` for
every directory whatever its mode, so under root the tests proceeded
and failed on the exit-code assertion instead of skipping. The new
`deny_dir_listing` probes with `read_dir`, and `unlistable_dir` is
refactored onto it.

CHANGELOG gains the entries for every metric-value and CLI-contract
change in the batch — #1102, #1106, #1117, #1130, #1131, #1142, #1146,
#1147 and #1149 were all unrecorded, as was `BlameSession` under
`### Added` despite already being in STABILITY.md.
Both found by an audit-tests pass over the whole batch, and both
demonstrated rather than argued.

`read_failures`' git helper reported any non-zero git exit as "no usable
git", so a host with a global `core.hooksPath` containing a failing
`pre-commit` turned the entire `bca vcs` stdout contract into four
silent skips — every ENOSPC case plus the BrokenPipe case added for the
regression this batch introduced. Verified by running the compiled
binary under such a HOME. The fixture now neutralises the host the way
`blame_fixture` already does for #941 (hooks path, gpgsign, gc.auto,
autocrlf), and only `git init` may skip: once a repository exists,
every later command runs against a directory the test owns, so a
failure is a fixture defect, not an environment to tolerate.

`paths_discovery`'s unlistable-directory test was the one #1131 case
left off the capability-probe convention, so a privileged runner failed
on the exit-code assertion instead of skipping. Verified under
`unshare -r`: it failed before, skips now. It uses the shared
`deny_dir_listing`, which probes with `read_dir` — root ignores mode
bits for a listing, while `fs::read` returns EISDIR whatever the mode,
which is what made the original guard inert.
`python_apply_boolean_operator` adds one structural unit to a boolean
operator for each enclosing `lambda`, halting the walk at the nearest
`expression_list`. That is a deviation from Campbell, who gives a
boolean sequence a fundamental increment and no nesting increment. No
other language applies it, and it appeared nowhere in the book.

Reviewed and deliberately kept: the intuition it encodes is defensible
and removing it would move every Python score for a construct nobody
has reported as wrong. Documented instead, in the Cognitive Complexity
per-language deviations list, with a measured table covering the
per-lambda ladder, the operator-switch interaction, and both boundary
cases.

Two things the walk does that a reader would otherwise get wrong: the
`if`/`for`/`while` stops are inert, because a lambda body is a single
expression and no lambda can sit above one of those statements; and
"outermost operator" is scoped to a lambda body, so a chain that
switches operators still pays two sequence increments.

Zero metric change. `python.rs` gains a pointer to the book section
rather than a second copy of the prose; #1090 already corrected the
in-source description.

Fixes #1150
`perf_10k_lines_200_commits_under_30s` rebuilt and rewrote a full
10 000-line file for each of 201 commits, spawning 407 `git` processes
and dominating the `vcs_per_function` binary's cost. Cut the history
depth to 50 commits, keeping the file at 10 000 lines / 2 000 spans —
span count is the dimension per-function bucketing scales in, and the
one #329's acceptance criterion is really about.

The budget moves with the fixture, not after it: leaving 30 s over a
quarter-size fixture would silently quadruple the regression the
assertion tolerates. Re-derived at the same headroom ratio from
measurements on one 16-core host (median of 5 runs, debug build):

  2000 funcs / 200 commits: blame 0.53 s, 407 spawns, budget 30 s (57x)
  2000 funcs /  50 commits: blame 0.14 s, 107 spawns, budget  8 s (57x)

The derivation is recorded in the test, along with what the shrink
trades away — sensitivity to a superlinear-in-history regression — so
the next person rescaling it does not re-measure from scratch.

The old work-product guard, `any(|s| s.commits_long > 0)`, held if a
single span attributed anything. Both windows are exact by construction,
so the test now asserts the exact history: `commits_long` is 2 for the
edited functions and 1 for the rest, and the set carrying a recent
commit equals exactly the edited set. Perturbing the fixture's edit
target by one function fails both; the old guard passed it.

Whole binary, best of 5: 1.69 s → 0.82 s wall, 1.63 → 0.82 CPU-s, 499 →
199 `git` spawns.

Fixes #1125
Eighteen per-language modules spelled out the same two statements at
their function-definition arm: reset structural nesting, then bump the
function-depth surcharge when this definition is lexically nested in
another. That pair is one named concept, and every one of those arms
already carried a comment saying so. `enter_function_boundary` gives it
a name.

The issue listed seventeen modules. Python is the eighteenth: #1149
added its reset after the issue was written, so it now conforms and the
annotation the issue planned for it is unnecessary.

Two callers still spell the statements out, for opposite reasons, and
both now say which half of the shared rule they are opting out of.
Elixir takes the reset without the depth bump, because
`increment_function_depth` matches `Call` ancestors by kind_id and every
`def` inside a `defmodule` has one. The `js_cognitive!` macro takes the
pair plus a `nesting.lambda` reset no other language performs, so a
`function` declaration inside an arrow function does not inherit that
arrow's lambda surcharge: measured, `() => { function g() { if (x) {} } }`
scores 1 where the all-arrow equivalent scores 3.

Zero behaviour change, verified by perturbation rather than by a new
test, since a symmetric-sum field admits none. Deleting the reset from
inside the helper fails exactly six tests, one per language that guards
the channel, which is what proves the eighteen call sites route through
it. Transposing the two statements fails none, confirming the ordering
carries no meaning.

Fixes #1103
#1143 step 2. `abc` 50 -> 40 costs zero hard-tier offenders — the one
the issue measured (`run_command_report` at 40.82) has since fallen to
39.06 under this batch's changes. Two functions land in the soft tier
(39.06 and 39.15 against a 0.95-scaled 38); both are under the hard
limit and are recorded in the baseline rather than suppressed, so the
early warning stays visible.

Step 1 (`nargs` 7 -> 6) is NOT taken, against the issue's
recommendation. It reads as a free ratchet on a hard-tier measurement —
zero new offenders, since all 73 offenders at limit 5 sit at exactly 6 —
but the soft tier scales every limit by BCA_HEADROOM (0.95), so a limit
of 6 puts all 73 permanently in the 95-100% band at once. They can
never clear it, because they are the limit. That buys ~73 baseline
entries for no hard-tier gain, which is the "reads as debt rather than
as a decision" outcome #1143 exists to avoid. Measured, not reasoned:
73 soft offenders at `nargs = 6 (limit 5.7)`.

`cognitive` 25 -> 15 (step 3) remains open at 19 offenders, all between
16 and 20 — re-measured after #1102 and #1103 moved the ABC walker
family the issue's original count was built on.
A single [thresholds] table has to fit every language in the tree, and
the measured spread is too wide for that: the 97.5th-percentile
per-function cognitive value runs from 4 in C# to 50 in C. A
[thresholds.lang.<slug>] table now layers over the global one per
metric, so a language inherits every limit it does not restate.

Slugs are the canonical language names --language accepts; an unknown
one is a tool error with a did-you-mean hint, while a recognised
language with no override falls through to the global table.
--print-effective-config emits one fully resolved table per overridden
language, inherited limits included, and still round-trips through
--config.

The soft tier is derived per language from that language's resolved
hard limit, so there is no [thresholds.lang.<slug>.soft] syntax. That
alone was not enough: classify_check_outcome read the hard-breach
ceiling from one global map keyed by metric name, so every soft
violation in a loosened language escalated to exit 5. Each resolved
threshold now carries the ceiling of the table that produced it,
stamped onto the Violation, and the classifier reads it from there.

Four defects surfaced while implementing and are fixed here:

- An absolute [thresholds.soft] value was not derived per language, so
  a language tightening its hard limit below it inverted the tiers.
  Now rejected, naming the table, as a "<ratio>x" factor above 1
  already is. Excluded for the lower-is-worse mi.* family, whose soft
  scaling is separately wrong (#1166).
- A scale-relative soft limit could not resolve against a hard limit
  that lived only in a language table.
- An alias-spelled limit ([thresholds] sloc = N) never escalated at the
  soft tier, because the ceiling lookup compared a canonical name
  against a raw-spelled map.
- A metric gated only by a language table was left uncomputed by the
  #1113 walk narrowing, so it read its zero default and passed.

Preproc and Ccomment slugs are rejected rather than accepted: bca check
never gates those grammars, so such a table could not fire.

thresholds.rs crossed its loc.ploc limit, so the soft tier moved to
threshold_soft.rs alongside the new threshold_lang.rs, leaving the
engine, the soft tier and the per-language tier in three modules.

Fixes #1141
Each `tests/*.rs` file is its own crate root, and each links the
tree-sitter runtime plus every grammar. Thirty-one at the workspace
root and thirty-seven under the CLI made linking — not compilation —
the tail of every incremental `cargo test`, and ten of them held a
single `#[test]` while linking a ~280 MB binary to run it.

Group them into six themed directory targets per crate (`tests/<group>/
main.rs` plus the former files as `mod`s): api, corpus, grammars,
output_formats, parity, vcs at the root; check, cli_ux, diff,
discovery, output, vcs under the CLI. Also declare `[profile.dev]
debug = "line-tables-only"` — nothing in this repo's workflow reads
full DWARF, and panic backtraces keep file:line — and document mold /
lld in CONTRIBUTING.md as an opt-in local `.cargo/config.toml`, not a
committed requirement.

Test bodies are unchanged. The per-module edits are mechanical:
`mod common;` becomes `use crate::common;` (51 files), a crate-level
`#![cfg]` moves onto the `mod` declaration so the driver's `//!` doc
stays ungated for the no-default-features and minimal-langs CI legs
(9 files), `tests/vcs.rs` becomes `vcs/vcs_rank.rs` and
`parser_reuse.rs` sheds its same-named inner module, both to avoid
module inception, and the five `insta` snapshots move to
`tests/output_formats/snapshots/` under the `output_formats__` prefix
insta derives from `module_path!()`.

`cargo nextest list --message-format json` before and after lists 4955
tests both times; canonicalising the merge-introduced module prefix
away leaves exactly 34 differences, all 1:1 renames from those last two
points. Measured here: test binaries 13.03 GB -> 1.74 GB, target/debug
after a clean build 18.8 GB -> 4.9 GB, cold `nextest list` 982 -> 762
CPU-s, and a relink after touching `src/lib.rs` 88.5/91.5 -> 27.4/29.8
CPU-s.

Fixes #1124
The 2,335 `check_metrics` / `check_func_space` call sites under
`src/metrics/` drove `metrics_inner` with `MetricsOptions::default()`,
so a test asserting one family paid for thirteen — Halstead's per-node
maps included.

`test_support` gains `check_metrics_only` / `check_func_space_only`
threading `MetricsOptions::default().with_only(...)`, plus two
`macro_rules!` generators. Each per-metric `mod tests` declares its own
`check_metrics` shim in place of the import it replaces, so the call
sites are untouched and a test added later inherits the restriction.
The handful that assert a *different* family get a wider shim of their
own rather than widening the module's.

Two of those were found the hard way: `python_tokens_distinct_from_
halstead` and its C++ sibling compare `tokens_sum()` against Halstead's
`N1 + N2`, which reads 0 when Halstead is deselected — green, and
testing nothing. The four `wmc.rs` tests reading `npm.class_nm_sum()`
failed loudly; the two `tokens` ones would have shipped.

Values are unchanged: no expected value moved, running every migrated
test back on the full set fails 0 of 3,245, and the 1,639 whole-`Stats`
snapshot assertions in these modules all match.

The new `metric_selection_parity` test pins the property the migration
rests on — for every metric in a selection's resolved closure, a
restricted walk reproduces the full walk's per-space values — with a
`with_only(&[])` run as the non-vacuity reference. Narrowing one walker
gate to `contains(Npa) && contains(Npm)` fails only this test, 1 of
3,245; the existing `with_dependencies_pulls_in_*` tests cover a dropped
*declared* dependency but not an undeclared coupling between gates.

Single-threaded per-run minima, all-features lib test binary, under
load average 30-45 on 16 cores: whole binary CPU 4.64 s -> 4.20 s, wall
5.16 s -> 4.42 s; the 2,317-test `metrics::` tranche 1.16 s -> 0.95 s
CPU. #1113's 5-7x was release-mode whole-file analysis; these fixtures
are a few lines each, where parse and space construction dominate.

Fixes #1127
`check_thresholds`, `check_baseline` and `init` each carried their own
copy of `TRIVIAL_RUST`, and the first two of `BRANCHY_RUST`, then wrote
the same bytes into a fresh `TempDir` in every test. Hoist them into
`tests/common/fixtures.rs` and point the twenty-two `check_thresholds`
tests whose tempdir existed only to hold a fixture and anchor a
hermetic cwd at the shared directory instead.

Two departures from the issue's plan, both measured:

The fixture directory is a deterministic, content-addressed path under
`std::env::temp_dir()`, not the proposed `LazyLock<TempDir>`. A
`TempDir` inside a `static` is never dropped, so that shape leaks a
directory per test process — and under nextest, which runs a process
per test, it also shares nothing. Hashing the fixture bytes into the
directory name makes the cache self-invalidating; publishing each file
by rename makes concurrent creation safe.

`manifest.rs` keeps a fixture of its own, renamed `FOUR_BRANCH_RUST`.
Its `BRANCHY_RUST` was *not* byte-identical to the other two: it is a
four-branch function against their five, and the limits that module
sets sit between the two. The issue's "per-test variance is the
manifest/cwd, not the sources" holds for three of the four files, not
all four.

Verified by perturbation: pointing `branchy_rs()` at the trivial
fixture fails ten of the converted tests and nothing else, so the
converted sites do read the shared file and would notice a wrong one.

CLI slice, three runs each, minima (CPU is the stable figure under
this machine's contention): 3.89 s wall / 17.02 CPU-s before, 2.91 s
wall / 15.17 CPU-s after.

The issue's second item is not actionable as written. There is no
in-process entry point for an integration test to migrate to:
`big_code_analysis_cli` exports only `Cli` and `run`, and `run` is
documented as terminating the calling process rather than returning.
Every case the suite covers — exit codes, stderr text, env scrubbing,
cwd discovery, output plumbing — lives on the process boundary, so all
483 spawns stay. Its premise is also stale: 483 spawns of a
representative `bca check` measure 3.0 CPU-s here, not the 30-60 s the
issue estimated before #1120 moved the suite to nextest.

Fixes #1126
Assistant tooling runs zsh, where an unquoted `$var` is a single word
however many spaces it holds — unlike bash and POSIX sh. The command
still runs, still exits 0, and still prints something, so the failure
surfaces as a wrong number rather than an error.

It cost two measurement loops during the #1090-#1151 batch. Re-measuring
#1143's threshold offenders, a `SCOPE="-p a -p b"` scalar reached `bca`
as one argument that matched no path, so all seven threshold rows
reported 0 offenders — a coherent-looking "already compliant everywhere"
that was pure artifact. The array form reported 19, 73 and 127.

The rule pins the boundary rather than the folklore: zsh does not split
*parameter expansions*, but it does split *command substitutions*, so
`for f in $(rg -l x)` is fine and only the intermediate variable
collapses. Every row of its comparison table was measured in both
shells, including that `PIPESTATUS` expands to nothing under zsh while
`$pipestatus[1]` reports correctly.

Two siblings that bit the same measurement are recorded alongside it:
`$?` after a pipeline reports the last stage, and `bca check` writes
offenders to stderr, so `2>/dev/null` silently empties the result.
`Search::first_occurrence`, `Search::act_on_node`, `bca dump`'s tree
renderer, and the Python bindings' mirror of `Node::preorder` each still
built a `TreeCursor` per visited node. All four now hold one for the
walk, via `children_with` on the Rust side and a caller-supplied cursor
in `push_children_for_preorder`. The two `Search` walks also drop their
staging `Vec`: children go on in source order and the freshly-pushed
tail is reversed in place, which is what `children.drain(..).rev()` did
and copies each child once instead of twice.

The counter that makes this testable guarded three of the six consumers,
and the comment describing its coverage blamed module reach. That was
not the boundary: `first_occurrence` and `act_on_node` sit in `node.rs`
alongside the counter and were equally unguarded. Reverting all three
unguarded sites to `children()` compiled clean and failed nothing.

The real obstacle was that `observation::counter!` emitted a private
module, so a guarded path in another module was structurally
unassertable. `observed()` is now `pub(crate)`; `record()` stays
`pub(super)`, since only the module owning a counted path should be able
to bump it. All six sites now assert, each verified by reverting its own
call site: 50 cursors over 50 nodes for the two `Search` walks, 12 over
34 for the dump.

Also pins `first_occurrence`'s pre-order contract, which nothing
covered — deleting the reversal that maintains it failed zero tests,
while the same deletion in `act_on_node` failed one. The existing
negative test cannot reach it: a match-everything predicate hits the
root before any child is pushed. The order is load-bearing, not
incidental; the C / C++ / Objective-C / mozcpp `get_func_space_name`
declarator searches are `first_occurrence` calls, so a reversed sibling
order changes which declarator names a function space.

Three documentation corrections alongside: `output::dump` was listed
among a *metric walk*'s child scans, which no `metrics()` call runs;
`PyNodeWalk` declared its cursor after the `Py<PyAst>` that keeps the
tree alive, making the new doc's claim false by drop order (harmless
today, since `ts_tree_cursor_delete` only frees its own stack, but the
field order now makes the claim true by construction); and the py
module doc pinned `=0.26.9` where the workspace pins `=0.26.11`.
The pattern the previous commit removed from the two `Search` walks — a
hand-rolled `goto_first_child` / `goto_next_sibling` loop over a scratch
`Vec`, drained with `.rev()` — survived in five more places, including
`spaces::compute::push_children`, which `metrics_inner` and `ops_inner`
both walk the whole tree with.

No allocation win: every one of these already hoisted its cursor. What
changes is one pattern instead of five, one copy per child instead of
two, and `push_children` shedding a caller-threaded scratch buffer plus
the `debug_assert` that policed it.

Three of the five deliberately do not reverse, and say so. `count` only
tallies; `preproc` records directives with byte offsets and replays them
in source order afterwards; `comment_rm` collects spans that
`remove_from_code` replays in reverse byte order. Imposing an order
there would imply a guarantee nothing relies on. `comment_rm` also keeps
its `chain.push` conditional on children actually being pushed —
dropping that guard would leave the chain one deep too far on every leaf
and desync exactly the truncate/push bookkeeping `make chain-audit`
checks.

Verified beyond the usual gate, since this touches the two hottest walks
and one of the five chain-threading walks: full workspace suite green
with no snapshot drift, `make chain-audit` green, and all 24
`make bench-scaling` probes within their complexity bound.
Batch fix for issues #1090-#1151: 21 issues resolved across 46 commits,
developed in an isolated worktree with one agent branch per issue.

Fixed: #1090 #1102 #1103 #1106 #1117 #1124 #1125 #1126 #1127 #1130 #1131
#1132 #1133 #1141 #1142 #1145 #1146 #1147 #1149 #1150 #1151, plus step 2
of #1143 (steps 3 and 4 remain open, re-measured).

#1145 had to land first: `cargo fmt --all` could not run inside a git
worktree at all, so the canonical gate was unavailable in the isolation
mode four skills document as their default.

Highlights: `bca ops` now opens spaces on the same predicate as
`bca metrics`, so Elixir gets per-function vocabularies (#1130); ABC
counts a ternary's operand slots in ten more languages, moving
`a ? !b : !c` from 1 to 4 (#1102, 266 snapshot files); Perl signatures
and Elixir arguments are counted rather than silently zero (#1147,
#1142); a per-worker blame session takes `--vcs-per-function` from
2.82 to 1.61 CPU-seconds (#1117); the scaling gate gained a width axis
and caught a second quadratic (#1133, #1106); and the test suite went
from 77 targets and 13 GB of binaries to 21 and 1.7 GB (#1124).

Every issue in the range contained at least one factual error found only
by measurement, and several would have shipped a wrong fix if taken at
face value — #1102's "uniform 0/2/4 grammar layout" (three field names,
two optional slots), #1142's "take the `arguments` field" (Elixir `call`
has only `target`), #1146's zero-code workaround (verified with a
relative path; both shipped hooks pass absolute ones), and #1126's
premise (superseded by nextest before it was written). Corrections are
recorded on each issue.

Sibling sweeps filed #1159-#1166 for defects of the same class found in
languages and seams outside this range.
#1141 is a user-facing feature and #1106 a performance change; the
other six are test and bench infrastructure. All were reported by their
implementing agents and never consolidated.
@codecov

codecov Bot commented Aug 1, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.27586% with 17 lines in your changes missing coverage. Please review.
✅ Project coverage is 98.11%. Comparing base (1c08659) to head (462bbac).
⚠️ Report is 52 commits behind head on main.

Files with missing lines Patch % Lines
src/vcs/git/blame.rs 91.52% 4 Missing and 1 partial ⚠️
src/metrics/abc/js_family.rs 89.47% 1 Missing and 1 partial ⚠️
src/metrics/abc/perl.rs 88.23% 1 Missing and 1 partial ⚠️
src/metrics/tokens.rs 89.47% 2 Missing ⚠️
src/test_support.rs 96.92% 2 Missing ⚠️
src/metrics/abc/cpp.rs 93.75% 1 Missing ⚠️
src/metrics/abc/php.rs 94.11% 1 Missing ⚠️
src/metrics/nargs.rs 99.74% 1 Missing ⚠️
src/ops.rs 98.21% 0 Missing and 1 partial ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #1173      +/-   ##
==========================================
- Coverage   98.18%   98.11%   -0.07%     
==========================================
  Files         276      276              
  Lines       69472    70238     +766     
  Branches    69042    69808     +766     
==========================================
+ Hits        68211    68914     +703     
- Misses        858      907      +49     
- Partials      403      417      +14     
Flag Coverage Δ
python 100.00% <ø> (ø)
rust 98.10% <98.27%> (-0.08%) ⬇️

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

Files with missing lines Coverage Δ
src/checker/perl.rs 81.81% <ø> (+18.18%) ⬆️
src/comment_rm.rs 95.48% <100.00%> (+0.17%) ⬆️
src/count.rs 90.47% <100.00%> (-0.44%) ⬇️
src/find.rs 100.00% <100.00%> (ø)
src/function.rs 87.50% <ø> (ø)
src/metric_set.rs 99.02% <ø> (ø)
src/metrics/abc.rs 99.59% <100.00%> (-0.03%) ⬇️
src/metrics/abc/c.rs 86.66% <100.00%> (+0.95%) ⬆️
src/metrics/abc/mozcpp.rs 86.66% <100.00%> (-1.43%) ⬇️
src/metrics/abc/objc.rs 93.87% <100.00%> (-1.78%) ⬇️
... and 49 more

... and 16 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.

dekobon added 2 commits August 1, 2026 12:43
Both are dead under a non-default cfg and the workspace builds with
`RUSTFLAGS=-D warnings`, so each failed a CI leg that a default local
build cannot reach:

- `MetricSet` in `spaces_tests.rs` is used only by the
  `#[cfg(feature = "rust")]` parity test, failing
  `features (no-default-features (lib))`.
- `crate::common` in the CLI's `read_failures.rs` is re-exported only
  for its `#[cfg(unix)] mod unix`, failing `test (windows-latest)`.
Its hand-written `Debug` had no test: the impl exists so a config dump
carries the memo count instead of the `gix::Repository` handle and the
mailmap, and nothing held it to that.

Both assertions are positive, and the count is seeded to 2 first
because a fresh session reports 0 — the same value a constant would.
Verified by perturbation: `finish_non_exhaustive` to `finish`, and the
memo length to a constant, each fail this test alone.
@dekobon
dekobon merged commit 462bbac into main Aug 1, 2026
55 checks passed
@dekobon
dekobon deleted the fix/batch-1090-1151 branch August 1, 2026 21:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

metrics/cognitive: Python boolean-ancestor stop set is unobservable — dead or untested?

1 participant