0045 a console call carries its reason on the line - CyrilB1531/lodestar GitHub Wiki
0045 โ A Console call carries its reason on the line, not in an exemption list
Status: accepted ยท Date: 2026-08-21
Context
Ten Console calls under bench/ narrated a run: four banners naming the harness, one line
per measured row, one per length bucket, two -> path lines after a results file was written,
and a filtered: notice duplicating what metadata.filtered already carries into the JSON โ
where bench/compare.py refuses a filtered run outright.
None of them was in a timed region. Harness.Measure prints after its measurement loop closes;
PairsHarness prints after TimeBucket returns; BucketRouteDiagnostics prints inside
[GlobalSetup], which BenchmarkDotNet runs once and does not time. No [Benchmark] method in
the repository contains one. So no published number was ever wrong because of them.
That is precisely why they accumulated. Each was individually harmless, each was added by someone who wanted to see progress during a multi-minute run, and nothing was counting. A guard that only fires on damage would never have fired here.
src/ had none, and has never had any. A library that writes to a console its caller did not
open is deciding for an application it cannot see โ a rule this project has kept by everyone
happening to remember it.
Decision
check_no_console_writeline.py refuses a Console call under src/ outright, and one under
bench/ that does not carry a console-print: marker naming its reason.
The marker sits above the call or trails it, and an empty one is refused โ the same shape and
the same refusal as check_comment_length.py's long-comment:. Whether the reason is a good
one is a review's call, not the guard's, which is the division
0015 already draws for an analyzer suppression.
src/ gets no marker, and will not. There is no reason a shipped package should print that
would survive review, so offering a way to write one down invites the argument rather than
settling it. The guard is a ratchet on a tree that is already at zero.
No exemption list in the script, which is the decision this record exists for.
check_machine_paths.py's own docstring states the case against one: an exemption list that
grows is a guard being switched off one file at a time. A list in the script is edited by
whoever is annoyed by the guard, in a file the reviewer of the offending change has no reason to
open. A marker is written in the diff that needs it, on the line that needs it, in front of the
person who can refuse the reason.
Four calls carry a marker today, each holding something no file holds:
| call | what it says |
|---|---|
Program.cs โ Console.Error.WriteLine |
the benchmark loaded the wrong build, so every number it prints is meaningless |
Program.cs โ Console.WriteLine |
which assembly and framework it did load: the pair of the refusal above |
RocParallelBench.cs |
why a cell is missing from the table, rather than letting it vanish |
BucketRouteDiagnostics.cs |
the two group sizes its own two measured rows are read against |
Consequences
The guard is offline and reads only tracked sources, so it joins the hook rather than the
exclusions โ 0037 sets that rule, and
tools/tests/test_pre_commit_hook.py enforced it here: wiring the guard into CI and not into
.githooks/pre-commit failed that test immediately, naming the guard and the two places it
could go. Measured on the machine that ADR timed the others on: 0.05 s.
The loser is worth naming. A guard with no escape at all was the alternative, and it would have cost the four calls above โ a silently wrong build being the one this project can least afford, since #354, #356 and #351 were all defects that corrupted measurements while everything looked green. The marker keeps them and still stops the tenth banner.
What this does not claim: removing the ten changed no measurement. They were never in a timed region, and anyone reading the guard as a performance fix would be reading it wrong. What it buys is a CI log a reader can use and a habit that stops growing.