Skip to content

perf(test): assert six rules with six rules, not with all 103 — twice over - #834

Merged
wenzowski merged 3 commits into
mainfrom
claude/ci-performance-degradation-ic29ck
Sep 2, 2026
Merged

wenzowski merged 3 commits into
mainfrom
claude/ci-performance-degradation-ic29ck

Conversation

@wenzowski

@wenzowski wenzowski commented Sep 2, 2026 •

Copy link
Copy Markdown
Contributor

Closes CLOUD-1358

644s of a 1693s suite was two tests

the_committed_portability_rules_fire_on_every_banned_shape and
the_committed_repo_agnosticism_rules_fire_on_every_banned_shape each ran enforce
over the whole 103-row committed ruleset — twice, for the banned-shape arm and its
clean-fixture discriminator — to assert 6 and 4 rows.

On the 2-core Windows runner: 324.85s and 319.12s, against a next-slowest case of
25.13s. That is 0.1% of 3868 cases carrying 38% of summed CPU, and the longer of the
two sets an asymptotic floor that makes sharding the job useless.

before after
portability 324.85s 0.128s
agnosticism 319.12s 0.119s

--rule was not actually repeatable

Both CHECK_RULE and ENFORCE_RULE declared ValueDecl::Str, whose own doc records
that ArgAction::Set keeps only the LAST occurrence — so a second --rule was
silently discarded rather than refused. ValueDecl::StrMany already existed for
exactly this.

select_rules refuses per id: a --rule naming no declared row is a usage error
naming the first unmatched id, so a typo in a narrowed test cannot pass vacuously.
Selection preserves declaration order rather than argument order, so stdout stays
byte-stable under a reordered command line (§6). Engine-side findings are emitted only
on the unnarrowed path, where they are still the whole tree's answer.

Shown able to fail

Dropping one id from each set reddens each case — portability on no-gnu-sed-z,
agnosticism on no-consumer-account-literal, the latter with one sorted pointer per banned shape per file, and nothing else. Both arms keep their clean-fixture
discriminator; the case count does not fall.

The rule sets are named once as consts rather than spelled at all four call sites. That
is not only the too_many_lines fix: the four sites were a drift surface the narrowing
itself created, and a discriminator whose set diverged from the arm it discriminates
would prove nothing about the set that arm ran.

Coverage lost, stated rather than buried

The unnarrowed enforce also proved the other 97 rows emit nothing on that fixture.
That is not what either case's name asserts and sibling cases over the committed bytes
still cover it — but it is a loss, not a free win.

What came out of this PR, and why

CLOUD-1349 (stale mediator detection) was implemented here and backed out before
landing
. The check was correct and its placement was not: it compared the binary on
PATH against target/release/batten, which made doctor answer differently depending
on when install:local last ran — a property of the world inside a gate that asserts a
property of the commit, which is the lock-check defect .claude/rules/toolchain.md
already records. land's own verify caught it via this_repository_is_healthy. The
full reasoning, and the finding that version equality provably does not discriminate a
stale mediator, are recorded on CLOUD-1349, which is back in Todo.

Verification

mise run verify → fast-forward-green on the pre-drop head; suite 4129/4129;
perf-compare within 1.30x of the merge base. Re-verified after the drop by land.

🤖 Generated with Claude Code

https://claude.ai/code/session_01CTqon6Q8TXmhGpdtQeaT6V

@linear-code

linear-code Bot commented Sep 2, 2026 •

Copy link
Copy Markdown
CLOUD-1358 Two `cli.rs` cases are 644s of the Windows suite's 1693s — 38% — and they set an asymptotic floor no sharding can pass

Why

Measured on the windows job of run 33659390795 (head 8c95e026, 2026-09-02), parsing per-case durations out of the job's own log — 3868 cases, 1692.8s summed CPU, 849.9s wall across 4 workers:

case windows
cli::the_committed_portability_rules_fire_on_every_banned_shape 324.85s
cli::the_committed_repo_agnosticism_rules_fire_on_every_banned_shape 319.12s
next slowest (pointer_only::no_verb_emits_content_it_merely_read) 25.13s

Two cases are 644.0s — 38% of the suite. The distribution is even more extreme than CLOUD-1223's: 25% of summed CPU is 2 cases (0.1%), 50% is 62 cases (1.6%).

Both are the shape CLOUD-1223 named and this pair escaped. Each reads the committed batten.toml, builds a fixture, and runs the whole 103-rule enforce ruleset — twice, over a dirty fixture and a clean one — to assert 6 rules (portability) and 4 rules (agnosticism). Four full evaluations at ~160s each on a 2-core runner.

CLOUD-1223's closing acceptance bullet is refuted for these two. It reads: "The four cli.rs cases named in the title are NOT individually fixed and do not need to be. Their headline share was inflated 4.7x by contention (39.15s in-suite against 8.37s isolated)." That was measured at 4 cores in a container. On the runner that actually gates landing they are 325s and 319s. The contention explanation does not survive the measurement.

The estimate that deferred this was wrong by 18x, and recording that is the point. These two were examined during PR #819 and declined as "~18s each, worth ~36s" — a local 4-core reading — with the trade stated as "saves ~9s each and costs the cross-rule sort-order property". The real figure is 644s.

They set the suite's asymptotic floor, which kills the sharding lever. A partitioned suite cannot finish faster than its slowest single case: 324.85s. cargo nextest --partition on windows — newly affordable since the cache fix took compile to 1 crate (commented on CLOUD-1225) — cannot take the job below that floor. Removing these two is the precondition for sharding being worth anything at all.

Refinement — Ready

  • Source of truth (§1). Per-case durations from the windows job log of run 33659390795, read via gh api repos/{owner}/{repo}/actions/jobs/{id}/logs with ANSI escapes stripped before counting. The two cases in crates/batten/tests/it/cli.rs. Not a local container reading — that instrument is what produced the 18x error above.
  • **Computable predicate (§2). **--rule becomes repeatable on both check and enforce, so a case naming N rules evaluates exactly those N. Both arms of both cases narrow: the dirty arm to the rules under test, the clean arm to the same set.
  • Full stdout equality survives, which is what makes this a narrowing rather than a weakening. The property the cases exist for is that findings sort by the (path, line, rule) tuple and every one of the 6 (or 4) rules contributes its line — a contains assertion would pass with five of six gone. Selecting exactly those rules preserves the ordering among them; what is lost is only the incidental proof that the other ~97 rows are silent on that fixture, which is not what either case's name asserts and which sibling cases over the committed bytes still cover.
  • Deliberately not in scope (§2). Deleting or weakening any case; the case count must not fall and assertions-not-gutted / tests-not-deleted stay green. Sharding windows — a separate lever, and one this row is the precondition for.
  • **Effect (§3). **read for the narrowing. The --rule arity change is a surface change; CheckFlags.rule moving from Option<String> to a repeatable form is an API break and must be declared, the way Enforce(EnforceFlags) was in 13950e21.
  • Output & exit (§5). Unchanged.
  • **Commit / bump (§6). **perf(test) for the cases, and the surface change declares its break.
  • Test obligation (§7). Shown able to fail per CLOUD-418: with any one of the named rules removed from batten.toml or set to severity = "allow", the asserted stdout bytes must still change. A --rule naming no declared row stays a usage error, never a clean run over nothing — the property CHECK_RULE already carries.
  • Blockers (§8). None. relatedTo CLOUD-1223 (the row whose closing bullet this refutes), CLOUD-1225 (where the sharding refutation and the cache measurement live), CLOUD-1208 (the harness and its null), CLOUD-418.

Acceptance

  • Both cases drop below 1s on the windows job, measured from that job's own log rather than locally.
  • windows summed CPU falls from 1692.8s to ~1049s; wall across 4 workers from ~850s toward ~262s.
  • The new asymptotic floor is named, so the next reader knows what sharding could now buy.
  • Every rule each case names still contributes its own line to the asserted bytes.

Generated by Claude Code

CLOUD-1349 The mediator ran 16 versions behind the tree it was judging, and nothing reported it — a stale engine reads exactly like a working one

§1 Subject

The absence of any comparison between the batten binary the hook registrations resolve and the workspace version of the tree it is judging. crates/batten/src/doctor.rs is where that comparison is missing, and it is the whole of this row's authority boundary.

The session-start handler that installs the binary is evidence in §2 rather than subject here, deliberately: its absence is what PRODUCED the mismatch on one container, and the row is about the mismatch being undetectable rather than about any one cause of it. A handler that runs perfectly still leaves a stale binary wherever the image shipped one.

§2 The measurement

Measured 2026-09-02 in a Claude Code web container, over one full session landing CLOUD-1321.

batten --version on PATH (/root/.local/bin/batten) 0.0.121
workspace version in the tree 0.0.137
/tmp/session-start-batten.log does not exist

mise.toml:1987 runs mise run install:local into that log and ::error::s on failure. The log's absence means the step never ran at all — so there was no failure to report, and the container kept whatever binary its image was baked with.

The engine mediated every tool call in that session against a rule table 16 versions old, and nothing anywhere said so.

§3 What that silently disabled, measured by the moment it stopped

The tell is the step change at the install, not an argument. Across roughly six hours the session issued dozens of cargo run … | tail, grep over tracked paths, and cmd; echo $? lists. Not one was refused.

mise run install:local was run to repair an unrelated failure (ci-wait dying on unrecognized subcommand 'pr', itself a symptom of the same staleness). Within five minutes of the binary reaching 0.0.137, the engine refused three calls:

  • verdict read dropped — verdict-not-discarded, a verdict-bearing command piped into tail;
  • verdict carry other — verdict-not-discarded, a ; list after a verdict-bearing command;
  • tool run loose … no-tool-substitution — grep over tracked repository paths.

All three shapes had been issued repeatedly before the install and had passed. The rules were in batten.toml the whole time; the binary reading them was not.

§4 Why this is the worst shape in the model rather than an ordinary bug

.claude/rules/policy-modules.md states the class this repository is organised around: a dead gate and a clean tree are byte-identical on the decision surface. Every mechanism here defends against it — input.tree.missing is a channel rather than an absence, NotAcquired keeps Absent and Unparsed apart, RuleSkipped is reported rather than folded into clean, a [[verdict]] nothing raises fails the load.

A stale mediator defeats all of it at once, one layer beneath where any of it can look. The config is correct, the modules are correct, the tests are green, and the process actually adjudicating calls is enforcing a different, older contract. Silence from the hook is the documented signal that it IS mediating (.claude/rules/toolchain.md: "Silence still means it is mediating") — and that sentence is exactly what makes this undetectable by a reader.

doctor is where the check belongs and it is not there. CARGO_PKG_VERSION appears in eight source files; doctor.rs is not one of them. doctor already asserts the three provisioning steps landed (CLOUD-476) — this is the fourth, and it is the one that decides whether the other three were asserted by the right binary.

§5 Why the existing sensors did not catch it

  • session-start:batten guards its own failure and not its own absence. A step that runs and fails writes a log and an ::error::; a step that never runs writes nothing, and nothing reads for it.
  • The unrecognized subcommand symptom is swallowed by design. target-prune and ci-wait both probe command -v batten and fall back, so a missing verb prints a tip: line into a task log and the task carries on. Those lines were visible in every land log in this session and read as noise.
  • doctor hooks answers whether registrations REACH the engine, never which engine. It reported 5 harness(es), 0 unwired over a 0.0.121 binary.

Refinement — Ready (the engine states which contract it is enforcing)

Refinement gate: Definition of Ready & Done. This body carries only specializations.

{
  "source_of_truth": "batten --version reported 0.0.121 while the workspace was 0.0.137, /tmp/session-start-batten.log did not exist so mise.toml:1987 never ran, and CARGO_PKG_VERSION appears in eight source files of which doctor.rs is not one",
  "gate": { "task": "doctor", "exits": [0, 1] },
  "commit_type": "fix",
  "blockers": [],
  "tests": [
    { "file": "policy/harness-wiring.rego", "mutation": "stray-unread" },
    { "file": "policy/harness-wiring.rego", "mutation": "stale-unguarded" }
  ]
}
  • **Authority boundary (§1). **doctor.rs grows the comparison; whatever batten.toml row declares it. The session-start handler's absence-vs-failure asymmetry is the same row's second half and may split.

  • **Computable predicate (§2). **batten doctor resolves the binary the registrations name, reads its --version, compares it to the workspace version of the tree being judged, and refuses on a mismatch naming both. A binary that cannot be resolved is could-not-look and says so, never clean.

  • Deliberately not in scope (§2). Auto-repair. doctor reports; mise run install:local is the remedy the refusal names, which is the routing CLOUD-1339 is separately about.

  • **Effect (§3). **read, plus resolving one binary's --version.

  • Test obligation (§7). A compiled-binary tier planting an older binary at the resolved destination and asserting doctor refuses naming both versions — shown able to fail by planting a matching one. The anti-vacuity mirror matters here more than usual: a case that never plants a mismatch passes over a check that does nothing.

    The claims above name EXISTING mutations deliberately, and the reason is a defect this row itself caused. The first draft claimed crates/batten/src/doctor.rs:stale-engine-passes and policy/harness-wiring.rego:unversioned-mediator-passes. Neither can bind: obligations-bound requires the declared slug to be a #MUTANT row in the declared file, #MUTANT is a # comment so it cannot live in a .rs file at all, and the second slug does not exist. batten check refused the filing branch with policy/harness-wiring.rego obligation-unbound — the gate catching a promise nothing could join. The claims now name the two mutations the implementing PR must keep CAUGHT, which is checkable today; the unbuilt case is described in this clause, which is where an obligation with no mechanism yet belongs.

  • Blockers (§8). None. relatedTo CLOUD-476 (the provisioning doctor already asserts), CLOUD-824 (a missing binary fails open and reports at provisioning time — this is the same argument for a WRONG one), CLOUD-1339, CLOUD-1340.

Acceptance

  • doctor refuses when the resolved binary's version differs from the tree's, naming both.
  • A session whose install:local never ran is told at provisioning time rather than six hours later by an unrelated unrecognized subcommand.
  • Shown able to fail.

§6 Cost of not doing it

Measured, this session: every mediated call for ~6 hours adjudicated against a 16-version-old table, three rule classes provably unenforced, and the discovery arriving only because an unrelated task reached for a subcommand that did not exist. A session that never happened to do that would have finished believing it had been mediated throughout — and would have been right about the silence and wrong about what it meant.


Found while landing CLOUD-1321 (PR #828).

Review in Linear

@coderabbitai

coderabbitai Bot commented Sep 2, 2026 •

Copy link
Copy Markdown

Warning

Review limit reached

Next included review available in 29 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used the included review currently available.

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

Learn how review limits work.

Review configuration:

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Free

Run ID: 97ccfd95-3930-440d-9701-9d7f2c3332b2

📥 Commits

Reviewing files that changed from the base of the PR and between 8f1f92f and dd9a3da.

⛔ Files ignored due to path filters (1)
  • crates/batten/tests/it/snapshots/it__snapshots__golden_json_schema.snap is excluded by !**/*.snap
📒 Files selected for processing (8)
  • completions/batten.fish
  • completions/batten.zsh
  • crates/batten/src/cli.rs
  • crates/batten/src/lib.rs
  • crates/batten/src/surface.rs
  • crates/batten/tests/it/cli.rs
  • man/batten-check.1
  • man/batten-enforce.1

Note

🎁 Summarized by CodeRabbit Free

Your organization is on the Free plan. CodeRabbit will generate a high-level summary and a walkthrough for each pull request. For a comprehensive line-by-line review, please upgrade your subscription to CodeRabbit Essentials by visiting https://app.coderabbit.ai/settings/billing.

Comment @coderabbitai help to get the list of available commands.

@wenzowski wenzowski changed the title perf(test): assert six rules with six rules; and decide which engine mediates perf(test): assert six rules with six rules, not with all 103 — twice over Sep 2, 2026
… over

`the_committed_portability_rules_fire_on_every_banned_shape` and
`the_committed_repo_agnosticism_rules_fire_on_every_banned_shape` each ran
`enforce` over the whole committed ruleset — twice, for the banned-shape arm and
the clean-fixture discriminator — to assert 6 and 4 rows respectively. On the
2-core Windows runner they cost 324.85s and 319.12s: 644s of 1692.8s summed CPU
across 3868 cases, so 0.1% of the suite is 38% of its cost, and the longer of the
two sets the asymptotic floor that makes sharding useless.

Narrow each arm to the rows it asserts. That needs `--rule` to be repeatable,
which it was not: both `CHECK_RULE` and `ENFORCE_RULE` declared
`ValueDecl::Str`, whose own doc records that `ArgAction::Set` keeps only the LAST
occurrence — so a second `--rule` silently discarded the first rather than
refusing. `ValueDecl::StrMany` already existed for exactly this; both flags take
it, and `CheckFlags`/`EnforceFlags` carry `Vec<String>` read with `get_many`.

`select_rules` takes `&[String]` and refuses PER ID: a `--rule` naming no
declared row is a usage error naming the first unmatched id, so a typo in a
narrowed test cannot pass vacuously. Selection preserves declaration order rather
than argument order, so output stays byte-stable under a reordered command line
(house-style §6). Engine-side findings are emitted only on the unnarrowed path,
where they are still the whole tree's answer.

Both arms of both cases keep their clean-fixture anti-vacuity discriminator and
their exact-stdout assertions; the case count does not fall.

Measured locally: 0.129s and 0.106s.

Refs: CLOUD-1358, CLOUD-1223, CLOUD-1225
…-rule

`ValueDecl::StrMany` changed both flags' help line, which the committed
completions, man pages and golden schema all carry verbatim. Generated by
`mise run schema`, `completions` and `man` — no hand edits.

Refs: CLOUD-1358
…our sites

The narrowing spelled `--rule <id>` inline at all four call sites — a
banned-shape arm and a clean-fixture discriminator per case — which pushed
`the_committed_portability_rules_fire_on_every_banned_shape` to 116 lines
against `clippy::too_many_lines`'s 100 and broke `lint:clippy`. `test:cargo`
does not run clippy, so the suite was green over it.

Two consts and a fold. Beyond the lint, the four sites were a drift surface the
narrowing itself created: a discriminator whose rule set diverged from the arm
it discriminates proves nothing about the set that arm ran.

Both cases still pass — 0.128s and 0.119s — and both were shown able to fail by
dropping one id from each set.

Refs: CLOUD-1358
@wenzowski
wenzowski marked this pull request as ready for review September 2, 2026 20:23
@wenzowski
wenzowski force-pushed the claude/ci-performance-degradation-ic29ck branch from 0f4c7d9 to dd9a3da Compare September 2, 2026 20:23
@sonarqubecloud

sonarqubecloud Bot commented Sep 2, 2026

Copy link
Copy Markdown

❌ The last analysis has failed.

See analysis details on SonarQube Cloud

@wenzowski

Copy link
Copy Markdown
Contributor Author

/fast-forward

@wenzowski
wenzowski merged commit dd9a3da into main Sep 2, 2026
10 of 11 checks passed
@wenzowski
wenzowski deleted the claude/ci-performance-degradation-ic29ck branch September 2, 2026 20:43
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.

1 participant