Code Audit (Deep)
Line-level audit. Companion to architectural-hotspots. Hotspots ranks
files by structural shape (fan-in, fan-out, LOC, cycles); this skill
reads files and emits specific, actionable findings with line numbers
and fix sketches.
The core failure mode this skill exists to prevent: producing vague
"consider refactoring X" advice instead of commit.rs:426 — clock sampled per element in hot loop; hoist or seed-and-increment. The
first is what a graph tool already said; the second is what the user
actually wanted.
Language scope
This skill is language-agnostic. The smells below are concepts that
recur across stacks; the parenthetical examples are illustrative for a
few common languages but never exhaustive. When you read a file, map
each smell to the equivalent construct in the target language:
- "fallible call return value discarded" covers
let _ = f() (Rust),
bare f() ignoring its error return (Go), try: f(); except: pass (Python), unawaited f() returning a Promise (TS / JS), _, _ = f() patterns (Lua / Go), f(); // ignore everywhere.
- "owned-when-borrow-suffices" covers
Vec<String> vs Vec<&str>
(Rust), []string copies vs slice aliases (Go), deep-copying lists
(Python), .slice() cloning vs index access (JS), std::string vs
std::string_view (C++).
- "lock-when-atomic-suffices" covers
Arc<Mutex<u64>> (Rust),
sync.Mutex for a counter (Go), threading.Lock around an int
(Python), Object.synchronized for a primitive (Java).
Never report a finding using a language construct the project does not
use. Substitute the project's equivalent before writing the fix sketch.
When to run
Use whenever the user wants findings inside code rather than rankings
across files. Triggers include:
- "audit
commit.rs" / "review plan.rs"
- "find perf issues in this module"
- "what could go wrong in
apply_changes?"
- "deep review of the parser"
- "where is this slow?"
- After running
architectural-hotspots and the user wants the next
level of detail on the flagged files.
Do not use when the user wants:
- Architectural overview / rankings → that's
architectural-hotspots.
- Style / formatting issues → clippy / eslint / ruff / staticcheck etc.
- Security review of specific CVE classes → a dedicated security skill
should run. Overlap is fine; this skill won't hunt for known CVEs.
- A single trivial bug fix in a known file — just fix it.
Method
The strength of this skill comes from depth, not breadth. Better to
audit three files thoroughly than ten superficially.
Find the project's record of already-rejected findings before
reading any code: DECISIONS.md, ADRs, a "rejection ledger" or
"verified negatives" section, *findings*.md, dismissed-issue
labels. Ask the user if nothing turns up and the repo looks mature.
Re-raising something the team already considered and rejected on
the record costs more credibility than the finding was ever worth,
and it is the fastest way to get an audit ignored wholesale. When
you drop a candidate for this reason, name the entry — that tells
the user their record is being read, not that you found nothing.
Pick targets.
- If the user named files, those are the targets.
- If not, and a recent hotspots report exists, take the top 3-5
files by combined signal (god + hub, god + tangle, or any file
inside a cycle).
- If neither, ask the user which files — not "should we audit?"
but "which files do you want audited?" so the answer drives
work.
Read every function body end to end. Headers and type
signatures are not enough. A finding requires a line number, and a
line number requires having read the line. Do not skip a function
because its name sounds boring or routine — apply_changes,
commit_one, cleanup, recover, flush often hold the
subtle ordering bugs. Skim-reading is the dominant failure mode of
this skill — defend against it actively by asking, for each
function, "if this had a bug, where would it be?".
Reading until the finding appears is a separate failure from
reading too little, and it feels like diligence. You read the
body, you found the line that confirms the bug, and you stopped —
one statement before the guard that made it safe. Reading
Motion::Last => adj.upper() and reporting an unbounded scroll is
correct up to the very next statement,
adj.set_value(to.clamp(lower, ceiling)), which disproves it. The
fix is a habit, not more effort: after a candidate appears, keep
reading to the end of the enclosing block and ask "what here would
make this not a bug?" — then answer it before writing the finding.
First pass: glyph sweep. Before going deep on novel findings,
do a fast scan at the language-glyph level — every smell below
lists concrete syntactic glyphs in several languages. These glyphs
are where the obvious bugs hide; missing them to chase one
interesting novel finding is a regression of attention. After the
glyph sweep produces its candidate list, then go deep on
higher-order concerns (durability ordering, coupling, axis-
specific structural issues).
Audit along the eight axes (next section). Most files only hit
three or four. Don't reach for findings in an axis that genuinely
doesn't apply.
Emit findings in the fixed format (later section). Categorized.
file:line. One-line symptom. One-line fix sketch. No paragraphs
of prose around each finding — the user is reading for signal.
Prove what you can; label the rest. For each finding, name the
command that would falsify it, and run it. If no such command
exists, mark the finding traced rather than proved. Both
are publishable — a traced finding backed by an honest label is
useful, a traced finding presented as measured is a liability.
The trap this closes is specific and common: running a real check
near the claim and letting it stand in for the claim. Reading
Motion::Last => adj.upper(), measuring how the toolkit clamps
adjustments, and reporting the finding verified — while the very
next statement was adj.set_value(to.clamp(lower, ceiling)) — is
a measurement of the wrong thing, and the label "verified" is what
makes it damaging. The question is never "did I run something?"
but "would this command have come out differently if the finding
were false?"
Before hand-rolling a harness, check whether the language already
has the tool: mutation testers, fuzzers, sanitizers, and coverage
tools mechanize a whole class of proof, and a replica you wrote
this afternoon is a slower substitute you also have to trust.
Verify what the tool actually does before citing it.
Pick the top 3-5 highest-leverage fixes out of the full list
and name them at the end. This is the deliverable the user will
actually act on first.
The eight axes
Each axis answers a different question. They are deliberately
orthogonal — a finding belongs in exactly one category.
1. Perf
What is making this slow that shouldn't be?
Smells:
- Syscalls / IO inside a per-element loop. File-system probes
(exists / stat / metadata / readdir), opens, reads, network
round-trips, DB queries — fine once, expensive per element.
Glyphs: Rust
std::fs::{metadata,exists,read} / Path::exists;
Go os.Stat / os.Open; Python os.path.exists / open();
JS fs.existsSync / fs.statSync; C stat() / open() /
access().
- Clock / RNG / time calls per element. Wall-clock or
monotonic-clock samples, RNG draws, UUID generations done per
iteration when one sample-and-increment would do.
Glyphs: Rust
SystemTime::now() / Instant::now() / rand::*;
Go time.Now() / rand.Int*; Python time.time() /
datetime.now() / random.* / uuid.uuid4(); JS Date.now() /
performance.now() / Math.random(); C clock_gettime /
gettimeofday / rand().
- Allocation in hot loop. Building a fresh container, formatting
a fresh string, copying a non-trivial value each iteration when a
reused buffer or borrowed view would do.
Glyphs: Rust
String::new() / Vec::new() / .to_owned() /
.clone() / format!(...); Go []T{} literal / make(...) /
fmt.Sprintf / string([]byte); Python list/dict comprehension /
f"..." / copy.copy / copy.deepcopy; JS array literal […] /
template literal in inner loop / Object.assign({}, …) / spread;
C++ std::string/std::vector constructor in loop body.
- Full re-scans where incremental would do. Re-running a regex
/ parse / hash over the entire post-image to detect changes that
could be tracked at write-time. Re-walking a tree that was just
walked.
- Redundant parse / compile. Same input parsed twice. Same
pattern compiled in two code paths. Same query / plan built per
call instead of cached.
Glyphs: regex
Pattern::compile / re.compile / new RegExp
inside a function called per request; SQL prepare per request;
tree-sitter Query::new / parser set_language per call.
- Forced materialization. Collecting a stream into a fully
materialised container whose only consumer is one-pass iteration.
Glyphs: Rust
.collect::<Vec<_>>() followed by .iter(); Go
full slice from iterator then range; Python list(...) then
for x in ...; JS Array.from(...) then .forEach.
How to look: read the body of every loop. Ask "is this O(work-per-item)
or am I doing O(work-per-file * items)?".
2. Correctness — error handling
Where do errors silently disappear?
Smells:
- Fallible call whose return value is discarded. Often
intentional (best-effort cleanup) but often a hidden bug. If
intentional, a comment explains why. If no comment, suspect.
Glyphs: Rust
let _ = f() / f().ok(); / f().unwrap_or(...) with
no log; Go bare f() ignoring err return / _ = f(); Python
try: f(); except: pass / try: f(); except Exception: pass;
JS unawaited Promise (f(); where f returns Promise) /
.catch(() => {}); C++ (void)f(); / discarded int return;
C bare unlink(p); ignoring -1.
- Error converted to absent-value type, original error dropped.
The error type carried useful information; the caller has lost it.
Glyphs: Rust
r.ok() / r.err() then dropping the other side;
Go if err != nil { return nil } / return val, nil swallowing
err; Python try: ...; except: return None; JS
try { ... } catch { return undefined }.
- First-error-wins aggregation where the caller needs to see
all failures. Common in parallel / batch contexts — collecting
results and returning only the first
Err hides every other
failure.
Glyphs: Rust .collect::<Result<Vec<_>, _>>() /
results.into_iter().find(|r| r.is_err()) / Rayon .try_reduce;
Go errgroup.Wait() returning first non-nil; Python
next(e for e in errs if e); JS Promise.all short-circuit
vs Promise.allSettled.
- Cleanup / rollback / recover function whose own failures are not
surfaced. A rollback that itself calls a fallible operation and
returns
() is hiding partial-state bugs.
Glyphs: any rollback / cleanup / recover / compensate /
unwind function whose signature returns () / void / None
but whose body invokes fallible IO (rename, unlink, flush).
- Early-return inside a loop where one bad element should not
abort the batch (or vice versa — should fail fast but does not).
Glyphs: Rust
? inside for; Go if err != nil { return err }
inside range; Python bare raise inside for; JS throw
inside forEach.
match / switch arms that absorb specific error variants
without comment — silently classify an error as success.
Glyphs: Rust Err(_) => return Ok(...); Go
case errors.Is(err, X): return nil; Python except SpecificError: pass; JS catch (e) { if (e.code === 'X') return; }.
- fsync / flush failure ignored — only observable when the OS
later loses data.
Glyphs: Rust
let _ = file.sync_all() / let _ = file.flush();
Go _ = f.Sync() / unchecked Close(); Python os.fsync in
a try/except: pass; C bare fsync(fd); ignoring -1.
- Byte-by-byte text manipulation losing UTF-8 / encoding info.
Iterating raw bytes and pushing them into a text container as if
they were codepoints corrupts every multibyte character.
Glyphs: Rust
out.push(b as char); Go string(b) where b is
one byte of a multibyte rune; Python operating on bytes and
decoding wrong / mixing str and bytes; JS String.fromCharCode
in a UTF-8 byte loop; C wchar_t cast from unsigned char.
- Identifier / index conflation. Function expects positional
index (e.g. 0-based child index), caller passes opaque identifier
(e.g. pointer-derived node id) cast to the same numeric type. The
types align so the compiler is silent. Glyphs: any
as u32 /
int(...) / uintptr cast that joins two semantically different
numbers; APIs named …_for_index vs …_for_id consumed
interchangeably; tree-sitter field_name_for_child vs
field_name_for_named_child mismatch is the canonical example.
3. Correctness — durability / ordering
If the process dies mid-operation, what state remains?
Smells, applicable any time the code mutates shared / persisted /
external state:
- Effect ordered before its precondition is durable. Deleting
backups before fsyncing the new content's parent directory. Removing
the old row before the new row is committed. Releasing the
in-memory lock before the on-disk update is observable.
- Commit before fsync. Reporting success before the write is
durable.
- Cache invalidation before write. Readers can observe the gap.
- Lock released before the protected state is fully published.
- Compensating-action ordering wrong. Rollback does steps in
same order as forward path, leaving an inconsistent intermediate.
- Parent-directory fsync skipped. On POSIX, a fsynced file in
an un-fsynced directory can vanish on crash. Easy to forget.
How to look: trace the order of side-effects through each function
that touches shared state. Ask "if I crash here, can a reader see
the new state without the old state, or vice versa, in a way the
contract forbids?".
4. Concurrency / pool placement
Are concurrency primitives installed in the scope the caller
expects?
Smells:
- Worker pool / thread pool installed around the wrong scope.
The user-facing flag (
--threads N, --concurrency M) is honored
in one phase but ignored in another because the pool wasn't
scoped wide enough.
- Async runtime spawned per call instead of shared.
- Connection pool / channel pool created at wrong unit of work
(per-request when it should be per-process, per-process when it
should be per-tenant).
- Per-thread state read from cross-thread context (or vice
versa).
spawn / Promise.all / errgroup with no bound on
parallelism — accidentally unlimited fan-out under load.
- Single-threaded fast-path inside otherwise-parallel pipeline
— a serial bottleneck masked by aggregate timing.
How to look: find every place the project defines a pool, an
executor, a runtime, a spawn site. For each one, identify the
scope it covers and the scope the caller assumed. Mismatches
are the bug.
5. Memory shape
What is held in memory that doesn't need to be, or held twice?
Smells:
- Pre- and post- of the same data held together. Struct holds
full new text and the rendered diff of old→new. The diff already
encodes the new text relative to old — keeping both doubles
per-item footprint.
- Owned-when-borrow-suffices. A copy is taken where a view or
reference into the original would be safe given lifetimes /
ownership.
Glyphs: Rust
Vec<String> vs Vec<&str> / String field where
&str suffices / .to_owned() on a value that outlives the
borrow; Go full-slice copy append([]T{}, src...) vs slice
alias; Python list(other) vs reference / copy.copy; JS
[...arr] / Array.from(arr) for read-only iteration;
C++ std::string vs std::string_view.
- Lock-when-atomic-suffices. A mutex protects a value that fits
in a machine word and could be an atomic.
Glyphs: Rust
Arc<Mutex<u64>> / Arc<Mutex<bool>>; Go
sync.Mutex around an int64 counter; Java synchronized for
a long / boolean; Python threading.Lock around an int.
- Variant-size disparity. One large variant of a discriminated
union inflates every instance — boxing the large variant fixes.
Glyphs: Rust
enum { Small(u8), Huge([u8; 4096]) }; C++ union
/ std::variant with size-imbalanced alternatives; Go interface
holding inconsistently-sized concrete types.
- Long-lived cache with no eviction policy.
- Whole-input buffer where a stream would work — tool that
could pipeline reads holds the entire input in memory.
Glyphs: Rust
fs::read_to_string / Vec::from_iter on a stream;
Go io.ReadAll; Python f.read() then process; JS
await response.text() on a 1GB response.
- Optional / nullable field that is always populated in
practice — the absent case is dead, the wrapper costs bytes
and forces every reader to handle a case that cannot occur.
Glyphs: Rust
Option<T> field whose constructor always sets
Some(...); Go *T always non-nil; Python Optional[T]
annotation but never None; TS T | undefined that's never
undefined in practice.
6. Function-level complexity
Where is one function doing too much, or too dangerously?
Visible only by reading function bodies — graph tools miss these
because they live below the file boundary.
Smells:
- Recursion with no depth bound — especially on data derived
from user input (parser, walker, AST visitor, JSON decoder).
Stack-overflow vector. The iterative-stack version is usually
one rewrite away.
- Function > ~80 lines doing more than one thing.
- Dispatch / switch / match with > ~10 arms — often a table or
registry pattern is clearer and easier to extend.
- Closure / inner function capturing many outer mutable
variables — usually a struct trying to be born.
- Function with > 5 parameters — parameter object or builder.
- Two functions whose bodies differ only by a constant or a
branch — collapse to one parameterised version.
7. Coupling — semantic, not graph
What couples that the import graph cannot see?
architectural-hotspots sees file-to-file imports. This axis
catches the rest:
- Two modules look orthogonal but both call the same set of
helpers from a third — they share an implicit protocol that
wants to be made explicit.
- A "library" module that branches on a value it gets from exactly
one caller — the abstraction has one user. Inline it or own the
branch in the caller.
- Dual orchestrators with overlapping responsibilities — one
orchestrator can usually absorb the other, or both should
delegate to a thinner core. Hotspots flags both as "tangles"
without naming the relationship.
- Type defined in module A, used only by module B — wrong home.
- "Generic" helper used only by one site — not generic, just
premature.
- Two functions doing the same operation in slightly different
ways across modules — same shape, divergent details.
8. Enforcement — is the invariant actually held?
The project states a rule. Does anything make it true?
Every other axis asks what the code does. This one asks whether the
guard on the code can fire. It is the axis that catches a check
that passes for the wrong reason, and it is invisible to the other
seven because nothing is wrong with the code under the guard — the
guard is wrong about its own reach.
Classify each invariant the project claims into three states. Only
one of them is safe:
- Derived — the check computes the invariant from the thing under
test, so it moves when the code moves. Safe.
- Asserted — the check names the value or symbol literally. It
holds today and goes stale silently the moment the code is renamed,
inlined, or restated somewhere the pattern does not reach. Finding.
- Absent — the rule exists only in prose: a comment, a README, a
docstring, a convention. Nothing enforces it. Finding.
Smells:
- Assertion that cannot fail. The check's inputs and its expected
value both derive from the single artifact under test, so it is
true by construction. Tell: a docstring promising a comparison
("compares X against the Y baseline") beside an assertion that
reads only X. A sibling step in the same block that does carry a
floor is strong evidence the missing one was an oversight.
- One-sided check on a two-sided property. Asserting the bad
state disappears without asserting the good state survives. A fix
that suppresses everything passes. Anything with a mirror,
an inverse, or an on/off needs both exercised.
- Gate whose selection rule cannot reach the violation. A
source-scanning check has several independent filters — which files
it reads, at what granularity, which keywords it matches, what name
shapes it accepts. A violation outside any one filter is invisible
while the gate stays green. Audit the rule by re-running it with
one filter widened at a time.
- Literal where the gate matches a symbol. A gate keyed on
const NAME cannot see the same value written as a bare number,
and a gate matching named constants cannot see a magic literal —
including the same value restated in a second language.
- Invariant stated only in prose. "Callers must…", "never call
this before…", "keep these two lists in sync" next to shared
mutable state or a required order. See
code-smells' Invariant by
Convention entry — that skill names the design smell, this axis
reports the specific unenforced site.
- Documentation citing something that no longer exists. A doc
naming a test, gate, or symbol that was deleted. Especially where a
gate exists to catch exactly this and its filters miss the file,
the verb, or the qualified name shape.
How to look: list every rule the project asserts about itself — gate
scripts, CI steps, test names, "must"/"never" comments, README
guarantees. For each, find the code that makes it true. Read the
gate's implementation, never its name — the name describes the
intent, and the gap between intent and selection rule is the finding.
Note this axis judges the project's own checks, which includes any
you or a predecessor added. A check you wrote and never watched fail
belongs here.
Output format
Use this exact template. Counts in the headings let the user scan
volume at a glance. Omit any section with zero findings — do not
pad.
**Perf (N)**
- `file:line` — symptom in ≤8 words. Fix: <one short phrase>.
**Correctness (N)**
- `file:line` — symptom. Fix: <one short phrase>.
**Durability/ordering (N)**
- `file:line` — symptom. Fix: <one short phrase>.
**Concurrency (N)**
- `file:line` — symptom. Fix: <one short phrase>.
**Memory (N)**
- `file:line` — symptom. Fix: <one short phrase>.
**Complexity (N)**
- `file:line` — symptom. Fix: <one short phrase>.
**Coupling (N)**
- `file_a:line + file_b:line` — symptom. Fix: <one short phrase>.
**Enforcement (N)**
- `file:line` — invariant, and which of derived/asserted/absent it is.
Fix: <one short phrase>.
**Top targets**
3-5 highest-leverage fixes, named with file + symptom.
Mark any finding you could not falsify by running something with a
trailing [traced]. Say once, at the end, how many findings were
proved and how many traced. A reader who knows which half is which
can act on both; a reader who cannot tell has to re-check all of them.
Examples
Good:
Perf (2)
commit.rs:426 — clock sampled per nonce in hot path. Fix:
sample once at construction, increment a counter.
commit.rs:328 — FS-probe syscall per backup entry. Fix:
single directory listing, then filter in memory.
Durability/ordering (1)
commit.rs:301 — backup deleted before parent-dir fsync.
Fix: fsync parents first, then unlink backups.
Concurrency (1)
main.rs:343 — --threads flag scoped only around planner;
apply phase runs on global pool. Fix: install scoped pool
around the whole pipeline.
Bad (vague, no line, no fix):
Consider refactoring commit.rs for performance. Some
operations may be inefficient.
Bad (over-prosaic, paragraph form):
Looking at commit.rs, around the nonce generator, I noticed
that it samples the wall clock, which involves a syscall. This
could be slow if called many times, although it depends on the
use case…
The format constraint matters because the user reads dozens of
findings; signal density wins.
Calibration
Finding counts depend far more on the codebase than on the line
count, so treat any number here as a prompt to re-examine, never as
a quota.
Do not scale a per-file range up across files. A rough anchor for
one unfamiliar ~500-line file with no local discipline is a handful
to a dozen findings. Four files is not four times that. On a mature
codebase — dense justifying comments, its own gates, a written record
of rejected findings — single digits across several files is the
honest result, and the padding pressure is the real risk. Auditing
2400 lines and reporting eight findings with a stated coverage claim
is a better deliverable than thirty with twenty-two of them reaching.
Two outcomes worth stating plainly rather than hiding:
- A file yielding one finding, or none. Say so. "I read all of
ops.rs and found one thing" is information. Quietly padding to
three destroys the signal in the other two.
- A low total because the project already rejected these. Name
the record you read (step 0). That is coverage, not absence.
More than ~25 findings on one file almost always means the bar
dropped. Cut to the ones that survive the table below.
Every finding must survive the "would the user act on this?"
test:
| Finding |
Acts on it? |
commit.rs:426 — clock per nonce, hoist |
yes |
commit.rs:301 — backups deleted before fsync, swap order |
yes |
commit.rs is long, consider splitting |
no — that's hotspots |
function could be more idiomatic |
no — that's clippy |
naming could be clearer |
no — not what this skill is for |
When in doubt, drop it.
Anti-patterns
- Skim-reading. Defaulting to function signatures and headers,
emitting findings about "what the function probably does".
Always read bodies before claiming a finding. The most
bug-prone functions are the ones whose names sound mundane —
read them first, not last.
- Reporting in the wrong language. Writing a Rust-flavoured
fix sketch for a Python project. Match the project's actual
stack.
- Findings the codebase already addresses. A safety
annotation, a documented "ignore error here because X", a
comment explaining why a clone is required — not findings.
Read the comments.
- …but a comment explaining a mechanism is a hypothesis, not a
reason to drop. "Safe because the toolkit never reuses the
widget", "these paths would agree only by accident", "this cannot
overflow because the caller validates" — each is a factual claim
about behaviour, and the axes apply to it exactly as they apply to
code. Dropping a real finding because a comment asserted it was
fine is the same error as reporting a false one, and it is harder
to catch because it leaves no trace in the output. Check the
explanation, then drop it or keep it. (Measured example: a comment
justified a re-cut on the grounds that widget reuse was
coincidental; a probe under the project's own headless display
showed 196 of 197 rebinds returned the item to the same widget, so
the stated reason was false and the finding was real.)
- Hand-waving "consider X" verbs. Replace with concrete
imperatives: hoist, inline, collapse, extract, box,
bound the recursion, aggregate all errors, swap order,
scope the pool.
- Trying to be exhaustive. A focused list of 10 real findings
beats a sprawling list of 25 mostly-noise. The user picks 3-5
to act on either way.
- Confusing axes. Durability/ordering bugs are not perf
bugs. Concurrency-scope mistakes are not correctness in the
error-handling sense. Pick the axis that points to the right
fix shape.
1---2name: code-audit-deep3description: Line-level code audit skill. Surfaces concrete, actionable findings — perf hotspots, error-handling correctness bugs, durability / ordering bugs, memory-shape problems, function-level complexity, semantic coupling, concurrency-primitive scope mistakes, and unenforced or unfalsifiable invariants (gates that cannot reach the violation, assertions that cannot fail, rules that live only in a comment) — that file-level architectural analysis cannot see. Language-agnostic. **This skill owns the word "hotspots" when the user wants line-level findings inside files** — phrasings like "what are the hotspots", "where are the hotspots in X", "find hotspots in this file", "show me the hotspots", "any hotspots in commit.rs?" all trigger this skill; prefer this over `architectural-hotspots` whenever the user is pointing at code and asking what's wrong with it, rather than asking which files in the repo are structurally suspect. Also trigger on "audit this", "review this code", "audit X", "review X", "find bugs in X", "what'4---56# Code Audit (Deep)78Line-level audit. Companion to `architectural-hotspots`. Hotspots ranks9files by structural shape (fan-in, fan-out, LOC, cycles); this skill10reads files and emits specific, actionable findings with line numbers11and fix sketches.1213The core failure mode this skill exists to prevent: producing vague14"consider refactoring X" advice instead of `commit.rs:426 — clock15sampled per element in hot loop; hoist or seed-and-increment`. The16first is what a graph tool already said; the second is what the user17actually wanted.1819## Language scope2021This skill is **language-agnostic**. The smells below are concepts that22recur across stacks; the parenthetical examples are illustrative for a23few common languages but never exhaustive. When you read a file, map24each smell to the *equivalent* construct in the target language:25- "fallible call return value discarded" covers `let _ = f()` (Rust),26 bare `f()` ignoring its `error` return (Go), `try: f(); except:27 pass` (Python), unawaited `f()` returning a Promise (TS / JS), `_,28 _ = f()` patterns (Lua / Go), `f(); // ignore` everywhere.29- "owned-when-borrow-suffices" covers `Vec<String>` vs `Vec<&str>`30 (Rust), `[]string` copies vs slice aliases (Go), deep-copying lists31 (Python), `.slice()` cloning vs index access (JS), `std::string` vs32 `std::string_view` (C++).33- "lock-when-atomic-suffices" covers `Arc<Mutex<u64>>` (Rust),34 `sync.Mutex` for a counter (Go), `threading.Lock` around an int35 (Python), `Object.synchronized` for a primitive (Java).3637Never report a finding using a language construct the project does not38use. Substitute the project's equivalent before writing the fix sketch.3940## When to run4142Use whenever the user wants *findings inside code* rather than *rankings43across files*. Triggers include:4445- "audit `commit.rs`" / "review `plan.rs`"46- "find perf issues in this module"47- "what could go wrong in `apply_changes`?"48- "deep review of the parser"49- "where is this slow?"50- After running `architectural-hotspots` and the user wants the next51 level of detail on the flagged files.5253Do **not** use when the user wants:54- Architectural overview / rankings → that's `architectural-hotspots`.55- Style / formatting issues → clippy / eslint / ruff / staticcheck etc.56- Security review of specific CVE classes → a dedicated security skill57 should run. Overlap is fine; this skill won't hunt for known CVEs.58- A single trivial bug fix in a known file — just fix it.5960## Method6162The strength of this skill comes from depth, not breadth. Better to63audit three files thoroughly than ten superficially.64650. **Find the project's record of already-rejected findings** before66 reading any code: `DECISIONS.md`, ADRs, a "rejection ledger" or67 "verified negatives" section, `*findings*.md`, dismissed-issue68 labels. Ask the user if nothing turns up and the repo looks mature.69 Re-raising something the team already considered and rejected on70 the record costs more credibility than the finding was ever worth,71 and it is the fastest way to get an audit ignored wholesale. When72 you drop a candidate for this reason, name the entry — that tells73 the user their record is being read, not that you found nothing.74751. **Pick targets.**76 - If the user named files, those are the targets.77 - If not, and a recent hotspots report exists, take the top 3-578 files by combined signal (god + hub, god + tangle, or any file79 inside a cycle).80 - If neither, ask the user *which files* — not "should we audit?"81 but "which files do you want audited?" so the answer drives82 work.83842. **Read every function body end to end.** Headers and type85 signatures are not enough. A finding requires a line number, and a86 line number requires having read the line. Do not skip a function87 because its name sounds boring or routine — `apply_changes`,88 `commit_one`, `cleanup`, `recover`, `flush` often hold the89 subtle ordering bugs. Skim-reading is the dominant failure mode of90 this skill — defend against it actively by asking, for each91 function, "if this had a bug, where would it be?".9293 **Reading *until the finding appears* is a separate failure from94 reading too little, and it feels like diligence.** You read the95 body, you found the line that confirms the bug, and you stopped —96 one statement before the guard that made it safe. Reading97 `Motion::Last => adj.upper()` and reporting an unbounded scroll is98 correct up to the very next statement,99 `adj.set_value(to.clamp(lower, ceiling))`, which disproves it. The100 fix is a habit, not more effort: after a candidate appears, keep101 reading to the end of the enclosing block and ask "what here would102 make this not a bug?" — then answer it before writing the finding.1031043. **First pass: glyph sweep.** Before going deep on novel findings,105 do a fast scan at the *language-glyph level* — every smell below106 lists concrete syntactic glyphs in several languages. These glyphs107 are where the obvious bugs hide; missing them to chase one108 interesting novel finding is a regression of attention. After the109 glyph sweep produces its candidate list, *then* go deep on110 higher-order concerns (durability ordering, coupling, axis-111 specific structural issues).1121134. **Audit along the eight axes** (next section). Most files only hit114 three or four. Don't reach for findings in an axis that genuinely115 doesn't apply.1161175. **Emit findings in the fixed format** (later section). Categorized.118 `file:line`. One-line symptom. One-line fix sketch. No paragraphs119 of prose around each finding — the user is reading for signal.1201216. **Prove what you can; label the rest.** For each finding, name the122 command that would *falsify* it, and run it. If no such command123 exists, mark the finding **traced** rather than **proved**. Both124 are publishable — a traced finding backed by an honest label is125 useful, a traced finding presented as measured is a liability.126127 The trap this closes is specific and common: running a real check128 *near* the claim and letting it stand in for the claim. Reading129 `Motion::Last => adj.upper()`, measuring how the toolkit clamps130 adjustments, and reporting the finding verified — while the very131 next statement was `adj.set_value(to.clamp(lower, ceiling))` — is132 a measurement of the wrong thing, and the label "verified" is what133 makes it damaging. The question is never "did I run something?"134 but "would this command have come out differently if the finding135 were false?"136137 Before hand-rolling a harness, check whether the language already138 has the tool: mutation testers, fuzzers, sanitizers, and coverage139 tools mechanize a whole class of proof, and a replica you wrote140 this afternoon is a slower substitute you also have to trust.141 Verify what the tool actually does before citing it.1421437. **Pick the top 3-5 highest-leverage fixes** out of the full list144 and name them at the end. This is the deliverable the user will145 actually act on first.146147## The eight axes148149Each axis answers a different question. They are deliberately150orthogonal — a finding belongs in exactly one category.151152### 1. Perf153154*What is making this slow that shouldn't be?*155156Smells:157- **Syscalls / IO inside a per-element loop.** File-system probes158 (exists / stat / metadata / readdir), opens, reads, network159 round-trips, DB queries — fine once, expensive per element.160 Glyphs: Rust `std::fs::{metadata,exists,read}` / `Path::exists`;161 Go `os.Stat` / `os.Open`; Python `os.path.exists` / `open()`;162 JS `fs.existsSync` / `fs.statSync`; C `stat()` / `open()` /163 `access()`.164- **Clock / RNG / time calls per element.** Wall-clock or165 monotonic-clock samples, RNG draws, UUID generations done per166 iteration when one sample-and-increment would do.167 Glyphs: Rust `SystemTime::now()` / `Instant::now()` / `rand::*`;168 Go `time.Now()` / `rand.Int*`; Python `time.time()` /169 `datetime.now()` / `random.*` / `uuid.uuid4()`; JS `Date.now()` /170 `performance.now()` / `Math.random()`; C `clock_gettime` /171 `gettimeofday` / `rand()`.172- **Allocation in hot loop.** Building a fresh container, formatting173 a fresh string, copying a non-trivial value each iteration when a174 reused buffer or borrowed view would do.175 Glyphs: Rust `String::new()` / `Vec::new()` / `.to_owned()` /176 `.clone()` / `format!(...)`; Go `[]T{}` literal / `make(...)` /177 `fmt.Sprintf` / `string([]byte)`; Python list/dict comprehension /178 `f"..."` / `copy.copy` / `copy.deepcopy`; JS array literal `[…]` /179 template literal in inner loop / `Object.assign({}, …)` / spread;180 C++ `std::string`/`std::vector` constructor in loop body.181- **Full re-scans where incremental would do.** Re-running a regex182 / parse / hash over the *entire* post-image to detect changes that183 could be tracked at write-time. Re-walking a tree that was just184 walked.185- **Redundant parse / compile.** Same input parsed twice. Same186 pattern compiled in two code paths. Same query / plan built per187 call instead of cached.188 Glyphs: regex `Pattern::compile` / `re.compile` / `new RegExp`189 inside a function called per request; SQL `prepare` per request;190 tree-sitter `Query::new` / parser `set_language` per call.191- **Forced materialization.** Collecting a stream into a fully192 materialised container whose only consumer is one-pass iteration.193 Glyphs: Rust `.collect::<Vec<_>>()` followed by `.iter()`; Go194 full slice from iterator then range; Python `list(...)` then195 `for x in ...`; JS `Array.from(...)` then `.forEach`.196197How to look: read the body of every loop. Ask "is this O(work-per-item)198or am I doing O(work-per-file * items)?".199200### 2. Correctness — error handling201202*Where do errors silently disappear?*203204Smells:205- **Fallible call whose return value is discarded.** Often206 intentional (best-effort cleanup) but often a hidden bug. If207 intentional, a comment explains why. If no comment, suspect.208 Glyphs: Rust `let _ = f()` / `f().ok();` / `f().unwrap_or(...)` with209 no log; Go bare `f()` ignoring `err` return / `_ = f()`; Python210 `try: f(); except: pass` / `try: f(); except Exception: pass`;211 JS unawaited Promise (`f();` where `f` returns `Promise`) /212 `.catch(() => {})`; C++ `(void)f();` / discarded `int` return;213 C bare `unlink(p);` ignoring `-1`.214- **Error converted to absent-value type, original error dropped.**215 The error type carried useful information; the caller has lost it.216 Glyphs: Rust `r.ok()` / `r.err()` then dropping the other side;217 Go `if err != nil { return nil }` / `return val, nil` swallowing218 err; Python `try: ...; except: return None`; JS219 `try { ... } catch { return undefined }`.220- **First-error-wins aggregation** where the caller needs to see221 *all* failures. Common in parallel / batch contexts — collecting222 results and returning only the first `Err` hides every other223 failure.224 Glyphs: Rust `.collect::<Result<Vec<_>, _>>()` /225 `results.into_iter().find(|r| r.is_err())` / Rayon `.try_reduce`;226 Go `errgroup.Wait()` returning first non-nil; Python227 `next(e for e in errs if e)`; JS `Promise.all` short-circuit228 vs `Promise.allSettled`.229- **Cleanup / rollback / recover function whose own failures are not230 surfaced.** A rollback that itself calls a fallible operation and231 returns `()` is hiding partial-state bugs.232 Glyphs: any `rollback` / `cleanup` / `recover` / `compensate` /233 `unwind` function whose signature returns `()` / `void` / `None`234 but whose body invokes fallible IO (`rename`, `unlink`, `flush`).235- **Early-return inside a loop where one bad element should not236 abort the batch (or vice versa — should fail fast but does not).**237 Glyphs: Rust `?` inside `for`; Go `if err != nil { return err }`238 inside `range`; Python bare `raise` inside `for`; JS `throw`239 inside `forEach`.240- **`match` / `switch` arms that absorb specific error variants241 without comment** — silently classify an error as success.242 Glyphs: Rust `Err(_) => return Ok(...)`; Go243 `case errors.Is(err, X): return nil`; Python `except SpecificError:244 pass`; JS `catch (e) { if (e.code === 'X') return; }`.245- **fsync / flush failure ignored** — only observable when the OS246 later loses data.247 Glyphs: Rust `let _ = file.sync_all()` / `let _ = file.flush()`;248 Go `_ = f.Sync()` / unchecked `Close()`; Python `os.fsync` in249 a `try/except: pass`; C bare `fsync(fd);` ignoring `-1`.250- **Byte-by-byte text manipulation losing UTF-8 / encoding info.**251 Iterating raw bytes and pushing them into a text container as if252 they were codepoints corrupts every multibyte character.253 Glyphs: Rust `out.push(b as char)`; Go `string(b)` where `b` is254 one byte of a multibyte rune; Python operating on bytes and255 decoding wrong / mixing `str` and `bytes`; JS `String.fromCharCode`256 in a UTF-8 byte loop; C `wchar_t` cast from `unsigned char`.257- **Identifier / index conflation.** Function expects positional258 index (e.g. 0-based child index), caller passes opaque identifier259 (e.g. pointer-derived node id) cast to the same numeric type. The260 types align so the compiler is silent. Glyphs: any `as u32` /261 `int(...)` / `uintptr` cast that joins two semantically different262 numbers; APIs named `…_for_index` vs `…_for_id` consumed263 interchangeably; tree-sitter `field_name_for_child` vs264 `field_name_for_named_child` mismatch is the canonical example.265266### 3. Correctness — durability / ordering267268*If the process dies mid-operation, what state remains?*269270Smells, applicable any time the code mutates shared / persisted /271external state:272- **Effect ordered before its precondition is durable.** Deleting273 backups before fsyncing the new content's parent directory. Removing274 the old row before the new row is committed. Releasing the275 in-memory lock before the on-disk update is observable.276- **Commit before fsync.** Reporting success before the write is277 durable.278- **Cache invalidation before write.** Readers can observe the gap.279- **Lock released before the protected state is fully published.**280- **Compensating-action ordering wrong.** Rollback does steps in281 same order as forward path, leaving an inconsistent intermediate.282- **Parent-directory fsync skipped.** On POSIX, a fsynced file in283 an un-fsynced directory can vanish on crash. Easy to forget.284285How to look: trace the *order of side-effects* through each function286that touches shared state. Ask "if I crash here, can a reader see287the new state without the old state, or vice versa, in a way the288contract forbids?".289290### 4. Concurrency / pool placement291292*Are concurrency primitives installed in the scope the caller293expects?*294295Smells:296- **Worker pool / thread pool installed around the wrong scope.**297 The user-facing flag (`--threads N`, `--concurrency M`) is honored298 in one phase but ignored in another because the pool wasn't299 scoped wide enough.300- **Async runtime spawned per call** instead of shared.301- **Connection pool / channel pool created at wrong unit of work**302 (per-request when it should be per-process, per-process when it303 should be per-tenant).304- **Per-thread state read from cross-thread context** (or vice305 versa).306- **`spawn` / `Promise.all` / `errgroup` with no bound on307 parallelism** — accidentally unlimited fan-out under load.308- **Single-threaded fast-path inside otherwise-parallel pipeline**309 — a serial bottleneck masked by aggregate timing.310311How to look: find every place the project defines a pool, an312executor, a runtime, a spawn site. For each one, identify the313*scope* it covers and the *scope* the caller assumed. Mismatches314are the bug.315316### 5. Memory shape317318*What is held in memory that doesn't need to be, or held twice?*319320Smells:321- **Pre- and post- of the same data held together.** Struct holds322 full new text *and* the rendered diff of old→new. The diff already323 encodes the new text relative to old — keeping both doubles324 per-item footprint.325- **Owned-when-borrow-suffices.** A copy is taken where a view or326 reference into the original would be safe given lifetimes /327 ownership.328 Glyphs: Rust `Vec<String>` vs `Vec<&str>` / `String` field where329 `&str` suffices / `.to_owned()` on a value that outlives the330 borrow; Go full-slice copy `append([]T{}, src...)` vs slice331 alias; Python `list(other)` vs reference / `copy.copy`; JS332 `[...arr]` / `Array.from(arr)` for read-only iteration;333 C++ `std::string` vs `std::string_view`.334- **Lock-when-atomic-suffices.** A mutex protects a value that fits335 in a machine word and could be an atomic.336 Glyphs: Rust `Arc<Mutex<u64>>` / `Arc<Mutex<bool>>`; Go337 `sync.Mutex` around an `int64` counter; Java `synchronized` for338 a `long` / `boolean`; Python `threading.Lock` around an `int`.339- **Variant-size disparity.** One large variant of a discriminated340 union inflates every instance — boxing the large variant fixes.341 Glyphs: Rust `enum { Small(u8), Huge([u8; 4096]) }`; C++ `union`342 / `std::variant` with size-imbalanced alternatives; Go interface343 holding inconsistently-sized concrete types.344- **Long-lived cache with no eviction policy.**345- **Whole-input buffer where a stream would work** — tool that346 could pipeline reads holds the entire input in memory.347 Glyphs: Rust `fs::read_to_string` / `Vec::from_iter` on a stream;348 Go `io.ReadAll`; Python `f.read()` then process; JS349 `await response.text()` on a 1GB response.350- **Optional / nullable field that is always populated in351 practice** — the absent case is dead, the wrapper costs bytes352 and forces every reader to handle a case that cannot occur.353 Glyphs: Rust `Option<T>` field whose constructor always sets354 `Some(...)`; Go `*T` always non-nil; Python `Optional[T]`355 annotation but never `None`; TS `T | undefined` that's never356 undefined in practice.357358### 6. Function-level complexity359360*Where is one function doing too much, or too dangerously?*361362Visible only by reading function bodies — graph tools miss these363because they live below the file boundary.364365Smells:366- **Recursion with no depth bound** — especially on data derived367 from user input (parser, walker, AST visitor, JSON decoder).368 Stack-overflow vector. The iterative-stack version is usually369 one rewrite away.370- **Function > ~80 lines doing more than one thing.**371- **Dispatch / switch / match with > ~10 arms** — often a table or372 registry pattern is clearer and easier to extend.373- **Closure / inner function capturing many outer mutable374 variables** — usually a struct trying to be born.375- **Function with > 5 parameters** — parameter object or builder.376- **Two functions whose bodies differ only by a constant or a377 branch** — collapse to one parameterised version.378379### 7. Coupling — semantic, not graph380381*What couples that the import graph cannot see?*382383`architectural-hotspots` sees file-to-file imports. This axis384catches the rest:385- Two modules look orthogonal but both call the same set of386 helpers from a third — they share an implicit protocol that387 wants to be made explicit.388- A "library" module that branches on a value it gets from exactly389 one caller — the abstraction has one user. Inline it or own the390 branch in the caller.391- **Dual orchestrators with overlapping responsibilities** — one392 orchestrator can usually absorb the other, or both should393 delegate to a thinner core. Hotspots flags both as "tangles"394 without naming the relationship.395- Type defined in module A, used only by module B — wrong home.396- "Generic" helper used only by one site — not generic, just397 premature.398- Two functions doing the same operation in slightly different399 ways across modules — same shape, divergent details.400401### 8. Enforcement — is the invariant actually held?402403*The project states a rule. Does anything make it true?*404405Every other axis asks what the code does. This one asks whether the406guard on the code can fire. It is the axis that catches **a check407that passes for the wrong reason**, and it is invisible to the other408seven because nothing is wrong with the code under the guard — the409guard is wrong about its own reach.410411Classify each invariant the project claims into three states. Only412one of them is safe:413414- **Derived** — the check computes the invariant from the thing under415 test, so it moves when the code moves. Safe.416- **Asserted** — the check names the value or symbol literally. It417 holds today and goes stale silently the moment the code is renamed,418 inlined, or restated somewhere the pattern does not reach. Finding.419- **Absent** — the rule exists only in prose: a comment, a README, a420 docstring, a convention. Nothing enforces it. Finding.421422Smells:423- **Assertion that cannot fail.** The check's inputs and its expected424 value both derive from the single artifact under test, so it is425 true by construction. Tell: a docstring promising a comparison426 ("compares X against the Y baseline") beside an assertion that427 reads only X. A sibling step in the same block that *does* carry a428 floor is strong evidence the missing one was an oversight.429- **One-sided check on a two-sided property.** Asserting the bad430 state disappears without asserting the good state survives. A fix431 that suppresses *everything* passes. Anything with a mirror,432 an inverse, or an on/off needs both exercised.433- **Gate whose selection rule cannot reach the violation.** A434 source-scanning check has several independent filters — which files435 it reads, at what granularity, which keywords it matches, what name436 shapes it accepts. A violation outside any one filter is invisible437 while the gate stays green. Audit the rule by re-running it with438 one filter widened at a time.439- **Literal where the gate matches a symbol.** A gate keyed on440 `const NAME` cannot see the same value written as a bare number,441 and a gate matching named constants cannot see a magic literal —442 including the same value restated in a second language.443- **Invariant stated only in prose.** "Callers must…", "never call444 this before…", "keep these two lists in sync" next to shared445 mutable state or a required order. See `code-smells`' *Invariant by446 Convention* entry — that skill names the design smell, this axis447 reports the specific unenforced site.448- **Documentation citing something that no longer exists.** A doc449 naming a test, gate, or symbol that was deleted. Especially where a450 gate exists to catch exactly this and its filters miss the file,451 the verb, or the qualified name shape.452453How to look: list every rule the project asserts about itself — gate454scripts, CI steps, test names, "must"/"never" comments, README455guarantees. For each, find the code that makes it true. **Read the456gate's implementation, never its name** — the name describes the457intent, and the gap between intent and selection rule is the finding.458459Note this axis judges the *project's own* checks, which includes any460you or a predecessor added. A check you wrote and never watched fail461belongs here.462463## Output format464465Use this exact template. Counts in the headings let the user scan466volume at a glance. Omit any section with zero findings — do not467pad.468469```470**Perf (N)**471- `file:line` — symptom in ≤8 words. Fix: <one short phrase>.472473**Correctness (N)**474- `file:line` — symptom. Fix: <one short phrase>.475476**Durability/ordering (N)**477- `file:line` — symptom. Fix: <one short phrase>.478479**Concurrency (N)**480- `file:line` — symptom. Fix: <one short phrase>.481482**Memory (N)**483- `file:line` — symptom. Fix: <one short phrase>.484485**Complexity (N)**486- `file:line` — symptom. Fix: <one short phrase>.487488**Coupling (N)**489- `file_a:line + file_b:line` — symptom. Fix: <one short phrase>.490491**Enforcement (N)**492- `file:line` — invariant, and which of derived/asserted/absent it is.493 Fix: <one short phrase>.494495**Top targets**4963-5 highest-leverage fixes, named with file + symptom.497```498499Mark any finding you could not falsify by running something with a500trailing `[traced]`. Say once, at the end, how many findings were501proved and how many traced. A reader who knows which half is which502can act on both; a reader who cannot tell has to re-check all of them.503504### Examples505506Good:507508> **Perf (2)**509> - `commit.rs:426` — clock sampled per nonce in hot path. Fix:510> sample once at construction, increment a counter.511> - `commit.rs:328` — FS-probe syscall per backup entry. Fix:512> single directory listing, then filter in memory.513>514> **Durability/ordering (1)**515> - `commit.rs:301` — backup deleted before parent-dir fsync.516> Fix: fsync parents first, then unlink backups.517>518> **Concurrency (1)**519> - `main.rs:343` — `--threads` flag scoped only around planner;520> apply phase runs on global pool. Fix: install scoped pool521> around the whole pipeline.522523Bad (vague, no line, no fix):524525> Consider refactoring `commit.rs` for performance. Some526> operations may be inefficient.527528Bad (over-prosaic, paragraph form):529530> Looking at `commit.rs`, around the nonce generator, I noticed531> that it samples the wall clock, which involves a syscall. This532> could be slow if called many times, although it depends on the533> use case…534535The format constraint matters because the user reads dozens of536findings; signal density wins.537538## Calibration539540Finding counts depend far more on the codebase than on the line541count, so treat any number here as a prompt to re-examine, never as542a quota.543544**Do not scale a per-file range up across files.** A rough anchor for545one unfamiliar ~500-line file with no local discipline is a handful546to a dozen findings. Four files is not four times that. On a mature547codebase — dense justifying comments, its own gates, a written record548of rejected findings — single digits *across several files* is the549honest result, and the padding pressure is the real risk. Auditing5502400 lines and reporting eight findings with a stated coverage claim551is a better deliverable than thirty with twenty-two of them reaching.552553Two outcomes worth stating plainly rather than hiding:554- **A file yielding one finding, or none.** Say so. "I read all of555 `ops.rs` and found one thing" is information. Quietly padding to556 three destroys the signal in the other two.557- **A low total because the project already rejected these.** Name558 the record you read (step 0). That is coverage, not absence.559560More than ~25 findings on one file almost always means the bar561dropped. Cut to the ones that survive the table below.562563Every finding must survive the "would the user act on this?"564test:565566| Finding | Acts on it? |567|---|---|568| `commit.rs:426 — clock per nonce, hoist` | yes |569| `commit.rs:301 — backups deleted before fsync, swap order` | yes |570| `commit.rs is long, consider splitting` | no — that's hotspots |571| `function could be more idiomatic` | no — that's clippy |572| `naming could be clearer` | no — not what this skill is for |573574When in doubt, drop it.575576## Anti-patterns577578- **Skim-reading.** Defaulting to function signatures and headers,579 emitting findings about "what the function probably does".580 Always read bodies before claiming a finding. The most581 bug-prone functions are the ones whose names sound mundane —582 read them first, not last.583- **Reporting in the wrong language.** Writing a Rust-flavoured584 fix sketch for a Python project. Match the project's actual585 stack.586- **Findings the codebase already addresses.** A safety587 annotation, a documented "ignore error here because X", a588 comment explaining why a clone is required — not findings.589 Read the comments.590- **…but a comment explaining a *mechanism* is a hypothesis, not a591 reason to drop.** "Safe because the toolkit never reuses the592 widget", "these paths would agree only by accident", "this cannot593 overflow because the caller validates" — each is a factual claim594 about behaviour, and the axes apply to it exactly as they apply to595 code. Dropping a real finding because a comment asserted it was596 fine is the same error as reporting a false one, and it is harder597 to catch because it leaves no trace in the output. Check the598 explanation, then drop it or keep it. (Measured example: a comment599 justified a re-cut on the grounds that widget reuse was600 coincidental; a probe under the project's own headless display601 showed 196 of 197 rebinds returned the item to the same widget, so602 the stated reason was false and the finding was real.)603- **Hand-waving "consider X" verbs.** Replace with concrete604 imperatives: *hoist*, *inline*, *collapse*, *extract*, *box*,605 *bound the recursion*, *aggregate all errors*, *swap order*,606 *scope the pool*.607- **Trying to be exhaustive.** A focused list of 10 real findings608 beats a sprawling list of 25 mostly-noise. The user picks 3-5609 to act on either way.610- **Confusing axes.** Durability/ordering bugs are not perf611 bugs. Concurrency-scope mistakes are not correctness in the612 error-handling sense. Pick the axis that points to the right613 fix shape.