fix: batch resolution of #1090-#1151 (21 issues) - #1173
Merged
Conversation
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
…7-31-2 # Conflicts: # .bca-baseline.toml
`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).
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.
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ 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
Flags with carried forward coverage won't be shown. Click here to find out more.
🚀 New features to boost your workflow:
|
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.
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.
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 --allcould not run inside a git worktree at all, somake pre-commitdied 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 —enumsfails identically and takes three more gate stages down with it.Correctness
a ? !b : !cmoves 1 → 4, matching Java/C#/Groovy. 266 snapshot files, all values upward, submodule pushed.bca opsopens function spaces on the same source-aware predicate asbca metrics, so Elixirdefmodule/deffinally get per-function vocabularies instead of a file-level one. New exhaustive-match-on-LANGparity test between the two walks.nargscounts Elixir arguments and Perl signatures rather than reporting 0 for every function.NArgs::computegainscode: &[u8], the last metric that lacked it.defs no longer inherit the enclosing conditional nesting, so a function in anifscores the same as its byte-equivalent elsewhere — the contract the book already documented.bca checkpreviously reported clean on a tree it could not fully read.bca dump | headwas also already broken and now exits 0.Performance
--vcs-per-functionover this repo'ssrc/goes 2.82 → 1.61 CPU-seconds. Most of the win is the object cache, whichinto_sync/to_thread_localhad been silently dropping; that gix chain is now pinned by a test.O(children × vocabulary)→O(vocabulary)).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:
0/2/4grammar layout. Three different field names, two optional slots — a fixedchild(4)scoresa ?: !bas 2 instead of 3.callnode'sargumentsfield. It has onlytarget; the first implementation measured 0 for every function.nargs7→6 costs 73 baseline entries for zero gain. Not taken.yieldis not the only route to the observable one.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-commitgreen on the merged tree (4,979 tests),make chain-auditgreen, all 24make bench-scalingprobes within bound, no.snap.newdrift, submodule SHAfed6e69ereachable on its remote.