Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
11a2b05
test(mcpverbscheck): pin the exact advertised MCP verb roster — a con…
usehoplite[bot] Sep 18, 2026
393c85c
fix(test): re-derive EXPECTED_VERBS in mcpverbscheck.sh for train 10'…
barefootski Sep 20, 2026
b4e1bd8
test: centralize GNU-vs-BSD stat compat; close a real-write env leak …
s0undt3ch Sep 19, 2026
da85a2d
test: fold four more gates onto statcompat.sh; close claudeconfigdirc…
s0undt3ch Sep 20, 2026
16c1e72
docs(readme,skills): the --top-k/--for inertness trap, first-run anti…
llvm-x86 Sep 20, 2026
d75c41a
docs(readme,skills): scope the --for budgeting rule, the --expand row…
llvm-x86 Sep 20, 2026
a7281de
fix(test): close six more hermetic-isolation leaks past PR #298's own…
barefootski Sep 20, 2026
556fc49
fix(dead-code): drop the hardcoded confidence="high" — a claim the co…
barefootski Sep 20, 2026
5a31662
fix(quality-delta): reuse-decline gets duplication's two clone-group …
barefootski Sep 20, 2026
ea789f8
fix(ingest): a Java/Kotlin import is a dependency edge, not a call
barefootski Sep 20, 2026
9e85c5a
fix(slice): disclose that unseeded --slice-flow=both reaches nothing …
barefootski Sep 20, 2026
0cf9774
docs(t13-followup): fix 1's confidence="high" and fix 3's import-as-e…
barefootski Sep 20, 2026
5644184
Merge lane/t13-contrib-finish into integration/train-13
joyful-ii-V-I Sep 20, 2026
b3afb29
Merge lane/t13-honesty-fixes into integration/train-13
barefootski Sep 20, 2026
34ef633
test(kotlincheck,qschemetrip): re-derive the JVM goldens and the mani…
barefootski Sep 20, 2026
dd9ca94
test(clean-env): git -C picks a directory, not a repository — clear G…
barefootski Sep 20, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 111 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,55 @@ not published here — see `docs/EVALS.md` for the instruments behind the headli

## [Unreleased]

### Fixed — `--dead-code` no longer claims a confidence its evidence cannot support

Every `--dead-code` root carried `confidence="high"`, hardcoded: nothing in the candidate loop — internal
linkage plus zero indexed callers — varied it, so the attribute was a constant wearing the shape of a
finding. The claim is also the one most likely to be acted on destructively, since the row an agent deletes
on a false "high" is live code reached by a virtual call, a reflection hook or a macro-generated caller,
none of which a name-based graph can see. The attribute is **removed** rather than replaced by a derived
number: there is no per-finding signal to derive one from, and `evidence="internal-linkage+zero-callers"`
already states the one thing the code actually knows. The full and compact legends, `--help` for
`--dead-code` and for `--safe-delete`'s `dead_code_candidate=`, and four skills that asserted the
confidence in prose all say the same thing now. `test/deadprecisioncheck.sh` asserts the attribute is
absent — the arm that used to pin the bug.

### Fixed — `reuse-decline` fired on clone groups `duplication` already exempts

`reportReusedClones` (kind `new-clone-of-reused-helper`) reads the same clone vectors as its sibling
`reportNewClones` (kind `duplication`) but carried neither of that sibling's two demotions: the
all-test-script skip, which exempts a group of sibling gate scripts repeating the house harness
boilerplate by convention, and the idiom→minor demotion. So a shell gate script cloning the house test
helper was exempt as `duplication` and gating as `reuse-decline`, from the same input — and that shape is
where the kind's one real-history firing came from. Both demotions are now applied identically in both
reporters.

### Fixed — a Java or Kotlin `import` is a dependency edge, not a call

`queries/java/tags.scm` and `queries/kotlin/tags.scm` captured an `import_declaration`/`import_header` as
`@reference.call`. That was inert while nothing owned a reference written outside every named definition;
once a top-level statement gained a `<file-scope>` owner (#60), an import line minted a real caller edge
and inflated `--callers=`/`--impact=` fan-in by one phantom caller per importing file. Both queries now
capture `@reference.import`, which routes to the existing `RefRole::Import` the C++ `using ns::name;` form
already used: the import stays fully visible on `--uses` as a `role="import"` use-site, and is excluded from
the call-graph CSR. No new C++ — the two query files are the whole change. `kParserVer` moves 118 → 119, so
any cache written by an earlier binary is refused and reparsed. Every other indexed language was checked:
Java and Kotlin were the only two that captured an import-shaped construct as a call.

A .kt file has no executable top level, so with this the Kotlin fixture correctly carries **no** module-scope
owner at all; `test/kotlincheck.sh` §1a pins that whole chain — no owner minted, the import still visible
with `role="import"`, and `square`'s fan-in counting real functions only.

### Fixed — unseeded `--slice-flow=both` now says that it adds nothing the flat rows do not

Unioned over a function's whole variable inventory, `--slice-flow=both`'s reach is byte-identical to the
union of the flat rows, which holds by construction: every flow row at depth ≥ 1 lands on an occurrence of
some other sliceable local, and that is already a row of *that* local's flat slice. Seeded with `--at=`, the
same analysis adds real reach. The unseeded case is not refused — the answer it returns is correct, just no
more informative — it is **disclosed**: the `<slice>` root carries `flow_redundant="1"` exactly when
`--slice-flow=both` runs with no `--at=` seed, never on `back`/`fwd` alone and never on a seeded run, with
one line in the legend pointing at the seed.

### Fixed — a call written outside every named function now has a caller, so `callers`, `impact`, `affected` and `test-gate` stop answering one short (#60)

A reference was attributed to the innermost definition whose span contains it, so a call written where no
Expand Down Expand Up @@ -301,6 +350,68 @@ the arm compares normally. When an arm is still refused, the root carries `reaso
debug trace. Both `ok=` postures and `reason=` are defined in the legend. Gates:
`test/scoutheadconflictcheck.sh` arms T9(a)–(e), `test/mergescoutcheck.sh`. (CodeRabbit review on #295)

### Fixed — sixteen gates now share one GNU/BSD `stat` compat helper instead of a per-gate copy

`stat -f` is GNU coreutils' filesystem-stat flag, not BSD's format-string flag, so it succeeds with junk
instead of failing — a caller-local `stat -f ... || stat -c ...` one-liner never reaches its own fallback on
Linux. Sixteen gates each hand-rolled the same detect-once-and-redefine fix independently:
`cachehashcheck.sh`, `cachesplitcheck.sh`, `clonecachecheck.sh`, `codexpromptroutecheck.sh`,
`evictioncheck.sh`, `g1freshcheck.sh`, `headsnapcachecheck.sh`, `mcpeditmodecheck.sh`,
`portablecachecheck.sh`, `prcontextcheck.sh`, `qsnapcachecheck.sh`, `qsnapprefetchcheck.sh`,
`statgatecheck.sh`, `cacheisolationcheck.sh`, `qsnapproducercheck.sh`, `sidecarsymlinkcheck.sh` and
`tempfilesymlinkcheck.sh` all now source the new shared `test/lib/statcompat.sh` instead — one place defines
the GNU-vs-BSD `stat` compat logic, not seventeen. Centralised by **@s0undt3ch** in #298.

### Fixed — a gate that builds a throwaway git repository could be aimed at the caller's repository instead

`git -C DIR` changes the working directory; it does not override the environment, and `GIT_DIR`,
`GIT_WORK_TREE`, `GIT_COMMON_DIR`, `GIT_INDEX_FILE`, `GIT_OBJECT_DIRECTORY`,
`GIT_ALTERNATE_OBJECT_DIRECTORIES` and `GIT_PREFIX` all outrank it. A gate that builds a fixture repo and
asks it a question therefore answered from somebody else's repository whenever one of those was exported —
by a git hook running the suite, a CI job, or a `git rebase` running the suite per commit — and then
passed or failed on data it never
selected. Measured at the reported call shape: with `GIT_DIR` set, `git -C "$REPO" init` created no `.git`
under `$REPO` at all, the gate's own commit landed **in the ambient repository**, and `git -C "$REPO"
rev-parse HEAD` read that foreign sha back, with the gate still reporting ALL PASS. The clearing joins the
agent-home names in the shared `test/lib/clean-env.sh` rather than becoming a 156th call-site copy: **155
gates** build a repo and were exposed, and the two that had already found this independently
(`dispatchordercheck.sh`, `pagingsweepcheck.sh`) had hand-rolled partial lists that each missed names the
other had. New gate `test/gitenvhermeticcheck.sh` proves the defect is live on this git, proves the helper
fixes it, pins the variable list against the helper, and sweeps the tree for a repo-building gate that does
not source it — with a control that strips the source line from a real gate and requires the sweep to flag
the copy. (CodeRabbit review on the train-13 branch)

### Fixed — a gate that varies `HOME=` per invocation could still leak into an ambiently-set agent-home variable

`CODEX_HOME`/`AGENTS_HOME`/`HERMES_HOME`/`CLAUDE_CONFIG_DIR`/`RIPWIRE_DATA_HOME` override the default an
agent's tools derive from `HOME`, so a gate that only sets `HOME=` per invocation is not actually sandboxed
on a machine where any of these is already exported ambiently. `codexpromptroutecheck.sh`,
`claudeconfigdircheck.sh`, `skillinstallcheck.sh` and `hermesinstallcheck.sh` now source the new shared
`test/lib/clean-env.sh` before varying `HOME=`, closing that leak in each. `claudeconfigdircheck.sh` — the
gate that exists specifically to test `CLAUDE_CONFIG_DIR` relocation — was the one this hit hardest: with
`CLAUDE_CONFIG_DIR` exported ambiently (a developer whose real Claude Code config is relocated, exactly the
case this gate tests for), its "unset" baseline arm wrote real files into that directory and then failed
comparing against its own contaminated baseline. Found and closed by **@s0undt3ch** in #298; carrying the
same shape through the rest of the suite closed six more — `hookcheck.sh`, `routehookcheck.sh`,
`agenttablecheck.sh`, `codexinstallhonestycheck.sh`, `meterdisclosurecheck.sh` and `releaseinstallcheck.sh`.

### Added — the advertised MCP verb roster is pinned by deriving it, so a count-preserving rename cannot slip through

`test/mcpverbscheck.sh` checked that `tools/list` advertises the expected *number* of verbs, which a rename
that swaps one name for another passes unchanged — and a renamed verb is a silently broken contract for every
agent that had wired up the old name. The gate now derives the roster from a live `tools/list` call and
compares the **names**, all 33 of them. Proved against the mutation it exists for: planting an added verb, a
removed verb and a count-preserving rename in the served stanza all redden the new arm, and the rename case
leaves the old count-only check green — which is the gap. Contributed by **@pt-act** in #291.

### Fixed — two documentation claims that described behaviour the binary does not have

`--expand=SYM` on an ambiguous name was documented without the scoping the disclosed `topk_default="0"`
actually applies, and the directory-of-repos row described a guard that does not exist, where the real
behaviour is a silent merge. Both corrected in `README.md` and in the `ripwire-navigate` and `ripwire-orient`
skills, against a built binary rather than from reading the source. Contributed by **@llvm-x86** in #302,
who built the Flask fixture that found them.

### Fixed — an ambiguous `--expand` buried its body behind the ranked map, and the escape hatch was stderr-only

Reported by @mariadb-KyleHutchinson in #289: `--expand=SYM` on a name matching more than one definition, in a
Expand Down
38 changes: 34 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -682,7 +682,16 @@ to do with it.** Two shapes get the most out of it, and one gets nothing.
<details>
<summary>The three controls that decide what it costs — <b>a budget</b> (<code>--token-budget</code> / <code>--top-k</code>), <b>routing</b> (<code>--help-task</code>), and <b>the skills</b> that teach an agent when <i>not</i> to reach for it</summary>

1. **A budget.** `--token-budget=N` caps the bundle; `--top-k=N` caps the rows. Unbudgeted `--for` returns
1. **A budget.** `--token-budget=N` caps the bundle; `--top-k=N` caps the rows **of the default map,
`--query`, `--format=candidates`, `--recall` and `--graph-query` — it does not shape plain `--for`**.
On `--for`, a *positive, explicit* `--top-k` is read by nothing: the run prints a stderr note
(`--top-k is not read by --for`) and still emits the full bundle. Two neighbours of that shape are
different and neither warns: `--for --format=candidates --top-k=N` *does* consume the flag (the
candidate export composes with it and caps the rows), and `--for --top-k=0` is refused outright by the
payload-only guard (`--top-k=0` needs a payload verb), not warned-and-emitted. Narrow plain `--for`
with its own arguments instead — `--signatures-only`
(drop the auto-bodies), `--token-budget=N` (shapes the bundle to fit), `--detail=N` (full bodies for
just the top N). Unbudgeted `--for` returns
a rich terminal bundle by design — right when it ends the question, wasteful when it does not.
2. **Routing.** `ripwire . --help-task="<task>"` names the ONE command the task actually wants, and
abstains when the evidence is thin. It is advice, it never runs anything. An answer of "just grep
Expand All @@ -692,6 +701,27 @@ to do with it.** Two shapes get the most out of it, and one gets nothing.
time — including the times it should not.
</details>

<details>
<summary>The invocations first-time users get wrong — <b>WRONG → RIGHT</b>, each row a real failure reported from the field</summary>

| WRONG | RIGHT | why |
|---|---|---|
| `ripwire . --for="…" --top-k=5` | `ripwire . --for="…" --signatures-only` (or `--token-budget=N`, `--detail=N`) | `--top-k` is inert on `--for`: the run warns on stderr and emits the full bundle anyway, so the agent *believes* it narrowed the output and did not. |
| `ripwire . --query="…"` as the default lens | `ripwire . --for="…"` | `--query` is the raw BM25 ranking — the binary's own help calls it debug and says "use --for". It is the right tool for hunting a vocabulary, the wrong default for a task. |
| `ripwire . --expand=SYM` where SYM is an **ambiguous** bare name | `ripwire . --expand=SYM --top-k=0` (or name it exactly: `--expand=FILE:NAME`) | A multi-match name keeps the ranked map — there IS something to disambiguate — so ~9K est_tokens of map ride along with the bodies. An **unambiguous** single match already defaults to `--top-k=0` on its own (disclosed as `topk_default="0"`); no flag needed there. |
| `--callers=<route handler>` expecting routes | find the URL in the project's own docs (e.g. a feature map), then `--expand` the handler | Framework route handlers have no callers in the graph — the decorator reaches them, not project code. Empty `--callers` on a handler is the design, not a bug. |
| `ripwire <dir-of-repos> …` (ONE root that happens to contain checkouts) | `cd` into ONE checkout first | Nothing refuses this: the crawl silently walks the nested repos and merges them into one corpus, so you pay for a map of everything and rank across unrelated codebases. Distinct from the real multi-root feature, which is N **explicit** positional roots (`ripwire dir1 dir2 … <verb>`, 2–16 checkouts merged on purpose). |

**What ripwire does not replace** — reach for `grep`/`read` here even when a verb looks close:

| still use grep/read for | why |
|---|---|
| route → handler lookup from a URL | ripwire ranks symbols, not URLs; it does not know your routes |
| templates, i18n catalogs, SQL migrations | not call-graph territory |
| one exact string in one file you can already name | `--grep=TERM` works, but `rg` is fine too — and the map is a fixed cost you did not need |
| UX flow through frontend event handlers | the JS is in the graph, but the flow needs line context, not a ranking |
</details>

<details>
<summary><b>What is measured and what is not</b> — these are single-agent figures; that the saving grows with the number of cold orientations is pre-registered and <b>unrun</b>, not a published result</summary>

Expand Down Expand Up @@ -1818,9 +1848,9 @@ wrong, and it has. These are the results that say so, all in-tree, all published
### In the tests

<details>
<summary><b>645 gate scripts</b>, five contracts no unit test can hold, and the house rule: write the gate before the code it measures</summary> <!-- gatecount -->
<summary><b>646 gate scripts</b>, five contracts no unit test can hold, and the house rule: write the gate before the code it measures</summary> <!-- gatecount -->

`test/regression.sh` names **645 gate scripts** and is the authoritative list; <!-- gatecount -->
`test/regression.sh` names **646 gate scripts** and is the authoritative list; <!-- gatecount -->
`python3 test/pargates.py . ./build/ripwire -j 6` runs the same set in parallel. On top of them sit the
contracts that do not fit a unit test: two runs byte-identical, warm output identical to cold, output
that pipes clean through `xmllint --noout`, a sanitizer build with `-fno-sanitize-recover=all`, and a
Expand Down Expand Up @@ -2027,7 +2057,7 @@ a runtime call with no syntax to read, so a Lua corpus reports no inheritance ed
implied), **Dart** (`.dart` — classes, mixins, extensions, enums, typedefs, functions, methods, getters/setters; `recv.m()`, `recv?.m()` and cascade `..m()` invocations are edges. Two stated floors: named constructors and factories index under the CLASS name, so `C()`, `C.seeded()` and `factory C.fromA()` are overloads of `C`; and `noSuchMethod` dynamic dispatch names its callee at run time. The grammar makes a function body a SIBLING of its signature rather than a child, so the definition span is extended through it at capture time — without that, every call in a body attributes to the enclosing class), **Elixir** (`.ex`/`.exs` — nested modules, structs, protocols and implementations, functions, macros, guards,
delegates, types, callbacks, attributes and literal ExUnit tests; module/name/arity resolution with lexical aliases,
filtered imports, default arguments, captures and pipes; see the
[static-analysis limits](docs/ARCHITECTURE.md#elixir-extraction)), **Kotlin** (`.kt` — classes, objects, companion objects, interfaces, enum classes and functions, extension functions included; bare and navigation calls, constructor delegation and imports are edges. Kotlin and Java share one call graph, and a call reaches the other language only when its own defines no candidate of that name, so adding `.kt` files never moves a Java edge. Stated floors: an explicit receiver (`A.f()`) does not narrow candidates; a multiplatform `expect`/`actual` type pair is two candidates; `.kts` is not indexed; and a file nesting string templates past 128 levels is refused and listed by `--skipped` — see the [Kotlin limits](docs/ARCHITECTURE.md#kotlin-extraction)), **GDScript** (`.gd` — a Godot file is a class body: `class_name` names it and its file-scope `func`/`var` are its members, with inner classes, constants, enums and their members, signals and call edges; `preload`/`load` produce no dependency edge yet, and `.tscn`/`.tres`/`.gdshader` are not indexed — see the [GDScript notes](docs/ARCHITECTURE.md#gdscript-extraction)), Bash, Go, Rust, Swift, C#, JSON + TOML + YAML (config keys — a
[static-analysis limits](docs/ARCHITECTURE.md#elixir-extraction)), **Kotlin** (`.kt` — classes, objects, companion objects, interfaces, enum classes and functions, extension functions included; bare and navigation calls and constructor delegation are call-graph edges; an import is a dependency edge only (role="import" on `--uses`, never a `--callers`/`--impact` edge — T13/fix3, 2026-09-20). Kotlin and Java share one call graph, and a call reaches the other language only when its own defines no candidate of that name, so adding `.kt` files never moves a Java edge. Stated floors: an explicit receiver (`A.f()`) does not narrow candidates; a multiplatform `expect`/`actual` type pair is two candidates; `.kts` is not indexed; and a file nesting string templates past 128 levels is refused and listed by `--skipped` — see the [Kotlin limits](docs/ARCHITECTURE.md#kotlin-extraction)), **GDScript** (`.gd` — a Godot file is a class body: `class_name` names it and its file-scope `func`/`var` are its members, with inner classes, constants, enums and their members, signals and call edges; `preload`/`load` produce no dependency edge yet, and `.tscn`/`.tres`/`.gdshader` are not indexed — see the [GDScript notes](docs/ARCHITECTURE.md#gdscript-extraction)), Bash, Go, Rust, Swift, C#, JSON + TOML + YAML (config keys — a
`[tool.ruff.lint]` table is one symbol under its full dotted name, and
`pyproject.toml`/`Cargo.toml`/CI workflows become greppable), and **Markdown** (`.md`/`.markdown` —
the DOC tier: every heading, ATX or setext, is a section symbol whose span runs to the next
Expand Down
Loading
Loading