- name
- ripwire-fresh-eyes
- description
- Maintenance risk in code you did NOT write — inheriting a module, planning a refactor, a suspected god object, 'what's gnarly / where's the rot / safe to touch?': hotspots, dead or inactive code, switches/defaults (--flags), clones, a function's real shape, bus factor, co-change. Naming the fix or judging YOUR new code → quality-bar. A single-lens question is a single call.
- allowed-tools
- Bash, Read
# Fresh-eyes risk review with ripwire
> Routing:
> • Any DIFF — your own, or an incoming PR you're reviewing — about to push or merge → **ripwire-change-check**.
> • **You have the measurement and now need the FIX** — which refactor a measured shape calls for, its
> precondition, and the fix-then-prove loop → **ripwire-quality-bar**'s shape → refactor playbook.
> • Architecture health (deps metrics, layering rules, modules) → **ripwire-layers**.
> • Finding where a specific BUG lives → **ripwire-find-bug**.
> • Not sure which skill? → **ripwire-router**.
>
> **Scope it to one unfamiliar subsystem** you're about to touch, instead of the whole tree, two ways:
> point `<dir>` at the subdirectory (`ripwire src/net --hotspots`), or keep the repo root and `--exclude` the
> rest. `--dead-code=DIR` also scopes directly. This is how you turn the repo-wide sweep into a
> "what's-gnarly-in-code-I-didn't-write" read of one module.
`<dir>` = repo root. Add `--exclude=build --exclude=.git` (repeatable) to keep build artifacts out — they
otherwise show up as false clones and dead code. This composes descriptive passes into one snapshot; run the
sections your question needs.
**Inheriting this subsystem?** Check `ripwire <dir> --notes --legend=compact` first — a prior agent's pinned gotcha
(`--note-add`'d trap, flake, or invariant) is the cheapest fact you can pick up before you start reading.
## Core health passes
1. **Maintenance hotspots** — `ripwire <dir> --hotspots --legend=compact`
```
<hotspots window="12mo" ranked="33"><f p="./src/main.cpp" churn="53" ccx="1365" score="72345" top="main:1306"/> …
```
`score = churn × ccx` (commits × Σ cognitive complexity). Top files = where developers keep working *and*
the code is hard — the highest-leverage places to improve. `top=` names the worst function.
**Recent regression, not an all-time read?** `--hotspots --since="2 weeks ago"` (or `--since=HEAD~20`)
scopes churn to commits after that point; unresolvable/absent `--since` degrades silently to all-history.
**On C-family/C# code, discount a body carrying `ppalt=`:** its `cx=`/`ccx=`/`nest=`/`loc=`/`locals=` are
summed over EVERY `#else`/`#ifdef` branch (deterministic, but an over-count vs any single build), so a
high score on a `ppalt=`-heavy function can be the preprocessor fan-out, not the logic — read `ppalt=`
before calling a row the worst in the file.
1a. **"What changed recently / who touched this / what is moving here?"** — `ripwire <dir> --rank-by=churn-decay --legend=compact`
```
<recent n="40" of="1857" merge_bombs_skipped="5"><rc p="src/graph.h" w="0.91" age="2"/> …
```
The HISTORY question, not the working-tree one: `--situ` answers what *you* changed and have not
committed, this answers what the repository has been moving, newest-and-heaviest first.
**Scope it to a directory: `ripwire <dir> --rank-by=churn-decay --in=DIR`** (root-relative, and it must
exist under the root) — a second `<recent scope="DIR" …>` block follows the global
one and the symbol map collapses to a counted stub, so the answer is the window rather than a whole-repo
map. `merge_bombs_skipped="N"` is the disclosure that N commits touching more than 100 indexed files
were left out of the weighting — if the commit you are hunting is a giant rename, it is in that N and
not in the rows. `--since=REV|DATE` bounds the window; `--limit/--offset` page the scoped block.
`ripwire <dir> --help-task="what changed recently in DIR" --legend=compact` routes here on its own.
1b. **What is BUILT but DARK here?** — `ripwire <dir> --flags --legend=compact` (`=SUBSTR` to narrow)
```
<flags gates="98" dark_gates="90" compile="32" cmake="9" env="57"><gate name="X_HARMONY_SFX" kind="compile"
default="0" dark="1" regions="7" loc="45" reads="8" p="sound/audioWiringFlags.h" l="47"/> …
```
Inheriting a repo, the invisible half is the code that ships compiled OUT. This harvests all three gate
patterns — `#ifndef`/`#define` header gates, CMake `option()`, `getenv()` reads — with each gate's
**default** and the size of the code it guards, dark first. Two things `--grep` cannot tell you: when a
name is BOTH a header gate defaulting to `0` and a CMake `option(... ON)`, the **CMake default wins**
(it is what the build passes) and the header appears as an `<also>` row — the contradiction a reader who
greps the header gets wrong; and a gate whose default IS another gate's name resolves through the alias
chain, so a master switch reports `<aliases n=…>` instead of a misleading `loc="0"`.
**Limits:** lexical, not preprocessed — this is the in-repo default, never the value *your* build used.
A gate needs a value (`#ifndef F` / `#define F 0`); valueless pairs are include guards and are excluded.
A gate read as a value (`constexpr bool k = F != 0;` then `if constexpr`) honestly reports `regions="0"`
here — **`--flip` below lifts exactly that limit.**
1b-ii. **If I flip THIS one, what lights up?** — `ripwire <dir> --flags --legend=compact --flip=NAME`
```
<flip gate="X_RRF_ALL" kind="cmake" default="ON" dark="0" family="12" regions="0" loc="0" branches="43"
bindings="11" hosts="11" downstream="95" dependents="146" tests="23" untested="0">
<member name="X_RRF_ISLANDS" via="alias" branches="12"/> …
<lights r="0" b="43"><b p="canyon/simLevel.cpp" l="133" gate="X_RRF_WIDTHWAVE" via="kWidthWave"
sym="buildStripTable"/> …
```
A list of ~90 dark gates is a map, not a decision. This is the decision: for ONE gate, the code that
becomes live (`#if` regions **and** the `if constexpr` branch sites a value-style gate governs), the
**symbols** holding it, what those transitively call (`downstream` — what starts executing), who depends
on them (`dependents`), the **tests** that reach them, and `untested` — the hosts no test covers. That
last number is the honest answer to "is it safe to flip?".
Alias chains run **both ways**: flipping a master rolls up every child that `#define`s to it, flipping a
child lights only that child and names its `<parent>` plus the siblings the parent's flip would add.
`kind="cmake"` also steers the *build graph* (an `if(NAME) target_sources(...)` can add whole files) —
those sites are listed as `<c>` rows and deliberately **not** followed. `kind="env"` is `runtime="1"`:
no delimited region, so the hosts are the symbols that consult the variable.
**Limits:** lexical and single-line, never preprocessed. A binding split across two lines is missed. The
value lane reads C-family source only and treats a file declaring its **own** constant of that name as
shadowing the gate's (C++ scoping) — but a *third* header's same-named constant, included rather than
redeclared, would still count. A lit site inside no indexed def (a guarded member field, a file-scope
`constexpr`, a test-macro body) counts into `filescope=` instead of a host. `--detail=N` lifts the row
caps. Report only — always exit 0; an unknown gate name refuses (exit 1) and names the near-misses.
1c. **Can I TRUST this repo's docs?** — `ripwire <dir> --doc-drift --legend=compact` (`=SUBSTR` to narrow to one doc)
```
<doc-drift docs="129" clean="110" anchors="1995" checked="716" unchecked="1279" drift="110">
<doc p="docs/ARCHITECTURE.md" anchors="50" checked="34" drift="9"><a k="file-line" l="186" c="80"
why="line-moved" ref="src/main.cpp:317" sym="isGitUrl" got="communityPresentation"/> …
```
Inheriting a repo, the design docs and audits are how you learn it — and the stale ones are how you learn
it *wrong*. Run this **before** you read them: it verifies the CHECKABLE anchors (`file:line` refs,
backticked symbol mentions, `= N` constants, `[N]` array extents) against the live index and reports only
what no longer holds, per doc. A doc with `drift="9"` gets read with suspicion; the 110 clean ones do not.
`why=` names the cause — `missing-file`, `past-eof`, `line-moved` (with `got=` naming the symbol that now
occupies the line), `undefined`, `const-value`, `array-extent`.
**Limits:** anchors, not prose — `§Status` lines, dates and "N of M done" tallies are NOT checked, and it
says so rather than pretending. Every lane under-reports deliberately (a name is stale only if it occurs
nowhere in the code as a token; a number only if the corpus binds it uniquely in a declaration), and
`checked + unchecked = anchors` with each declined check named in an `<unchecked>` row. Exit is always 0:
drift is a report, not a gate.
**Want it to BECOME a gate?** — `ripwire <dir> --doc-drift --legend=compact --gateability`. The reason CI can't gate on
drift is usually a handful of UNDATED design docs: with no date the lexical lanes can't tell "stale" from
"a record of what was true then", so their rows stay unclassifiable. This lists exactly those docs and
what one annotation each would fix:
```
<gateability docs="17" projected_drift="0">
<fix p="docs/ARCHITECTURE.md" live="6"/> …
```
`live=` is that doc's still-failing anchors; `projected_drift=` is the repo-wide drift you'd be left with
if every listed doc got one ISO date in its H1 or a front-matter self-date line. Treat it as an **upper
bound on the win, not a mandate** — dating a doc that is genuinely live hides real rot rather than
resolving it. Use it to turn "CI stays non-gating forever" into a finite to-do list.
**Add `--with-history` when the mention lane matters.** `undefined` only means "defined nowhere here",
which on a repo whose PLAN docs name unbuilt features is *expected*, not rot — measured on a 2900-file
repo, 243 of 325 `undefined` rows were names that had never existed there. `--with-history` makes one
`git log` pass over everything reachable from HEAD and splits the lane: `why="deleted"` with `got="removed
in <commit> (<date>)"` and `at=` the file (real rot, ~75% fewer rows but every true positive kept), versus
`<unchecked r="never-in-history">` for names the repo never had. It is opt-in because the walk costs ~3 s
on that repo against a 0.64 s default; the result is memoized per (repo, HEAD sha), so every later
question on the same commit — including `--whereis --with-history` — is a cache load.
1d. **Did a task in this plan just never get launched?** — `ripwire <dir> --plan-lint=FILE --legend=compact`. Where 1c
explicitly does NOT check `§Status` lines, dates or "N of M done" tallies, this is the narrow
structural check that closes that one gap — a wave-closer's job, mechanized. It is opt-in per FILE
(never a directory sweep) because the house PLAN dialect it recognizes is deliberately narrow: a task
card is exactly an H3 heading opening with a task id (`T5`, `T10`, `T7b`), a status ledger is exactly
one heading (any level) reading "Status" once a leading section mark is stripped. A file showing
neither an H3 card nor a ledger heading reports `dialect="0"` and nothing further runs — most plans
are exactly that file, and this is not a failing lint for them.
```
<plan-lint file="wave.md" dialect="1" cards="6" ledger="1" git="1" stale_commits="20" gating="1">
<card id="T5" line="40" status="missing" why="unlaunched" gating="1"/> …
```
Once `dialect="1"`: a card's status is satisfied EITHER by a glyph on the LAST non-blank line of its
own body OR by a `§Status` ledger line naming its id — folded by DIGITS, so a bare card `T7` is also
answered by a lettered ledger mention `T7a`/`T7b` (the real house shape when a card's own body names
lettered sub-tasks and the ledger tracks each separately) — with the card's own body winning when it
has one. Unresolved is `status="missing"`, with `why=` naming the exact shape: `unlaunched` (a ledger
exists and never names this id at all — the exact field failure this verb exists to catch: a card that
was planned but never started, silent until a human happened to notice), `unresolved` (the ledger names
it but no line carries a glyph), or `no-glyph` (this document carries no ledger at all, so the card's
own body was the only possible source). An hourglass line whose git-blamed commit sits more than
`stale_commits=` commits behind HEAD is `stale="1"` (never claimed outside a git repo; blames whichever
line the status actually resolved to, marked `src="ledger"` when that is the ledger and not the card);
a task id named in the ledger with no matching card (same digit fold) is a `ledger-orphan`; an owed/OWED
mention with no check-mark or cross anywhere LATER in the SAME document is undischarged.
**Limits, stated up front:** never semantics — it does not judge whether a card's claims are true (that
stays 1c's job); single-document only (a successor plan's discharge of an owed item is invisible here);
substring matching on "owed"/"OWED" with no disambiguation (a doc that merely quotes the word reads the
same as a real marker); a document that uses card headings as plain labels with NO status mechanism
anywhere (no ledger, no glyph on any card) still reads every card as `missing` — a known, disclosed gap,
not a guessed-at fix. Unlike `--doc-drift`'s always-0 report, this **gates**: exit 2 when `dialect="1"`
and any row carries `gating="1"`, exit 0 clean or `dialect="0"`, exit 1 only when FILE could not be read.
2. **Dead code** — `ripwire <dir> --dead-code[=DIR] --legend=compact`
`<dead-code count="N" confidence="high" evidence="internal-linkage+zero-callers"><d n="orphan" …/>` —
source-defined free functions with explicit internal linkage and **in-degree 0** in the indexed call
graph. `--dead-code=DIR` scopes to a sub-tree. Methods, declarations, headers, and external-linkage entry
points are excluded, so `count="0"` means "no high-confidence candidates," not "no dead code." Name-based
and tree-local — **verify before deleting**: confirm against the compiler's unused-symbol diagnostics or a
linker map; ripwire narrows the field, the toolchain proves it.
3. **Duplicate bodies** — `ripwire <dir> --clones --legend=compact`
`<clones groups="N" type3="M"><group type="2" tokens="161" n="2"><f n="line" p="…:31"/><f n="line"
p="…:28"/></group>`. `type="2"` = exact/renamed (identifiers + literals normalized, so a renamed copy
still matches); `type="3"` = a gapped NEAR-miss (similarity 0.80–1.0, an inserted/changed statement) —
check `type=` before assuming two members are byte-identical. Larger `tokens=` = more dedup value; a fix
to one likely belongs in all. **Rule of Three:** extract on the *third* occurrence, not the second — a
little duplication beats the *wrong* abstraction.
A group carrying `idiom="…"` (`threshold-ladder`, `switch-name-table`, `builder-chain`) plus
`demoted="1"` is one the detector judged an IDIOM COLLISION, not shared logic: same shape, no shared
domain identifiers, different scopes — five unrelated bucketing ladders in five subsystems match each
other's token stream and consolidating them would be the wrong abstraction. Demoted groups still print
and still count; they stop *gating*. The idiom is named precisely so you can overrule it by reading the
bodies — treat it as the tool showing its work, not as a row it decided for you.
4. **AST smells** — `ripwire <dir> --lint --legend=compact`
`<lint findings="N"><rule name="magic-number" count="82"/> …` then per-finding `<f rule= p= …>` with the
enclosing symbol. No-build, AST-only — a fast complement to clang-tidy, not a replacement. Skim the
per-rule counts for the dominant smell.
**Two rule prefixes are also family evidence.** `naming-*` (`naming-case`/`naming-confusable`/
GitHubで見る