Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
52 commits
Select commit Hold shift + click to select a range
c7e42f8
refactor(bench): rename Probe.depths to sizes
dekobon Aug 1, 2026
5b0c163
fix(build): root each excluded crate in its own workspace
dekobon Aug 1, 2026
f0edcf5
chore(grammars): pin vendored grammar deps with = requirements
dekobon Aug 1, 2026
78e0a38
fix(gates): parse manifests with tomllib in check-excluded-manifests
dekobon Aug 1, 2026
ce3c8ad
fix(cli): fail, not panic, when stdout cannot be written
dekobon Aug 1, 2026
07e968d
test(bench): add a width-axis probe family to the scaling gate
dekobon Aug 1, 2026
da7784f
fix(cognitive): correct inverted Python boolean-walk doc, test arm
dekobon Aug 1, 2026
d6fbb4b
Merge branch 'worktree-agent-ad9bfff4b42d4b987' into fix/batch-2026-0…
dekobon Aug 1, 2026
2272d70
Merge branch 'worktree-agent-a6ad9698d62213581' into fix/batch-2026-0…
dekobon Aug 1, 2026
f219e11
Merge branch 'worktree-agent-a79d94f097a2dd1a7' into fix/batch-2026-0…
dekobon Aug 1, 2026
edd4b38
refactor(cli): drop the two-step stdout lock, shorten Stdio paths
dekobon Aug 1, 2026
8bf8b9b
fix(metrics/cognitive): reset nesting at a Python def boundary
dekobon Aug 1, 2026
ce12098
fix(cli): fail the walk when a directory cannot be listed
dekobon Aug 1, 2026
30556fc
fix(cli): flush stdout so vcs failures exit 1
dekobon Aug 1, 2026
3725142
Merge branch 'worktree-agent-a5a17281c2f92e039' into fix/batch-2026-0…
dekobon Aug 1, 2026
b1fe886
Merge branch 'worktree-agent-a72eabb4269186c69' into fix/batch-2026-0…
dekobon Aug 1, 2026
fd98455
perf(vcs): hold one blame session per worker thread
dekobon Aug 1, 2026
cacb37e
Merge branch 'worktree-agent-a08a81c7421af542f' into fix/batch-2026-0…
dekobon Aug 1, 2026
40ebdd5
fix(metrics/nargs): count Perl subroutine signatures
dekobon Aug 1, 2026
4501153
fix(abc): count ternary operand slots as unary conditions
dekobon Aug 1, 2026
f3ff0ca
Merge branch 'worktree-agent-a8615171d2d7c0fed' into fix/batch-2026-0…
dekobon Aug 1, 2026
00ef2b5
fix(metrics/nargs): count Elixir function arguments
dekobon Aug 1, 2026
0452510
Merge branch 'worktree-agent-a50d6ba0afcdec740' into fix/batch-2026-0…
dekobon Aug 1, 2026
035e079
chore(self-scan): refresh baseline line numbers after wave-4 merges
dekobon Aug 1, 2026
56db8ce
fix(ops): open spaces with the source-aware predicate
dekobon Aug 1, 2026
14e25ec
perf(spaces): finalize parent Halstead/MI once, not per child
dekobon Aug 1, 2026
e72d815
Merge branch 'worktree-agent-a52bc43d8d3199d15' into fix/batch-2026-0…
dekobon Aug 1, 2026
98d108c
fix(check): report when a named path overrides an exclude
dekobon Aug 1, 2026
f468d88
Merge branch 'worktree-agent-a235358faf7595d44' into fix/batch-2026-0…
dekobon Aug 1, 2026
6abae2b
fix(walk): case-fold the seed's language lookup
dekobon Aug 1, 2026
2828dda
fix(cli): exempt BrokenPipe from the vcs write path
dekobon Aug 1, 2026
0041064
test(cli): stop two guards skipping where they should run
dekobon Aug 1, 2026
7d3decc
docs(cognitive): document Python's per-lambda boolean surcharge
dekobon Aug 1, 2026
869910b
test(vcs): shrink the per-function perf fixture and its budget
dekobon Aug 1, 2026
f81c347
Merge branch 'worktree-agent-a5da92889f55a4f7c' into fix/batch-2026-0…
dekobon Aug 1, 2026
d5b5760
refactor(cognitive): extract the shared function-boundary rule
dekobon Aug 1, 2026
fe4ea3e
Merge branch 'worktree-agent-afd58fcfae8613407' into fix/batch-2026-0…
dekobon Aug 1, 2026
9f18fcf
chore(self-scan): converge abc onto the shipped default
dekobon Aug 1, 2026
25e14fb
feat(config): per-language [thresholds] overrides in bca.toml
dekobon Aug 1, 2026
2f99d3d
Merge branch 'worktree-agent-ab31f8db518503db2' into fix/batch-2026-0…
dekobon Aug 1, 2026
11daa14
test(perf): merge 68 integration test binaries into 12 drivers
dekobon Aug 1, 2026
c66cd87
perf(test): compute one metric family per per-metric test
dekobon Aug 1, 2026
0f1433d
Merge branch 'worktree-agent-abb956415b110b315' into fix/batch-2026-0…
dekobon Aug 1, 2026
6de2a18
test(cli): serve the shared CLI fixtures from one directory
dekobon Aug 1, 2026
9ae62da
Merge branch 'worktree-agent-a44bcbc6abc8c63cb' into fix/batch-2026-0…
dekobon Aug 1, 2026
da267f0
docs(rules): record that zsh does not word-split parameter expansions
dekobon Aug 1, 2026
b437570
perf(node): hoist cursors out of the last per-node walks, guard all six
dekobon Aug 1, 2026
a2ded22
refactor: fold the last staging-Vec child pushes into children_with
dekobon Aug 1, 2026
4009665
Merge branch 'fix/batch-2026-07-31-2'
dekobon Aug 1, 2026
7574d4c
docs(changelog): record the eight unlogged entries from the batch
dekobon Aug 1, 2026
985b0ff
fix(test): gate two imports on their only consumer's cfg
dekobon Aug 1, 2026
462bbac
test(vcs): pin BlameSession's Debug contract
dekobon Aug 1, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
180 changes: 90 additions & 90 deletions .bca-baseline.toml

Large diffs are not rendered by default.

26 changes: 16 additions & 10 deletions .bcaignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,22 @@
# `.bca-baseline.toml` in sync — the baseline keys are sensitive to
# which files the walker visits.
#
# This file is *walk* scope: what gets analyzed at all. It is not the
# place to exempt something from the threshold gate — an explicitly
# named path overrides these globs by design (the ripgrep convention;
# see `commands/README.md`), so a per-file caller such as the agent
# hooks in `.claude/hooks/` and `.opencode/plugins/` bypasses every
# entry here and reports offenders in files `make self-scan` correctly
# ignores. Gate-scope exemptions belong in `bca.toml`'s
# `[check] exclude`, which survives an explicit path (#1146).
#
# So an entry earns its place here only when the files should never be
# measured: build output, vendored or generated trees, prose, and the
# large third-party corpora under `tests/repositories/`. Dev tooling we
# author but do not ship — `utils/`, `xtask/`, `enums/`, the benchmark
# harness, the editor hooks — moved to `[check] exclude` in #1146,
# because "do not gate this" was always the actual intent.
#
# Patterns use the `./`-prefix convention. The walker re-anchors every
# seed to that form (#488), so these match the same files regardless of
# whether the root was spelled `.`, `$PWD`, or a manifest-resolved
Expand All @@ -21,20 +37,10 @@
# test modules (#1066). It is not a sibling unit-test file, so the
# `*_tests.rs` glob does not reach it, and none of it ships.
./**/test_support.rs
./enums/**
./.opencode/**
./target/**
./big-code-analysis-book/**
./docs/**
./packaging/**
./utils/**
./xtask/**
# Dev tooling, same treatment as ./xtask/**: the benchmark harness
# (#1068) ships nothing, and its report formatters are long runs of
# `writeln!(...)?` whose `nexits` counts say nothing about production
# maintainability.
./big-code-analysis-bench/**
./generate-grammars/**
./man/**
./abuild-keys/**
./**/examples/**
Expand Down
10 changes: 10 additions & 0 deletions .claude/hooks/bca-check.sh
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,16 @@
# model. Stays silent (exit 0) on a clean file, an unsupported file
# type, a tool error, or a missing analyzer — it must never block an
# edit.
#
# Scope note (#1146): this passes ONE named path, and a named path
# overrides every walker exclude by design — `.bcaignore`, `--exclude`,
# and the manifest `exclude` list all shape directory-walk scope, not
# gate scope. Files that must never be gated therefore belong in
# bca.toml's `[check] exclude`, which survives an explicit path. This
# repo's dev-tooling globs live there for that reason; `bca` warns on
# stderr, naming the glob, whenever a named path overrides a walker
# exclude, so a miswired entry shows up here rather than silently
# producing false offenders.
set -euo pipefail

root="${CLAUDE_PROJECT_DIR:-$PWD}"
Expand Down
96 changes: 96 additions & 0 deletions .claude/rules/shell.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
# Shell Rule

The `Bash` tool runs **zsh**, not bash (`echo $0` → `/usr/bin/zsh`).

## zsh does not field-split an unquoted parameter expansion

In POSIX sh and bash, `$var` unquoted is split on `IFS`. **In zsh it is
not.** The value arrives as a single word, however many spaces or
newlines it contains.

This is the one zsh/bash divergence that reliably produces a *wrong
answer rather than an error*, because the command still runs, still
exits 0, and still prints a plausible result.

Measured in this repository's shell:

| expression | zsh | bash |
| --- | --- | --- |
| `FLAGS="-p a -p b"; cmd $FLAGS` | **1 argument** | 4 arguments |
| `FILES=$(cat two-lines); for f in $FILES` | **1 iteration** | 2 iterations |
| `for f in $(cat two-lines)` | 2 iterations | 2 iterations |
| `cmd "${ARR[@]}"` where `ARR=(-p a -p b)` | 4 arguments | 4 arguments |

Note the third row. **Command substitution *is* split in zsh** — only
*parameter expansion* is not. So `for f in $(rg -l pattern)` behaves as
you expect, and the trap is specifically the intermediate variable:
assign the output first and the loop silently collapses to one
iteration. Do not "fix" the working form while chasing this.

## What it cost here

Two measurement loops during the #1090-#1151 batch, both of which
produced confident, uniform, entirely fabricated numbers.

Re-measuring #1143's threshold offenders:

```zsh
SCOPE="-p src -p big-code-analysis-cli/src -p big-code-analysis-web/src"
for spec in nargs=7 nargs=6 abc=50 cognitive=15; do
bca check --no-config --exclude-tests $SCOPE --threshold "$spec" …
done
```

`$SCOPE` reached `bca` as the single argument
`-p src -p big-code-analysis-cli/src -p big-code-analysis-web/src`,
which matched no path, so every row reported **0 offenders**. Seven
rows of zeros is a coherent-looking result — "the repo is already
compliant everywhere" — and it is the answer that would have shipped
had the number not been implausible enough to re-check. The array form
reported 19, 73 and 127.

## How to apply

- **Build argument lists as arrays, expand them quoted:**

```zsh
SCOPE=(-p src -p big-code-analysis-cli/src)
bca check "${SCOPE[@]}" --threshold cognitive=15
```

`"${ARR[@]}"` expands to one word per element in both shells. This is
the only spelling that is correct in zsh *and* bash, so prefer it even
in a script you think only zsh will run.

- **Iterate lines with `while IFS= read -r`, never through a variable:**

```zsh
while IFS= read -r f; do …; done < list.txt
rg -l pattern | while IFS= read -r f; do …; done
```

This also survives paths containing spaces, which the split forms do
not.

- **When a loop must reuse a captured list, capture into an array:**
`FILES=("${(@f)$(cat list.txt)}")` splits on newlines only, or just
re-run the command inside the `for`.

- **Sanity-check any measurement loop against a single hand-run case
before believing the table.** One `bca check -p src …` typed out in
full would have caught this immediately. A loop that emits a tidy
column of zeros deserves that check specifically, because zero is
what every one of these failure modes produces.

## Two siblings worth knowing

Both bit the same measurement in the same session, and both also yield
a plausible number rather than an error:

- **`$?` after a pipeline is the *last* stage's status.**
`cmd | head` reports `head`'s success even when `cmd` failed. zsh
spells the per-stage array `$pipestatus` (1-indexed); `PIPESTATUS`
is bash-only and expands to nothing here.
- **`bca check` writes offenders to stderr.** `2>/dev/null` on a check
invocation discards the entire result and leaves an empty stdout that
reads as "no offenders".
2 changes: 1 addition & 1 deletion .claude/rules/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@ they sit upstream of nearly every test in the workspace:

| Path | Normalisation |
| --- | --- |
| `check_metrics` → `test_support::check_func_space` | `trim_end().trim_matches('\n')`, then `push(b'\n')` |
| a module's `check_metrics` shim → `test_support::check_func_space_with` | `trim_end().trim_matches('\n')`, then `push(b'\n')` |
| integration suites → `read_file_with_eol` → `normalize_line_endings` | unconditional `data.push(b'\n')` |

Both guarantee a trailing newline, so **"a node ending at EOF" is
Expand Down
14 changes: 14 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -577,6 +577,20 @@ jobs:
# gate without surfacing the regression in CI.
- name: grammar-marker-sync self-tests (explicit)
run: python3 -m unittest -q utils/check-grammar-marker-sync-test.py
# Defensive twin for the workspace-exclusion gate (#1145, #1151):
# an excluded crate that roots no workspace of its own breaks
# `cargo fmt --all` inside every git worktree, and an unpinned
# tree-sitter dependency lets `cargo update` move a grammar
# silently. `make lint` already runs this gate; invoking it here
# (matching the twins above) keeps it enforced even if a future
# refactor drops it from the aggregate recipe.
- name: check-excluded-manifests (explicit)
run: python3 utils/check-excluded-manifests.py
# Run the gate's own unittests as their own explicit step
# so a refactor that breaks the script can't disable the
# gate without surfacing the regression in CI.
- name: check-excluded-manifests self-tests (explicit)
run: python3 -m unittest -q utils/check-excluded-manifests-test.py
# Defensive twin for the enums-codegen-drift gate (#405):
# running any grammar regen previously regenerated
# `src/c_langs_macros/*.rs` to a pre-optimization form
Expand Down
14 changes: 10 additions & 4 deletions .github/workflows/pages.yml
Original file line number Diff line number Diff line change
Expand Up @@ -189,13 +189,19 @@ jobs:
contents: read
security-events: write
actions: read
# The workspace-minus-vendored exclude list lives in `.bcaignore`
# at the repo root and is wired in via `exclude_from` in the
# auto-discovered `bca.toml` manifest, so the invocations below
# pick it up without an explicit `--exclude-from` flag. If you
# The walk deny-set — build output, vendored trees, prose, and the
# third-party corpora under `tests/repositories/` — lives in
# `.bcaignore` at the repo root and is wired in via `exclude_from`
# in the auto-discovered `bca.toml` manifest, so the invocations
# below pick it up without an explicit `--exclude-from` flag. If you
# edit `.bcaignore`, also refresh `.bca-baseline.toml` in the same
# commit — the baseline keys are sensitive to which files the
# walker actually visits.
#
# The manifest's `[check] exclude` is a separate, gate-only list
# (#1146): the dev-tooling trees named there are walked, so they
# appear in the reports and hotspot tables published below, and are
# exempt only from the threshold gate step.
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
Expand Down
10 changes: 10 additions & 0 deletions .opencode/plugins/bca-check.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,16 @@
// guidance text from .claude/hooks/bca-guidance.txt so the two hooks
// never drift. Both behaviours go beyond the book's bare-`bca` +
// inlined-string example (see the note at the bottom of this file).
//
// Scope note (#1146): this passes ONE named path, and a named path
// overrides every walker exclude by design — `.bcaignore`, `--exclude`,
// and the manifest `exclude` list all shape directory-walk scope, not
// gate scope. Files that must never be gated therefore belong in
// bca.toml's `[check] exclude`, which survives an explicit path. This
// repo's dev-tooling globs live there for that reason (this file
// included); `bca` warns on stderr, naming the glob, whenever a named
// path overrides a walker exclude, so a miswired entry surfaces here
// rather than silently producing false offenders.

import { existsSync, readFileSync, accessSync, constants } from "node:fs"
import { resolve, sep } from "node:path"
Expand Down
26 changes: 26 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,16 @@ repos:
entry: python3 -m unittest -q utils/check-versions-test.py
pass_filenames: false

# Self-tests for the workspace-exclusion gate. Runs on edits to
# the script or its test file (the unit tests import the module,
# so a logic change there must re-run them).
- id: check-excluded-manifests-test
name: check-excluded-manifests-test
language: system
files: '^utils/check-excluded-manifests(-test)?\.py$'
entry: python3 -m unittest -q utils/check-excluded-manifests-test.py
pass_filenames: false

# Sync-test for check-grammar-crate.py's EXTENSIONS table against
# src/langs.rs (#869). Re-runs when the script, its test, or the
# source-of-truth language table changes.
Expand Down Expand Up @@ -193,6 +203,22 @@ repos:
entry: python3 utils/check-versions.py
pass_filenames: false

# Workspace-exclusion gate — every crate in the root
# `[workspace] exclude` array must root its own workspace, or
# cargo's upward search escapes a nested git worktree and
# resolves against the main checkout. See #1145.
- id: check-excluded-manifests
name: check-excluded-manifests
language: system
# `tree-sitter-[a-z]+` missed `tree-sitter-c-sharp/` and
# `tree-sitter-mozjs2/` — any hyphen or digit in a crate
# directory took its manifest out of the hook's scope. Match
# any top-level directory, like the broad `Cargo\.toml`
# alternation the check-versions hook uses.
files: '(^Cargo\.toml|^[^/]+/Cargo\.toml|^utils/check-excluded-manifests\.py)$'
entry: python3 utils/check-excluded-manifests.py
pass_filenames: false

# Man-page packaging gate — every man/bca*.1 page (the top-level
# bca.1 and every bca-*.1 subcommand page) must appear in both
# the deb and rpm asset lists of its owning crate (bca-web.1 in
Expand Down
42 changes: 40 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,8 @@ and `cargo run -p big-code-analysis-web --`.
`check-snapshot-anchors.py`, `check-manpage-assets.py`,
`check-grammar-marker-sync.py`, `check-enums-codegen-drift.sh`,
`check-grammar-crate.py`, `check-grammars-crates.sh`,
`verify-name-only-churn.py`, and each gate's `*-test.py` self-tests.
`check-excluded-manifests.py`, `verify-name-only-churn.py`, and each
gate's `*-test.py` self-tests.
Each resolves the repository root from its own location
(`Path(__file__).resolve().parents[1]`) rather than the cwd, so it
runs correctly from anywhere; callers invoke them as `utils/<name>`.
Expand Down Expand Up @@ -131,6 +132,13 @@ and `cargo run -p big-code-analysis-web --`.

## Tool choice

- **Shell**: assistant tooling runs **zsh**, which does not field-split
an unquoted parameter expansion the way bash does — `cmd $FLAGS`
passes one argument, not several. The failure is silent and yields a
plausible result rather than an error, so it has already produced
fabricated measurement tables here. Build argument lists as arrays and
expand them `"${ARR[@]}"`. See
[`.claude/rules/shell.md`](.claude/rules/shell.md).
- **Code search**: `rg` (ripgrep). Never `grep` via Bash.
- **File search**: `fd` (or `fdfind` on Debian/Ubuntu). Never `find` via
Bash.
Expand Down Expand Up @@ -502,7 +510,37 @@ weigh them, do not drive them to zero at any cost.
## Tree-sitter grammars

External grammar crates are version-pinned (`=0.23.5`, `=0.26.10`,
etc.) in the root `Cargo.toml`. Treat the pinned version as fixed:
etc.) in the root `Cargo.toml` **and in each workspace-excluded crate's
own manifest** (the five vendored grammars plus `enums`) —
`utils/check-excluded-manifests.py` enforces both, so neither a root
pin nor a new vendored grammar can reintroduce a caret range (#1151).
Member crates take their grammars through `workspace = true` and carry
no requirement of their own. The gate reads manifests with `tomllib`,
so a literal string (`tree-sitter-cpp = '0.23.4'`) is checked like any
other, and it matches `tree-sitter` anywhere in a dependency name —
`dekobon-tree-sitter-groovy` and any future `bca-tree-sitter-*` are in
scope. A pin means exactly `=X.Y.Z` (whitespace after `=` is fine); a
compound requirement such as `=0.25.0, <0.26` is rejected, because the
`.grammar-marker-baseline.toml` entry that `check-grammar-marker-sync.py`
compares against can name only one version.

One deliberate exception: `tree-sitter-language`, the ecosystem's shared
`LanguageFn` trait shim, is **not** a grammar and must stay caret-ranged.
`=`-pinning it makes the workspace unresolvable (`tree-sitter-irules
0.1.1` requires `^0.1.7`, and cargo unifies 0.1.x deps) and would break
downstream consumers of the published `bca-tree-sitter-*` crates. The
gate carries it in `PIN_EXEMPT_DEPS`.

The `tree-sitter` runtime is **not** exempt, though the "not a grammar"
half of that rationale fits it too. The exemption is about unification
pressure and the runtime has none — the lockfile shows 25 crates
depending on `tree-sitter-language` against one external dependent
(`tree-sitter-perl`) for `tree-sitter` — and every manifest here already
pins it at `=0.26.11` with the workspace resolving. Its ABI version is
also what each vendored `parser.c` was generated against, so an
accidental bump is precisely the drift the gate exists to catch.

Treat the pinned version as fixed:

- Do not loosen pins to a range without explicit user approval.
- Bumping a grammar version is a deliberate, separate change — usually
Expand Down
Loading