{"slug": "ripwire-ripgrep-of-ai-context-cli-mcp-giving-coding-agents-a-map-of-any-repo", "title": "ripwire: ripgrep of AI context (CLI+MCP) giving coding agents a map of any repo", "summary": "Red Hat's Emerging Technologies group released ripwire, an open-source CLI and MCP server that generates a ranked, deterministic call graph for coding agents, aiming to replace grep-based exploration with token-budgeted answers about which symbols to touch, what breaks, and which tests to run. The tool supports 21 languages via vendored tree-sitter grammars and installs in one line, activating task-shaped skills for agents like Claude Code, Codex, Cursor, Windsurf, Gemini, opencode, and aider. On a sample repository, ripwire answered an incremental cache invalidation query in about 4.3K tokens, listing ranked symbols with risk annotations, one-hop call context, and confidence scores.", "body_md": "**ripwire is the ripgrep of AI context.** Point it at any repository and your agent gets a ranked,\ndeterministic call graph — what to touch, what it breaks, which tests to run — instead of grepping\naround and reading whole files.\n\n**Languages:** Rust · C++ · Objective-C/C++ · C · Metal · CUDA · Python · Go · Swift · TypeScript ·\nJavaScript · Java · Ruby · PHP · Lua · Bash · C# · JSON · TOML · YAML · Markdown — [twenty-one\nvendored grammars](#languages), and adding another is a vendored tree-sitter grammar plus one row in a\ndeclarative table.\n\nOne self-contained binary on your own machine, offline, installed in one line — and the same line\ninstalls *and activates* the task-shaped skills that teach your agent *when* to reach for it, not\njust how, for every agent it finds on the machine. If your\nagent can run shell commands — Claude Code, Codex, Cursor, Windsurf, Gemini, opencode, aider — it is\nset up the moment the install finishes; [the MCP server is the optional second\ninterface](#set-it-up-in-your-coding-agent). Install it and ask it something before you finish\nreading this page:\n\n```\nRIPWIRE_REPO=redhat-et/ripwire bash -c \"$(curl -fsSL https://raw.githubusercontent.com/redhat-et/ripwire/main/scripts/install.sh)\"\nripwire . --for=\"incremental cache invalidation\"\n```\n\nOne deterministic, token-budgeted answer: the relevant symbols, their callers, the change risks, and the tests that reach them. Run on this repository (2026-08-30), that second line answers in about 4.3K tokens with:\n\n- **The ranked symbols, in rank order** — the cache-header constant`kCacheMagic` first (with its doc\ncomment quoted in place and the one`next=` call that opens it), then`spanTierMemoPath` (the\ncache-path composer),`ingestCommitTree` , …`ingest` — each row with its file, line, and signature.\n- **Risk, annotated in place** — complexity, git churn (`ingest` shows 128 recent edits),\nchange amplification (touch`ingest` and 266 graph nodes feel it), purity and test coverage. The\nfragile spots are visible*before* anything touches them.\n- **One-hop call context** —`spanTierMemoPath` calls`shaKeyedCachePath` ,`headSnapRepoHex` ,`exclConfigHex` ; no second query needed to see the neighbourhood.\n- **Its own confidence** — this answer says`confidence=\"high\"` with the score margin attached; a\nflat ranking says`low` , so it reads as a starting point instead of masquerading as an answer.\n\nEvery number on this page is a measurement on a corpus we happen to have. **Yours is one we don't.**\n\nAfter you have actually used it on your own repository for a while, hand your agent\n[`prompts/improve-for-my-language.md`](/redhat-et/ripwire/blob/main/prompts/improve-for-my-language.md). It harvests that\nsession's own transcript — where ripwire answered, where it missed, where you fell back to grep —\nand every finding it produces has to cite the moment it came from: what you asked, which command\nran, what came back. Open an issue with the result.\n\nThat is worth more to this project than a bug report, because it arrives in the form the project already runs on: evidence with its provenance attached, not an impression. Several languages here are one contributor's corpus away from being measurably better, and we cannot see your code.\n\n## The actual wire format — what your agent reads (minified XML; trimmed and line-wrapped here)\n\n```\n<ctx task=\"incremental cache invalidation\" confidence=\"high\" margin_pct=\"20\"\n     bundle=\"compact\" bodies=\"0\" reason=\"compact-route\" est_tokens=\"3995\">\n  <sigs shown=\"23\" total=\"40\" capped=\"1\">\n    <d l=\"106\" n=\"kCacheMagic\" p=\"src/ingest_cache.h\" cx=\"0\" in=\"0\" churn=\"11\" amp=\"71\" pure=\"1\" r=\"1\"\n       next=\"--expand=src/ingest_cache.h:kCacheMagic\">\n      <doc>incremental cache (--cache): per-file content hash + raw facts so a\n           re-run re-parses ONLY …</doc>constexpr std::uint32_t kCacheMagic = …</d>\n    <d l=\"1307\" n=\"spanTierMemoPath\" p=\"src/ingest_astquery.h\" cx=\"1\" in=\"2\" churn=\"5\" amp=\"44\" r=\"2\"> … </d>\n    <d l=\"247\" n=\"ingestCommitTree\" p=\"src/dmm.h\" cx=\"6\" in=\"1\" churn=\"6\" amp=\"27\" r=\"3\"> … </d>\n    …\n    <d l=\"191\" n=\"ingest\" p=\"src/ingest.cpp\" cx=\"4\" in=\"14\" churn=\"128\" amp=\"266\" tested=\"1\" r=\"13\"> … </d>\n    … </sigs>\n  <hops shown=\"2\" total=\"6\" capped=\"1\" noedge=\"2\">\n    <h l=\"1307\" p=\"src/ingest_astquery.h\" n=\"spanTierMemoPath\">\n      <calls total=\"3\"><c n=\"shaKeyedCachePath\" l=\"1621\"/> … </calls></h> … </hops>\n</ctx>\n```\n\n`cx=` complexity, `churn=` git edit frequency, `amp=` change amplification, `r=` rank; `<hops>` rows\ncarry the one-hop call context, caps disclosed. Every attribute is defined in the one legend at the\ntop of the real output, which also self-reports the bundle's cost — `est_tokens=\"3995\"` here.\n\n| The agent without a map | The agent with ripwire | \n|---|---|\n| greps a common word, gets hundreds of hits across dozens of files | one ranked answer — `est_tokens=\"3995\"` on this repository (re-derived 2026-09-05, the run above) | \n| reads whole files to find the symbols that matter | those symbols, with complexity, churn and test coverage inline | \n| finds the callers only if it thinks to grep for them too | callers, blast radius and the tests to run, in the same bundle | \n| pays for every line it read, right or wrong | measured at **5.0%** of what that grep-and-read pass spends (re-derived 2026-08-23) | \n\nAnd against five retrieval competitors on a held-out LocBench slice, it finds **all** gold files in\nthe top 10 on **58.3%** of instances — the best alternative lands 40.0% — while indexing in 0.31 s.\n[The full leaderboard, losses included ↓](#graph-ranked-retrieval-it-finds-the-right-files-more-often-than-the-alternatives)\n\n**Django's migration autodetector, coloured by complexity.** Thresholds are fixed, so the colour means the same thing on every repo you point it at.\n\n```\nripwire path/to/django/db/migrations --rank-by=rrf --top-k=120 --color-by=cx --html=map.html\n```\n\n| <sub>**The same graph, re-coloured by git churn.** 76% of these nodes move to a different band — structure and history disagree, and one run shows you both.</sub> | <sub>**A dashed shaft is a guess.** 31 of 183 edges here are one arm of a split the resolver could not choose between. No other tool marks which of its arrows it is unsure about.</sub> | \n\n## How to read these pictures — the five lenses, the fixed thresholds, and what the renderer refuses to draw\n\nOne self-contained HTML file (`--html[=FILE]`), no server, no CDN, no external asset. `--color-by=lang|community|cx|churn|tested` sets the initial colour; the page embeds all five and keeps a live selector, so switching lens costs no second run.\n\nRead from the figures above, which state their own rules in a sidecar saved beside each image:\n\n- **arrow points caller → callee** — the graph is directed, and the page draws it that way.\n- **`31 of 183 shafts dashed in this view = the resolver could not choose between same-name definitions and split the call over all of them`** — per*edge* , not per symbol. A symbol-level \"this function makes some ambiguous calls\" would mark every one of its edges, which would be a lie about most of them.\n- **labels: top 24 by in-view degree, one per name** — one label per distinct name, so a picture of a container class stops crowding out the functions you asked about.\n- **shapes: ● fn ■ cls ✚ var** — kind is nominal data on a nominal channel; complexity never uses shape.\n- **module outlines: `7 of 12 modules with 3+ nodes in view (cap 12; 3 dropped as too thin to read as a region; 2 dropped as enclosing mostly other modules)`** — three separate truncations, each with its own count and its own reason.\n\nThe `cx` and `churn` ramps share one five-stop scale, ordered so lightness rises with the value — it survives greyscale printing, and every adjacent pair stays separable under protanopia, deuteranopia and tritanopia. Thresholds are fixed rather than per-corpus quantiles, so a hot node cannot be manufactured by a cold repository.\n\n`churn` needs real git history: a shallow clone reports every file as one commit, and a directory with no repository says `churn unavailable` rather than drawing zeros.\n\nTen everyday moments, re-measured on this repository, 2026-08-08. Figures are ~tokens (≈ bytes/4);\nevery ratio comes from a real run, reproduced by the command in its row — raw byte counts and exact\ncommands in\n[`docs/EVALS.md` §5](/redhat-et/ripwire/blob/main/docs/EVALS.md#readme-grade-rows-re-measured-on-this-repository-2026-08-08).\n\nOrdered understand → navigate → review-the-change:\n\n| Ask it | Command | ripwire | naive read | token savings | \n|---|---|---|---|---|\n| \"Orient me in this repo\" | `ripwire .` | **~5.6K tok** | ~20K–25K tok — read `README.md` (+`docs/ARCHITECTURE.md` ) | 3.6×–4.5× | \n| \"Where is X handled?\" | `ripwire . --for=\"…\"` | **~2.1K tok** | ~4.9K–20K tok — `grep -rn <term> src/` , then read the file it points at | 2.3×–9.3× | \n| \"What do I already know?\" | `ripwire . --recall=\"…\"` | **~15K tok** | ~445K tok — read all 119 markdown docs this repo carries | 29.2× | \n| \"Set me up for this task\" | `ripwire . --pack-task=\"…\"` | **~2.1K tok** | ~16K–80K tok — read every relevant file, whole | 7.7×–37.7× | \n| \"Show me this one function\" | `ripwire . --expand=SYM --top-k=0` | **~260–16.5K tok** body (+~5.7K for the ranked-neighborhood bundle) | ~43K–174K tok — read the whole file it lives in | 2.6×–670× | \n| \"Who calls this function?\" | `ripwire . --callers=SYM` | **~580 tok** | ~40K–52K tok — `grep -rn SYM src/` (mostly noise), then open 2–3 files to sort real calls from mentions | 69.2×–89.1× | \n| \"Is it safe to change this?\" | `ripwire . --impact=SYM` +`--uses=SYM` | **~1.3K tok** | ~18K tok — open every direct-use file, whole | 14.4× | \n| \"I have a stack trace\" | `ripwire . --from-trace=FILE` | **~1.4K tok** | ~124K–298K tok — grep all 7 frame names, then open the innermost file(s) | 86.9×–208.6× | \n| \"I changed these files — tests? blast radius?\" | `ripwire . --situ` | **~410 tok** | ~3K–132K tok — `git diff` +`grep -rn <syms> test/` , then open the candidates | 7.3×–324.2× | \n| \"Review this PR/diff\" | `ripwire . --pr-context=REF` | **~1.9K tok** | ~4.8K–51K tok — `git diff REF` , then open the touched files | 2.6×–27.5× | \n\n## Same-correct-answer verification, and the honesty line these ratios come with\n\n**These aren't summaries that gamble with information.** Each row is scored\nsame-correct-answer-or-it-doesn't-count, and both sides were checked, not assumed: orient surfaces\nthis repo's own pipeline files (`ingest.cpp`, `graph.h`, `serialize.h`) in the first screen, the same\nthree `docs/ARCHITECTURE.md` names as central; the `--for` row lands `mcpStale`\n(`src/mcpindex.h:633`), the actual staleness check, 5th-ranked; `--recall` lands the container-rule\ndoc (`AGENTS.md`) that states, verbatim, the same \"no `std::map`\" rule `CONTRIBUTING.md` explains in\nfull; `--pack-task` names the same three touch points a human would — `cachelint.h`, `mergeCachePack`\n(`src/main.cpp:1787`), the `lintrules.h` helpers it reuses; `--expand --top-k=0` hands back the\nrequested function's complete, unmodified body — the ranked-neighborhood addition costs the same\n~22.6 KB regardless of which function you ask for, confirmed on two (a fixed floor, not per-function\nvariance); `--callers` on `langOfPath` names its 2 real callers, the same ones a `grep` hit-list\nburies under 5 files of comment-only mentions; `--impact`+`--uses` on `coversOrEquals` names the same\n2 direct call sites `--uses` alone would, plus (disclosed) a transitive reach `--uses` doesn't cover\nat all; `--from-trace` resolves all 7 frames of a real call chain by name to the same definitions a\nper-frame grep would eventually find, mixed with call sites and comments; `--situ` on a 2-file diff\nnames the same 6 real test harnesses, 2 of which a filename grep across `test/` cannot find even\nafter opening every one of its 41 candidates — a completeness gap, not just a byte one; `--pr-context`\nsurfaces co-change partners (`test/regression.sh`, `src/main.cpp`) a raw `git diff` has no way to\nknow were usually touched and weren't this time. The map ranks and discloses — it never paraphrases\nyour code — and every truncation is disclosed in the header.\n\n**The honesty line, made concrete:** the same auto-selection behind the `--expand` row also runs the\nother way. On a small file (`pageRankDouble` in `src/pagerank.cpp`, 5,559 B) the ranked bundle would\ncost 27,916 B — nearly 5× more than the file — so ripwire serves the file itself instead, disclosed\nas `mode=\"whole-file\"` on the response, not silently.\n[`docs/EVALS.md` §7](/redhat-et/ripwire/blob/main/docs/EVALS.md#7-honest-counterexamples) lists that and the other counterexamples\nthis project publishes against itself.\n\n**Won 27 · lost 7 · tied 14** on 48 matched questions across django, webpack and this repository,\nspending **~77K tokens against its ~486K** for the whole sweep.\n\n## The full result, how it was measured, and where ripwire still loses\n\nThe 48 questions span symbol lookup, conceptual search, blast radius, and one-call task orientation. ripwire indexes the same three repositories in 0.25–0.45 s and 6.6–16.5 MB, against that server's 23–52 s and 391–623 MB, and answers a warm query in a median 197 ms against its 1,082 ms. Its seven wins are real and named one by one in the method.\n\nBoth arms warm with a pre-built index, median of 3 timed calls, stdout to a file rather than a pipe.\nThe competitor ran in its stronger retrieval configuration; its numbers were recorded once and then\nfrozen, and ripwire's side was re-run after the fixes the first pass produced. Per class, as a share\nof the competitor's bytes on totals: symbol lookup **1.35×**, conceptual search **1.23×**, blast\nradius **0.39×**, task orientation **0.06×**.\n\nWhere it loses: on a plain one-symbol lookup the competitor answers in about a kilobyte carrying callers and callees, and ripwire spends roughly three times that to also hand back the body. It ranks a chunk-id plugin first on one webpack query where ripwire never surfaces the directory at all — a ranking miss, and a fix for it was built, met its pre-registered band, and was reverted anyway for failing a separate standing requirement. Its depth-labelled blast radius and its import edges are both better presentations than ripwire's flat reaching-set.\n\nFull method, pins, per-class tables, the carried-versus-re-judged ledger, and the complete list of\nwhat the competitor does better:\n[`docs/EVALS.md` §2](/redhat-et/ripwire/blob/main/docs/EVALS.md#2-head-to-head-against-other-tools).\n\n**Nothing it is unsure about reaches your agent unlabelled — and nothing it could not see goes\nunnamed.** Every guess is marked in the output, and every mark has a next step — up to handing it a\ncompiler-grade index. Point it at a repository whose main language it has no grammar for and the\nmap's first line says so (`unindexed=\"ml:793,mli:607,…\"` on a facebook/infer clone); a file it\nindexed but cannot vouch for carries a parse-health row; every file the crawl passed over is\nitemized with its reason. A confident-looking map that lies by omission is the failure mode this\ntool refuses.\n[What it misses, and what to run next →](#what-it-misses-and-what-to-run-next)\n\n**58.3% against 40.0% for the best tool tested — and it answers before they finish indexing.** Every\narm below was re-run in full on 2026-08-08 — one ripwire binary (the profile-guided release build\nthat now ships), one evaluator, one 60-instance held-out LocBench slice: paired, zero exclusions,\nsame gold set, and the metric code imported unmodified into every arm. *Strict file@10 = **all** gold files inside the top 10*, which is whether\nyour agent starts in the right place at all.\n\n| Round 4 — LocBench, Python-dominant | strict file@10 | any@10 | index (median) | query (median) | \n|---|---|---|---|---|\n| **ripwire `--for`** | **58.3%** | **85.0%** | **0.31 s** | 0.108 s | \n| codebase-memory-mcp 0.9.0 | 40.0% | 63.3% | 1.24 s | 0.075 s | \n| repowise 0.37.0 | 33.3% | 53.3% | 34.0 s | 1.159 s | \n| graphify 0.9.34 | 31.7% | 46.7% | 7.82 s | 0.614 s | \n| Aider repo-map 0.86.2 | 20.0% | 35.0% | *(inside query)* | 2.920 s | \n| codeseek 0.1.31 (better of its two arms) | 15.0% | 20.0% | 3.37 s | 0.040 s | \n\nRipwire leads every arm on both accuracy metrics and in both strata. Paired, the losses are small and\nthey are published: **2** instances to codebase-memory-mcp, **2** to repowise, **1** each to graphify\nand aider. **Cold from nothing to an answer — parse, rank, reply, no cache — ripwire takes 0.213 s**,\nagainst a ~35 s index-then-query for repowise; its worst single index in this run was **352 s**.\n\nThree things this table costs us, said plainly. **codebase-memory-mcp is the real runner-up at 40.0%,\nnot repowise** — an earlier round credited it with 26.7%, and re-running it fairly raised it. The\nmargin over the best competitor is therefore **1.46×**, not the 1.75× two separately-dated tables used\nto imply. And **multi-file gold is hard for everyone**: ripwire leads the stratum at 21.4% strict, but\nits own any@10 there is 78.6% — it finds *a* gold file and misses the siblings, and no arm in this\ntable solves that.\n\nHeld out wider — **243 instances across 78 repositories** — ripwire lands **60.9%** against **27.6%**\nfor its own pre-routing baseline: **+33.3pp** paired, clustered-bootstrap 95% lower bound **+25.0pp**,\nbought for +3.4% warm latency and **−39.4%** tokens. Full provenance, the losing instances one by one,\nand a third round against a compression-layer competitor: [Measured](#measured) and\n[`bench/headtohead/r4-2026-08-06/`](/redhat-et/ripwire/blob/main/bench/headtohead/r4-2026-08-06), whose harness is committed so\nanyone can re-run the whole comparison.\n\nTest scaffolding does not pollute the ranking from inside source files either: `#[cfg(test)] mod tests`, `describe()` blocks, `Test*` classes and `[Fact]` attributes are detected syntactically,\nwherever they live — not by file path alone. On an astral-sh/ruff clone (5,945 files),\n`--ignore-tests` removes **23,907** test symbols where path rules alone caught 18,532\n(`ripwire <ruff> --ignore-tests`, 2026-08-14; the per-language fixtures are pinned by\n`test/testscopecheck.sh`).\n\n*The table above, read two other ways. Every figure in both is one of its measured numbers; only the\nmanners and the cynicism are editorial.*\n\n## 📖  **The same table, as narrated by Jane Austen**\n\n*A Survey of the Neighbourhood's Eligible Instruments — being an account of five gentlemen of\nretrieval, and one lady of no pretension whatsoever.*\n\nIt is a truth universally acknowledged, that an engineer in possession of a large repository must be in want of a map.\n\n**Mrs. Codebase-Memory** must be named first, for she has risen a great deal in the estimation of the\nneighbourhood — two-fifths of her answers entirely correct, which is more than any other caller can\nsay, and she is ready in a second and a quarter. It must nevertheless be recorded that her card\nannounces accomplishments in the semantic line; that upon enquiry the semantic line is not at home;\nand that the household denies all knowledge of it. One is left with the impression of a capable woman\nill-served by whoever prints her cards.\n\n**Mr. Repowise** is by common consent the most substantial of the party, and no one who has waited\nupon him would dispute it. He is possessed of a handsome index and a manner of great thoroughness;\nbut he must be *seen to*. One does not simply address Mr. Repowise. One sends word, and dresses, and\nwaits — three-and-thirty seconds on an ordinary morning, and upon one memorable occasion in the\ncountry, seven minutes and four seconds — during which interval a less consequential neighbour has\nanswered the question, taken her leave, and thought no more about it. He answers creditably when at\nlast he arrives, one time in three; whether that is worth the toilette, each family must determine\nfor itself.\n\n**Mr. Graphify** enjoys a great many admirers. He does not rank his acquaintances; he calls upon them\nin whatever order his walk happens to take him, and reports the order of the walk as though it were\nan opinion. He has been known to arrive carrying a hundred and thirty megabytes of correspondence.\nPressed once for any answer at all, he replied that no matching nodes were found, and considered the\nmatter closed.\n\n**Mr. Aider** is the most gentlemanly of the company and by far the most difficult to consult. He\ncannot be asked a question — the thing is simply not done. One may mention names in his hearing and\nhope he takes the hint; he does take it, and is fully ten points the better for it, which says more\nabout the hint than about Mr. Aider. But he forms his view of the neighbourhood before you speak and\nretains it after, and one cannot escape the feeling that the conversation was never truly with you.\n\n**Mr. Codeseek** is a young gentleman of quick habits who suffers from an affliction of address.\nSpeak to him plainly, in the language of ordinary complaint, and he will regard you with perfect\ncomposure and say nothing whatever — nothing, upon sixty occasions out of sixty. Name a person\nprecisely as that person is named, and he grows animated directly. It is not stupidity; it is a want\nof imagination in the matter of introductions.\n\nAnd there is **ripwire**, of whom nothing is said in the drawing rooms, because she has already gone\nhome. She was asked; she answered, in thirteen hundredths of a second; every gold file within the\nfirst ten, in eight-and-fifty cases of the hundred. She keeps no establishment, corresponds with no\ndistant authority, and has never once been indexed at a party. Mr. Repowise finds her abrupt.\n\nShe is.\n\n<sub>Every figure above is a measured number from the round-4 table on this page: the index medians\n(1.24 s, 34.0 s, 3.37 s) and repowise's 352 s worst case, the 40.0% and 33.3% strict file@10, the\n0-results-on-60/60 fallback arm, the absent `semantic_query` tool, graphify's 129 MB\nlargest graph and its 1-of-60 empty ranking, aider's +10 pp personalization delta, and ripwire's\n0.108 s / 58.3%. Provenance in [bench/headtohead/r4-2026-08-06/](/redhat-et/ripwire/blob/main/bench/headtohead/r4-2026-08-06)\nand [docs/EVALS.md](/redhat-et/ripwire/blob/main/docs/EVALS.md); only the manners are editorial. These are other\npeople's real work, and the joke is aimed at the trade-offs, never at the authors.</sub>\n\n## 🕵️  **The same table, worked as a case file** — a private eye who trusts no index he didn't build himself\n\n*The Long Index — in which a man asks six informants one simple question, and only one of them has\nthe decency to answer it.*\n\nIt was a million lines of somebody else's mistakes, and I needed one file out of it before the coffee went cold. So I did what you do. I went and talked to the people who say they know the neighborhood.\n\n**Codebase-Memory** had the best record in the room and she knew it — two answers right out of every\nfive, handed over in a second and a quarter, which in this business is practically a kindness.\nTrouble was the card. Right under her name it said *semantic query*, real classy, real\nexpensive-looking. I asked to see it. She said it wasn't in. I asked the house. The house had never\nheard of it. I've known a lot of good people ruined by whoever printed their cards.\n\n**Repowise** was the heavyweight — everybody told me so before I got through the door. Big index,\ngood tailoring, thorough as a tax man. Only you don't just ask Repowise a question. You send word.\nYou wait. Thirty-three seconds on a good day, and one bad morning out in the country, seven minutes\nand four seconds — long enough to get the same answer somewhere else, drive home, and forget his\nname. He came through one time in three. For some outfits that's worth the wait. I've got a\nmetabolism.\n\n**Graphify** never met a fact he wouldn't hand you. Ask him one thing and he turns up with a hundred\nand twenty-nine megabytes of everything, unsorted, in whatever order he tripped over it — and he'll\nreport that order like it's a considered opinion. It isn't. Leaned on him once for anything at all;\nhe looked me dead in the eye, said no matching nodes, and figured we were square.\n\n**Aider** was a gentleman, which is another way of saying you couldn't file a straight question into\nhim in triplicate. Wouldn't be asked. You mention things, loud, and hope — and sure enough, drop the\nright names and he's ten points sharper, which tells you everything about the names and nothing about\nAider. He'd made up his mind about the place before I opened mine, and kept it after. You never did\nfeel the conversation was with you.\n\n**Codeseek** was young and had a condition. Talk to him like a human being — plain, tired, the way a\nman actually asks for help — and he'll look clean through you and say nothing. Sixty times out of\nsixty, nothing. But name the thing exactly, badge number and all, and the kid lights right up. It\nisn't that he's slow. He just never learned how people knock on a door.\n\nAnd **ripwire**. Nobody at the table brought her up, on account of she'd already left. Took the\nquestion, answered it in thirteen hundredths of a second — every file I needed inside the first ten,\nfifty-eight times out of a hundred — keeps no office, wires no head branch, never once got herself\nindexed at a party. Repowise says she's abrupt.\n\nShe is. That's why I hired her.\n\n<sub>Every figure above is a measured number from the round-4 table on this page: the index medians\n(1.24 s, 3.37 s, 34.0 s), repowise's 352 s / six-minute worst case, the 40.0% and 33.3% strict\nfile@10, codeseek's 0-of-60 plain-language arm, codebase-memory's advertised-but-absent semantic\ntool, graphify's 129 MB largest graph and its 1-in-60 empty return, aider's +10 pp name-drop delta,\nand ripwire's 0.108 s / 58.3%. Provenance in\n[bench/headtohead/r4-2026-08-06/](/redhat-et/ripwire/blob/main/bench/headtohead/r4-2026-08-06) and\n[docs/EVALS.md](/redhat-et/ripwire/blob/main/docs/EVALS.md); only the cynicism is editorial. These are other people's\nreal work, and the joke is aimed at the trade-offs, never the authors.</sub>\n\n**Name a symbol and it is the first hit — and it is never a mystery which ranker answered.** Every\n`--for` query is served by one of three lanes; a confidence-gated router picks by reading the\nquery's *shape*, discloses its choice on the output (`route=`), and `--no-route` overrides it.\nRouting lifts recall@1 on name-shaped queries **61.1% → 91.3%** in `src/`, **59.2% → 85.5%** at the\nrepository root — and the gate is load-bearing in both directions: route *everything* to the name\nlane and prose queries collapse from **0.967 MRR to 0.016**. Both numbers ship together. Reproduce\nwith `ripwire <dir> --eval-retrieval`, which grades EVERY doc-commented symbol in the corpus and\nprints its own `population=`/` scored=`/` rule=`; the full per-ranker tables are in\n[`bench/ANSWERQUALITY.md`](/redhat-et/ripwire/blob/main/bench/ANSWERQUALITY.md#re-measured-2026-09-05--census-sampler-midrank-ties-the-current-numbers).\n\n| Lane | Built for | Why it wins there | Where it loses | \n|---|---|---|---|\n| **name-exact** | identifier-shaped queries ( `chooseForRanker` ,`pack task` ) | whole-name match ignores body noise: **91.3%** recall@1,**0.960** MRR in`src/` | scores zero on any word that is not literally a name — forced onto prose it dies ( **0.016** MRR) | \n| **subtoken+body** | prose and task queries (\"where is the content hash computed\") | the only lane that matches vocabulary living in doc comments and bodies | exact names drown in shared subtokens ( **61.1%** recall@1 on name queries) | \n| **mention anchor** | a pasted path, `Type.method` , or issue URL | a literal mention is lifted above any score — paste the ticket, don't paraphrase it | adds nothing when the query names no artifact | \n\n## **How each lane finds things, and how the router picks — step by step**\n\n**How the conceptual lane finds what you didn't name.** The subtoken+body lane is why a query with\nno symbol name in it still lands:\n\n- **Both sides are split into subtokens.**`SplitChunksPlugin` becomes`split` +`chunks` +`plugin` , and\nso does your query — so words match*pieces* of names you never typed.\n- **Three evidence fields, not one.** A symbol is scored on its name subtokens, its doc comment, and\nits body — vocabulary that only exists in a comment or an implementation still finds its symbol.\n- **BM25 with per-query IDF.** Rare, discriminating words dominate the score; words the whole corpus\nshares contribute almost nothing. Type the three words only the right function uses and they carry\nthe query.\n- **Lookalikes are down-weighted, not hidden.** Fixture, test-data, and generated paths score at a\nfraction, so a test vocabulary-twin cannot outrank the real source (adversarial-class pollution@5:\n28% → 0%,`docs/EVALS.md` §4) — but they stay in the index and are still found when*asked for* .\n- **The list ends at a cliff, not a quota.** The cut is adaptive: output stops where the scores drop\noff, so a sharp answer is a short list and a diffuse one is disclosed as such, instead of a fixed\ntop-k padding both.\n\n**How the router picks.** The gate is built on cheap, corpus-derived evidence, and its bias reflects\nan asymmetry the table above makes plain: a *missed* name-route costs a few ranks; a *false* one is\ncatastrophic (0.016 MRR).\n\n1. **Identifier shape is trusted outright.** A camelCase/snake token — or a short query carrying\none — routes name-exact. Someone who types`chooseForRanker` is naming, not describing.\n2. **The all-words test.** If*every* content word equals some symbol's whole name (`pack task` where both`pack` and`task` are real symbols), that is strong evidence of a name query.\n3. **The plausibility test.** Present is not enough — each matched name must be*specific* : few\ndefinitions, and not a subtoken carried by half the corpus's symbol names (thresholds derived\nfrom the index itself, not a hardcoded stdlib list). This is what catches`split chunks` :\nevery word names a symbol, but`split` names a String method defined everywhere — so the route\nis**declined** and the conceptual lane runs, which finds`SplitChunksPlugin` easily.\n4. **Every decision is disclosed.**`route=` states the lane that ran; a decline names the anchor\nthat failed and why.`--no-route` forces the conceptual lane when you disagree.\n\nThe proof the gate earns its keep: the routed lane matches the *best* single lane on both query\nmodes simultaneously — 0.960 MRR on names (equal to forced name-exact) and 0.967 on prose (within\nnoise of pure subtoken+body, `src/`). No single lane does that.\n\nThe honest boundary: the router classifies the query's shape — it cannot rescue vocabulary that is\nnot in the index. A prose query whose concept lives only in a compound class name\n(`SplitChunksPlugin` contains no `splits` subtoken) is correctly sent to the conceptual lane, which\nthen has little to grab; that gap is measured and recorded in\n[docs/EVALS.md §7](/redhat-et/ripwire/blob/main/docs/EVALS.md), not hidden. Numbers re-derived 2026-08-08 on this tree;\nper-lane table and history in [docs/EVALS.md §4](/redhat-et/ripwire/blob/main/docs/EVALS.md).\n\nOn mid-task questions it had never seen, ripwire answers at **5.0%** of what a grep-and-read pass\nspends — **5.2%** on the questions both arms fully answered. `--pack-signatures` returns **74.7%\nfewer bytes** than full bodies at top-50 (re-derived on this tree, 2026-09-06). The output is already\ndense enough that running a dedicated context compressor over it saved **exactly 0 tokens**.\n\nBoth of those first two figures moved when they were re-derived on 2026-08-23, and they moved in\n**opposite** directions — 7.3% → 5.0% overall, but 1.7% → 5.2% on the both-answered subset. Same\nfrozen questions, same frozen verb ladders, same corpus pin, same tokenizer; the naive arm reproduced\nto the token. What changed is where ripwire spends: the compact conceptual route made its *misses*\nmuch cheaper, while richer default bundles made the questions it *answers* dearer. Both numbers are\nprinted because printing only the one that improved would be the failure this project exists to not\ncommit. The full per-question re-derivation is in the Round 3 note under [Measured](#measured).\n\nIt is also cheap enough to call on reflex: this repository parses in **~0.15 s** cold and **~0.10 s**\nwarm (`time ./build/ripwire . --no-cache`), so the agent asks instead of guessing.\n\n### Better Code: It automates the review judgments nobody has time to make — every lens from published research\n\n`--quality-panel` runs the calls a good reviewer makes by hand — is this function too tangled, is it\nnamed badly, does it hide control flow inside an idiom, does its history say it keeps breaking, must\nyou read five other files to follow it, does it mutate state three hops away — as **six independent\nevidence families**, and ranks by how many of them *agree*, never as one blended score. Each family\nimplements published work — McCabe on shape, Butler on naming, Gopstein's atoms of confusion on\nidiom, Nagappan & Ball on churn, Beck & Diehl on colocation, Henry & Kafura on state — with the\nlesson taken from each paper, and the rules measured and *withdrawn*, in\n[`docs/LINEAGE.md`](/redhat-et/ripwire/blob/main/docs/LINEAGE.md). Pooled over five corpora (n = 27,889) the largest correlation\nbetween any two families is **+0.168**: they really are measuring different things, so two families\nfiring on the same function is corroboration rather than one metric counted twice.\n\nThat matters most for code an agent wrote. Empty-catch error masking is **+47%** more common in\nAI-authored commits, a function rewritten again inside two weeks **+15%** more likely, and reuse is\n*declining* as AI's share of commits grows (GitClear, *AI Copilot Code Quality*, 2026). Each of\n`--quality-delta`'s 10 kinds targets one measured mode like those, and it reports **only what your\nchange made worse** — then `--exemplar` shows the pattern in your own repo to copy, and `--test-gate`\nnames the tests that must run before \"done.\"\n\nTree-local numbers above are reproducible with the commands shown; the rest are dated, sourced\nmeasurements in [`docs/EVALS.md`](/redhat-et/ripwire/blob/main/docs/EVALS.md) — each with its instrument, its corpus, and its\ncounterexamples, because the losses ship beside the wins. Zero runtime dependencies, C++23, builds\nwith the network off.\n\nBuilt for **Codex, Claude Code, Cursor, Windsurf, Gemini, opencode, aider**, and any agent that can\ncall a CLI.\n\n## **What comes back** — real output from this repository, pretty-printed and trimmed (re-captured 2026-09-05; rows are served in rank order, each naming its file)\n\n```\n<ctx task=\"incremental cache invalidation\" route=\"routed: subtoken+body BM25 (--for's default) — no strong\n     name hit, multi-word conceptual query\" confidence=\"high\" margin_pct=\"20\"\n     bundle=\"compact\" bodies=\"0\" reason=\"compact-route\" est_tokens=\"3995\">\n  <sigs shown=\"23\" total=\"40\" capped=\"1\">\n    <d l=\"106\" n=\"kCacheMagic\" p=\"src/ingest_cache.h\" cx=\"0\" ccx=\"0\" in=\"0\" churn=\"11\" amp=\"71\" pure=\"1\" r=\"1\"\n       next=\"--expand=src/ingest_cache.h:kCacheMagic\"><doc>incremental cache (--cache): per-file content hash + raw facts so a re-run re-parses ONLY c…</doc>constexpr std::uint32_t kCacheMagic = 0x4b505443</d>\n    <d l=\"1307\" n=\"spanTierMemoPath\" id=\"src/ingest_astquery.h::rw::spanTierMemoPath\" p=\"src/ingest_astquery.h\" cx=\"1\" ccx=\"0\" in=\"2\" churn=\"5\" amp=\"44\" r=\"2\"><doc>Composed exactly the way every OTHER blob family is (quality.h): one fixed-width identity hex pe…</doc>inline std::string spanTierMemoPath( const std::string&amp; diskPath )</d>\n    <d l=\"247\" n=\"ingestCommitTree\" id=\"src/dmm.h::rw::dmm::ingestCommitTree\" p=\"src/dmm.h\" cx=\"6\" ccx=\"5\" in=\"1\" churn=\"6\" amp=\"27\" r=\"3\"><doc>Ingest the tree at `sha`, materialized out of `root`&apos;s object store. …</doc>inline bool ingestCommitTree( const std::string&amp; root, const std::string&amp; sha, … )</d>\n    <d l=\"841\" n=\"mcpRefreshedThisRequest\" id=\"src/mcpindex.h::rw::mcpRefreshedThisRequest\" p=\"src/mcpindex.h\" cx=\"1\" ccx=\"0\" in=\"2\" churn=\"20\" amp=\"43\" r=\"4\"><doc>P1-15 — the `_reingest` envelope field for a response whose handling ran an INCREMENTAL pass, …</doc>inline bool mcpRefreshedThisRequest( std::uint64_t passesAtEntry )</d>\n    …\n  </sigs>\n  <hops shown=\"2\" total=\"6\" capped=\"1\" noedge=\"2\">\n    <h l=\"1307\" p=\"src/ingest_astquery.h\" n=\"spanTierMemoPath\">\n      <calls total=\"3\"><c n=\"shaKeyedCachePath\" l=\"1621\"/><c n=\"headSnapRepoHex\" l=\"1359\"/><c n=\"exclConfigHex\" l=\"1554\"/></calls>\n    </h>\n    <h l=\"247\" p=\"src/dmm.h\" n=\"ingestCommitTree\">\n      <calls total=\"10\" shown=\"7\" capped=\"1\">…</calls>\n    </h>\n  </hops>\n</ctx>\n```\n\nThe cache cluster, ranked and annotated in place: `cx`/` ccx` complexity, `in` reuse count, `churn`\nrecent commits, `amp` change amplification, `tested` coverage — the fragile spots are visible\n*before* the agent touches them, in a few thousand tokens (`est_tokens=\"3995\"`, self-reported in the\nheader) instead of five whole files. This is a *conceptual* query, so the bundle is the **compact**\nshape: the ranked map plus one-hop callee edges, no inline bodies, and the root says so rather than\nleaving you to notice. Read the map, then `--expand=SYM` the one you want — or pass `--auto-bodies`\nto get bodies inline as before.\n\n## **What the quality panel shows** — real output from this repository, trimmed (legend comment elided)\n\n``` bash\n$ ripwire . --quality-panel --limit=1\n<quality_panel preset=\"default\" families=\"6\" enabled_n=\"6\" cut=\"2\" eligible=\"6497\" ranked=\"524\" …>\n<s p=\"src/graph.h:723\" n=\"buildGraph\" fam=\"4\" of=\"6\" fired=\"structural,confusion,historical,colocation\">\n<e f=\"structural\" counted=\"1\" why=\"ccx=764 loc=1368 nest=8 humps=34 deep=315 ev=98 rrank=1\"/>\n<e f=\"confusion\" counted=\"1\" why=\"atom-embedded-crement*4\"/>\n<e f=\"historical\" counted=\"1\" why=\"hrank=12 churn=36\"/>\n<e f=\"colocation\" counted=\"1\" why=\"crank=32\"/>\n</s>\n…\n</quality_panel>\n```\n\nFour of six independent evidence families corroborate on `buildGraph`, each with its own reason\nshown inline — never a single blended score. `eligible=\"6497\"` narrows to `ranked=\"524\"` (2-of-6\nagreement): an **8.1%** shortlist of this repository's own functions, not a guess (re-derived\n2026-08-23 — the corpus grew, the shortlist share did not move). Full six-family breakdown, real\nnumbers per family → [The quality panel](#the-quality-panel) below.\n\nFull retrieval tables — including the MRR figures behind the router numbers above — in\n[`bench/ANSWERQUALITY.md`](/redhat-et/ripwire/blob/main/bench/ANSWERQUALITY.md) and [Measured](#measured).\n\n[**▶ The whole tool in 29 slides**](/redhat-et/ripwire/blob/main/present/ripwire-showcase.pdf) — every figure names the instrument that pins it\n\n<sub>renders in your browser · [pptx](/redhat-et/ripwire/blob/main/present/ripwire-showcase.pptx) beside it · [the numbers behind it](/redhat-et/ripwire/blob/main/docs/EVALS.md)</sub>\n\n[What it answers](#what-it-answers) · [Quickstart](#quickstart) ·\n[The quality panel](#the-quality-panel) · [Benchmarks](#measured) ·\n[Honesty contract](#the-honesty-contract) ·\n[Agent setup](#set-it-up-in-your-coding-agent) · [Docs](#documentation)\n\nAround the core sit 179 long flags advertised in `--help`, across seven families — plus an MCP\nserver, so a coding agent can call any of them mid-task instead of grepping and reading whole files.\nNot sure which of them fits the task in front of you? `ripwire . --help-task=\"<task in words>\"`\nrecommends ONE executable command with the evidence behind the pick — advice only, it never runs\nthe recommendation — and abstains honestly when the evidence is too thin to name a winner.\n\n## Which surface is the authority — `--help` vs `docs/COMMANDS.md` — and the four reflex verbs worth memorising\n\n`./build/ripwire --help` is generated from the binary's own flag table and is always the authority;\n[`docs/COMMANDS.md`](/redhat-et/ripwire/blob/main/docs/COMMANDS.md) documents every one of the 145 documented flags — 94 of them\nwith a real invocation and its recorded output (counts re-derived 2026-08-23; `test/docscommandscheck.sh`\nfails if that documented set and the binary's own flag table ever disagree). Each family below links there.\n\nFour reflexes worth wiring into muscle memory: `--from-trace=FILE` for an error you have in hand,\n`--edit-check=SYM` right after an edit (did the contract change, and which callers are now provably\nincompatible), `--merge-scout=REF1,REF2` before landing parallel branches, and `--pack-task=\"…\"` for\nranking, bodies, callers and tests in one budgeted bundle.\n\n| Family | The question | Representative flags | \n|---|---|---|\n| [**understand a codebase cold**](/redhat-et/ripwire/blob/main/docs/COMMANDS.md#understand-a-codebase-cold) | \"What is this repo, and what matters in it?\" | `--for` ·`--help-task` ·`--tree` ·`--lego` ·`--exemplar` ·`--recall` ·`--top-k` ·`--token-budget` ·`--max-tokens` | \n| [**navigate / answer a question**](/redhat-et/ripwire/blob/main/docs/COMMANDS.md#navigate--answer-a-question) | \"Who calls this? Is it safe to change? Which tests?\" | `--callers` ·`--callees` ·`--uses` ·`--impact` ·`--path` ·`--connect` ·`--affected` ·`--situ` ·`--test-gate` ·`--grep` | \n| [**zoom the detail ladder**](/redhat-et/ripwire/blob/main/docs/COMMANDS.md#zoom-the-detail-ladder) | \"Show me more — but only where it pays.\" | `--detail` ·`--pack-signatures` ·`--outline` ·`--expand` ·`--compress` | \n| [**assess quality / structure**](/redhat-et/ripwire/blob/main/docs/COMMANDS.md#assess-quality--structure) | \"Where is the risk, and did I just add some?\" | `--quality-panel` ·`--hotspots` ·`--clones` ·`--metrics` ·`--deps` ·`--lint` ·`--quality-delta` ·`--dmm` ·`--edit-check` ·`--pr-context` ·`--merge-scout` | \n| [**self-diagnosis**](/redhat-et/ripwire/blob/main/docs/COMMANDS.md#self-diagnosis) | \"Is my setup actually working?\" | `--doctor` | \n| [**security**](/redhat-et/ripwire/blob/main/docs/COMMANDS.md#--scan-skillsdir) | \"Is this agent skill file safe to install?\" | `--scan-skill` ·`--scan-skills` | \n| [**knobs / modes**](/redhat-et/ripwire/blob/main/docs/COMMANDS.md#knobs--modes) | shape, format, cache, budget | `--json` ·`--format` ·`--mcp` | \n\n**Prebuilt binary** — macOS (arm64 / x86-64) and Linux (arm64 / x86-64, built for **RHEL 8+**;\nevery release is smoke-tested on a RHEL 9 userland before it publishes). Downloads the latest\n[GitHub Release](https://github.com/redhat-et/ripwire/releases), verifies its SHA-256, and installs\nto `~/.local/bin`. From v0.2.2 the release tarball also ships the eighteen agent skills, and the\ninstaller stages them under `~/.local/share/ripwire/skills` **and activates them for every agent it\ndetects** (Claude Code, Codex), printing one line per agent saying what it did. An agent that is not\ninstalled is never given a skills directory, hooks are never registered without an explicit `--hook`,\nand `RIPWIRE_NO_ACTIVATE=1` stages without activating for image builds. When no agent is detected the\nactivation one-liner is printed instead:\n\n```\nRIPWIRE_REPO=redhat-et/ripwire bash -c \"$(curl -fsSL https://raw.githubusercontent.com/redhat-et/ripwire/main/scripts/install.sh)\"\n```\n\n**Building it yourself needs CMake 3.24+ and a C++23 compiler, and nothing else installed first** —\nevery dependency is vendored in-tree, so the build completes with the network off.\n\n```\ngit clone https://github.com/redhat-et/ripwire.git\ncd ripwire\ncmake -S . -B build && cmake --build build -j\n./build/ripwire .          # the ranked map — start here on an unfamiliar repo\n```\n\n## Why there is no download step — vendored grammars, the offline-build proof, the languages parsed, and putting it on `PATH`\n\n**Or build from source.** Requirements: CMake 3.24+ and a C++23 compiler — that means clang 16+ /\nAppleClang 15+ (Xcode 15) / gcc 13+ / MSVC 19.36+, and if your distro's CMake is older than 3.24,\n`pip install cmake` or `brew install cmake` gets a current one everywhere. Nothing else —\ntree-sitter's core, all 21 grammars and the test framework are vendored under `third_party/deps`,\nso there is no download step and no package manager to satisfy. Prove that with the network off:\nadd `-DFETCHCONTENT_FULLY_DISCONNECTED=ON` and the build still completes.\n\nTwo builds, two jobs — pick by what you are doing:\n\n```\n# building to USE it — the fast binary (Release implies LTO; scripts/pgobuild.sh adds PGO, what CI ships)\ncmake -S . -B build-release -DCMAKE_BUILD_TYPE=Release && cmake --build build-release -j\n\n# building to WORK ON it — plain configure, no build type (why that matters: the trap, under the fold below)\ncmake -S . -B build && cmake --build build -j\n```\n\nParses **C/C++, Objective-C/C++, Python, TypeScript, JavaScript, Java, Ruby, PHP, Lua, Bash, Go,\nRust, Swift, C#** — plus JSON/TOML/YAML config keys, markdown sections, Metal, and CUDA (`<<<>>>`\nlaunches are call edges).\n\nTo put it on `PATH`, `./install.sh` builds and atomically installs the binary plus the matching\n`skills/` and `hooks/` assets into a detected prefix (Homebrew's if present, `~/.local` otherwise;\noverride with `RIPWIRE_INSTALL_PREFIX`). Set `RIPWIRE_ACTIVATE_CODEX=1` to also refresh Codex's skill\nlinks and advisory hooks from that same staged version; activation is otherwise explicit.\n\nWiring it into your agent takes one more minute — `wrap` **prints** the recipe for your client, it\nnever edits your config:\n\n```\nripwire wrap claude             # prints: claude mcp add ripwire -- ripwire --mcp\nripwire wrap --all              # detect every installed agent, print each one's recipe\nskills/install.sh --codex       # Codex CLI: the task-shaped skills that say when to query — and when to stop\n```\n\nFour commands worth learning first:\n\n```\nripwire .                                          # the ranked map — start here\nripwire . --for=\"incremental cache invalidation\"   # the task lens: what to touch, ranked\nripwire . --callers=someFunction                   # who calls it\nripwire . --test-gate                              # before you commit: which tests must run\n```\n\n## CLI or MCP, the `-DCMAKE_BUILD_TYPE=Release` trap, and the honesty contract in one line\n\nThe CLI is the recommended baseline because it works in every shell-capable agent; the MCP server is\noptional, for agents whose workflow benefits from persistent tool registration. Full walkthrough,\nall six clients: [Agent setup](#set-it-up-in-your-coding-agent).\n\n**The trap, spelled out:** never configure the *dev* tree (`build/`) with\n`-DCMAKE_BUILD_TYPE=Release`. Release defines `NDEBUG`, which compiles the degrade-path alerts out\nand blinds the gates that assert them — and every gate and bench number is measured against\n`build/`, so changing that tree's flavour silently moves all of them at once. Release belongs in\nits own tree (`build-release/` above; `./install.sh` builds its own `build-install/` the same way).\nCI builds both flavours on purpose — see [`CONTRIBUTING.md`](/redhat-et/ripwire/blob/main/CONTRIBUTING.md).\n\n**The honesty contract, in one line:** every count ripwire cannot prove is a total ships labelled a\nfloor, every truncation is disclosed where it happens, and a zero means *none found* — never *none\nexists*. Two runs over the same tree are byte-identical, and a warm run equals a cold one. That is a\ncontract, gated on every pull request and every push to main, not a tendency.\n[The full discipline — and the losses published next to the wins →](#the-honesty-contract)\n\n**Six independent evidence families, ranked by how many of them agree — never one blended score.**\nPointed at this repository's **4,956** eligible functions, 2-of-6 agreement leaves **401** worth a\nsecond look: an **8.1%** shortlist. Pooled over five corpora (n = 27,889) no two families correlate\nabove **+0.168**, which is what makes agreement corroboration rather than one metric counted twice.\nWhat each family actually looks at — on this repository's own source, not a synthetic example:\n\n| Family | Question | Backing verb | On this repo | \n|---|---|---|---|\n| **structural** | shape: complexity, size, nesting *and how much of the body is deep* , params, local-variable count — absolute bars, not a ranking | `--metrics` | `buildGraph` (`src/graph.h:462` ):`ccx=698 loc=1244 nest=8 humps=30 deep=283 locals=114` —**114 local variables invisible to every quality lens until this session** , because naming/size analysis has always stopped at a function's signature;`locals=` is a disclosed floor (`locals_floor=\"1\"` ), threaded through the same walk that already computes`ccx` /`nest` , at zero extra parsing cost | \n| **lexical** | identifier text: the 10 `naming-*` lint rules (short, wordy, case-mixed, uninformative, …) | `--lint` ,`--naming-consistency` ,`--lint --naming-locals` | see below — the one family with a fix, not just evidence | \n| **confusion** | syntactic idiom: the 7 `atom-*` rules (implicit predicates, nested ternaries, embedded`++` /`--` , …) | `--lint` | corpus-wide finding counts, not a per-function claim to spotlight here | \n| **historical** | git change frequency — `score = churn × cognitive complexity` | `--hotspots` | `src/main.cpp` :`churn=42 ccx=3387 score=142254` — top of`--hotspots` ' ranking, and its own worst function (`main` ,`ccx=387` ) is where developers keep working*and* the code is hardest. One caveat the panel's own legend states and this table must too: churn is measured**per file** , so every symbol in a file carries that file's`churn=` /`hrank=` verbatim — this family is file evidence*inherited* by the row, never the row's own history | \n| **colocation** (local reasoning) | how much you must read that isn't in front of you | `--context-ratio` | `computeQualityDelta` (`src/mcpverbs.h:2031` ), ~50 lines,**87.0%** of its distinct references resolve outside its own file — by the tokens a reader must actually read,**99.6%** . A refinement of Beck & Diehl's per-class congruence (FSE 2011); Martin's instability`I = Ce/(Ca+Ce)` is its cruder ancestor | \n| **state** (unintended side effects) | mutable state a change here can perturb, that `--impact` (who calls you) never asks about | `--nonlocal-state` | `ensure_global_init` (`src/infra/profilePmc.h:288` ) reaches 3 distinct global/static cells through its own body and callees — a tiny, innocent-looking call site can still break state three hops away. Unsound by construction (no pointer aliasing, no indirect calls), so every count is a floor | \n\n## How the six families are joined, the churn caveat, and the `nest=` profile deep-dive\n\n`--quality-panel` joins **six independent evidence families** (structural shape, lexical naming,\nsyntactic-confusion idioms, git churn history, cross-file colocation, and non-local mutable state)\nand ranks by the *count* of families agreeing, never a weighted composite — averaging correlated\nmetrics and calling it several is the Maintainability Index's well-known failure mode. On this\nrepository the panel measures **4,956** eligible functions and narrows them to **401** worth a\nsecond look at 2-of-6 agreement — an **8.1%** shortlist, not a guess — and the largest correlation\nbetween any two families, pooled across five independent corpora (n = 27,889), is **+0.168**: the\nfamilies really are measuring different things. The six-family table above shows what each finds on\nthis repository's own source; the sections below are the parts that need more than a row.\n\nThe structural family's `nest=` reports the single deepest line in a function. One line at depth 9 and\na thousand lines at depth 9 produce the same number — which means a long **blocked-sequential** body (a\nrun of shallow scoped steps, its max set by one inner loop nobody has to hold in their head) is\nindistinguishable from a **tangled** one that sustains depth for hundreds of lines. Every consumer of\n`nest=` inherited that blindness: the panel's structural family, `--readability`'s rank, the ensemble join.\n\n`--metrics` now emits the **profile** beside the max — `humps=` (how many maximal regions reach the\nnesting bar, CodeScene's \"bumpy road\": a rise above the threshold then a fall, so repeated missing\nabstractions read differently from one deep tangle) and `deep=` (how many lines lie inside them, against\nthe `loc=` already on the row). Both come from the same fused walk that already computes `ccx`/` nest`, at\nzero extra parsing cost; `deep=` is a disclosed floor (`deep_floor=\"1\"`). Both are **absent** exactly when\n`nest <` the bar — not-deep, never a hidden `0`. `deep` counts **lines** and `humps` counts **regions**,\nand two regions can share a line — a one-line `if(c){x;}else{y;}` at the bar is two regions on one line —\nso `deep` below `humps` is legal output rather than a defect. On this repository's own source:\n\n| function | `loc` | `nest` | `humps` | `deep` | deep/loc | reading | \n|---|---|---|---|---|---|---|\n| `ingest` (`src/ingest.cpp` ) | 1632 | 8 | 25 | 467 | **29%** | genuinely tangled | \n| `buildGraph` (`src/graph.h` ) | 1244 | 9 | 30 | 308 | **25%** | genuinely tangled | \n| `main` (`src/main.cpp` ) | 1061 | 6 | 29 | 111 | **10%** | long, mostly shallow steps | \n| `dispatchMcpLine` (`src/mcp.h` ) | 1099 | 7 | 22 | 102 | **9%** | a dispatch table, not a tangle | \n| `runDefaultMap` (`src/main.cpp` ) | 650 | 4 | 7 | 15 | **2%** | blocked-sequential | \n| `ur_walkTree` (`src/ingest.cpp` ) | 87 | 7 | 1 | 43 | **49%** | *small* and tangled | \n\n`loc` and `nest` alone rank `main` and `dispatchMcpLine` beside `ingest` and `buildGraph`; the profile\nseparates them, and it promotes `ur_walkTree` — 87 lines, so no size bar fires, yet proportionally the\ndensest thing in the table. **This changes no ranking**: `humps > 0` is exactly `nest >= bar`, which is\nprecisely when the `nest` bar already fired, so the family count and the panel's shortlist are untouched.\nIt is strictly more evidence on rows that already appear — a reader can tell the two shapes apart without\nopening the file.\n\nThe panel also carries **one join, and it is deliberately not a seventh family**: a row whose structural\nevidence includes `deep=` (a body that *sustains* depth at the nesting bar) and that no indexed test\nreaches is annotated `join=\"deep+untested\"` — the pair where a refactor is most wanted and least safe,\nput side by side because both facts are already on the row. It changes nothing: not `fam=`, not `of=`,\nnot the order, not which rows appear (counting it would be the structural family wearing a second hat).\nThe root reports `tested_scope=` (symbols any indexed test reaches — the join's honest denominator) and\n`deep_untested=` (rows carrying the annotation across the whole row set); at `tested_scope=\"0\"` no\nindexed test reaches *anything* here, so \"untested\" would be a fact about what was crawled rather than\nabout the code, and the annotation is emitted on **no** row.\n\n**Five lenses sit *beside* the six-family join — deliberately outside its vote.** Each is out for a\nstated reason, not an oversight:\n\n| Lens | Asks | Why beside the join | On this repo | \n|---|---|---|---|\n| `--field-affinity` | which struct fields are read together but declared far apart; each loop's access shape (index vs pointer-chase) | its subject is a **type** , not a function — attributing a struct's finding to the functions touching it is a claim the lens never makes | `MainDispatch` :**12** findings at separation cost**92.88** ;**1,374** loops classified,**5** genuine pointer-chases | \n| `cache-*` lint rules +`--with-profile` | cache-hostile access shapes (alloc-in-loop, `p=p->next` ,`a[b[i]]` , node containers, …), then which are*measured* hot | rows are facts about **sites** , joined to per-scope hardware counters — not per-function evidence a family vote could count | aggressive rules: **0** hits in shipping`src/` ;**327** findings adversarially triaged →**0** fix-worthy; the one open refactor settled by measurement (5.2 ms) | \n| `--readability` | least-readable-first ordering (Halstead volume, Posnett sigmoid) | the fitted score **saturates past 20 lines** — only the ordering is meaningful, and an ordering cannot vote in a count | ordering only, never a grade | \n| `--naming-consistency` | off-convention names, each with a computed `propose=` | the one lens that emits **advice** — a fix is not evidence, so it does not vote | camelCase dominant at **93.0%** ;**136** names flagged with proposals | \n| `--lint --naming-locals` | the naming rules pointed at local variables inside already-flagged functions | **opt-in and unvalidated** — stays outside any join until a real-corpus audit clears it (the withdrawn-rule lesson) | + **973** findings that were structurally invisible before | \n\n## The depth on each lens — citations, caveats, and the withdrawn-rule note\n\n**Two verbs sit *beside* the panel, not inside its six-family join** — worth knowing the boundary\nrather than blurring it:\n\n- **`--field-affinity`** (cache-friendly co-access) is explicitly**not** a panel family, by unit: it\nmeasures which struct fields are read together but declared far apart, and its subject is a*type* ,\nnot a function — attributing a struct's finding to the functions that touch it would be a claim the\nlens itself never makes (`docs/EVALS.md` §9.9.2).`MainDispatch` (`src/main.cpp:1335` , the 144-byte\nstruct threaded through nearly every verb) still carries**12** real findings against a separation\ncost of**92.88** — fields read together in the same call routinely cross cache-line boundaries.\nChilimbi, Davidson & Larus's cache-conscious structure definition (PLDI 1999), validated on real\nhardware counters; the advice-not-transform posture (report a split, never auto-reorder a struct)\nfollows Hundt, Mannarswamy & Chakrabarti (CGO 2006).**This is a genuinely rare kind of tool** : code\nreview catches cache-unfriendly patterns constantly, but every adjacent tool that reasons about\nmemory-access shape (Intel Advisor's pattern classifier, DMon, PerfLint) does it by*running the\nprogram first* — a two-round adversarial literature and patent search found no shipping tool and no\npublished work that does this**statically, before a line executes** (tier: RARE BUT REAL, the full\ncitation trail and hedged claim wording in[`docs/LINEAGE.md`](/redhat-et/ripwire/blob/main/docs/LINEAGE.md) ). The same verb now\nalso classifies each loop's access shape —`index` /handle-based (predictable, the hardware\nprefetcher can hide the latency) vs. pointer-chase (data-dependent, no struct layout fixes an\nunhideable per-hop stall) — and, for genuine chases, checks whether the pointer you dereference to*reach* the next node sits next to the payload you're about to read, since that cache-line fetch is\nunavoidable and colocating there is a strictly higher-value fix than generic field reordering. On\nthis repository:**1,374** loops classified,**5** genuine pointer-chases found in a codebase\ndeliberately built handle-based rather than pointer-linked (guardrail G2) — the lens staying quiet\non code written to avoid the problem is itself a check that it isn't firing at random.**Ships\nentirely report-only** : an A/B benchmark against a real 64 MB shuffled linked list measured a\nmostly-null result, so the ranking-affecting half of this feature is a provable no-op until a\nblind real-corpus validation session clears it — reported here at the same honesty level as\neverything else in this table, not oversold ahead of the evidence.\n- **The `cache-*` lint pack + `--with-profile`** (cache-friendly*access patterns* — the other half\nof the locality story, shipped 2026-08-07/08) covers what`--field-affinity` deliberately does not:\neight AST shapes practitioners agree hurt — node-based containers,`vector<T*>` /vector-of-indirect\n(the \"matrix as vector of vectors\"), heap allocation inside loops,`p = p->next` chase advances,`a[b[i]]` gather subscripts, by-value`shared_ptr` parameters, and existing manual prefetches\nflagged for re-measurement — loop-fenced by span algebra, C-family only, facts never verdicts. The\nhonesty numbers, both directions: on this repository the aggressive rules fire**only in benches\nand test fixtures, zero in shipping `src/`** (guardrail G2 holding is itself the check the rules\naren't firing at random), and a 13-agent adversarial triage of all 327 findings confirmed**zero** as fix-worthy — every plausible refactor died on \"win unmeasurable without a profile\". That gap is\nexactly what**`--lint --with-profile=FILE`** closes: it joins a` RIPWIRE_PROFILE` build's own\nper-scope hardware counters (`#PROF_TSV` ) onto findings, so a row carries`heat_total_ms` /`heat_l1d_mpki` from a real run — static shape × measured PMU weight, the two halves of SYZYGY's\nadvice mode (Hundt, CGO 2006) finally in one command. Worked example: the one surviving refactor\ncandidate (flattening the Louvain adjacency) was settled by its new`PROFILE_SCOPE` in a single\nmeasured run —**5.2 ms, 5.9% of the verb** — a wasted afternoon prevented by a number\n([`docs/CACHELINT.md`](/redhat-et/ripwire/blob/main/docs/CACHELINT.md) holds the full catalog, the wave-2 specs, and the\ncompiler-handled myths deliberately*not* checked).\n- **`--readability`** is a sibling lens, not a panel family either — the one classic model in the tree\nwith a published closed form: Halstead volume (Halstead,*Elements of Software Science* , 1977) and\nthe Posnett/Hindle/Devanbu sigmoid fit (MSR 2011,[doi:10.1145/1985441.1985454](https://doi.org/10.1145/1985441.1985454) ),\nfitted on snippets of 20 lines or fewer — past that the fitted score saturates and only the*ordering* stays meaningful, which is exactly how the verb is used: least-readable-first, never as a\ngrade. Halstead's volume specifically (not the later, less-trusted difficulty/effort derivatives) is\namong the metrics shown to track measured cognitive load directly (Peitek, Apel, Parnin, Brechmann &\nSiegmund, ICSE 2021,[doi:10.1109/ICSE43902.2021.00056](https://doi.org/10.1109/ICSE43902.2021.00056) ) —`--readability` emits volume and stops there; difficulty and effort are computed nowhere in this\ntree.\n- **`--naming-consistency`** is the*lexical* family's one exception to \"evidence, never advice\": every\nother lens in this panel tells you WHAT is wrong, never a computed fix. Case-style consistency is\nthe one property with a corpus-derivable answer — on this repository's`src/` , camelCase is the\ndominant convention at**1,677/1,803 (93.0%)** agreement, and the verb flags**136** off-convention\nnames with a mechanically recombined`propose=` value for each (no dictionary, no synonym judgment —\nsee[What it answers](#what-it-answers) ).\n- **`--lint --naming-locals`** points those same naming rules at local variable names — the thing a\nhuman reviewer flags immediately in a sprawling function and no static tool measured until this\nsession. Opt-in, off by default: on this repository, a plain`--lint` finds**2,225** findings;\nadding`--naming-locals` finds**3,198** —**973 findings that were structurally invisible** a moment ago, scoped tightly (only inside functions already flagged large/complex, only locals\nnested two blocks deep for the short-name rule) so it doesn't just relabel every loop counter in\nthe tree. Ships disabled by default on purpose: this repository's own history includes a naming\nrule that shipped on plausibility and was later measured to flag its*best* -named functions — see\nthe withdrawn-rule note below — so a rule this new stays opt-in until a real-corpus audit clears it.\n\nFull citation table, evidence tiers, and what got measured and *withdrawn* (a naming rule that\nflagged this repository's best-named functions, kept as the standing argument for measuring before\nshipping) → [`docs/LINEAGE.md`](/redhat-et/ripwire/blob/main/docs/LINEAGE.md).\n\n**Four real invocations against this repository**, each printed as the binary actually prints it:\n`--callers` (and why its count is a floor), the default ranked map (and what `amb=` admits),\n`--test-gate` (exit 4 while obligations remain), and `--from-trace` (feed it the error itself, not a\nparaphrase of one).\n\n## How these excerpts were edited — minified output wrapped for reading, and exactly which numbers are elided\n\nOutput is minified — one line, no whitespace between tags — so the excerpts below are wrapped for\nreading, and each one's leading legend comment is elided. Nothing else is edited, except that\ncorpus-size numbers (file/symbol/edge counts, the ranked-map header's token/ambiguity tallies,\nPageRank `k=` values, and the test-gate example's\n`script_gates_unmodelled=` — a count of the script runners under `test/`, recursively) drift as this repository\ngrows: **the ranked map** elides those specifically, and says so again at the point of use, and **the\ntest gate** additionally trims its `<u>` rows down to 2 of the 25 the real run prints, behind a\ntrailing `…`.\n\n## `--callers` — a call graph built on the spot, and why `count=\"6\"` ships labelled a floor\n\n**Ten seconds, no index server, no embeddings, no API key** — a parse and a call graph, built on the\nspot:\n\n``` bash\n$ ripwire . --callers=rankGraphTeleport\n<callers of=\"rankGraphTeleport\" defs=\"1\" count=\"6\" root=\".\" hop_tested=\"0\" hop_untested=\"6\" counts_floor=\"1\">\n<s t=\"fn\" n=\"runEval\" p=\"src/eval.h:169\"/>\n<s t=\"fn\" n=\"rankGraph\" p=\"src/graph.h:2581\"/>\n<s t=\"fn\" n=\"anchoredLexicalRank\" p=\"src/graph.h:3130\"/>\n<s t=\"fn\" n=\"churnRankedGraph\" p=\"src/main.cpp:983\"/>\n<s t=\"fn\" n=\"runDefaultMap\" p=\"src/main.cpp:1101\"/>\n<s t=\"fn\" n=\"getIndex\" p=\"src/mcpindex.h:1104\"/>\n</callers>\n```\n\n`counts_floor=\"1\"` is the point. Call edges are extracted from source text by name, so dynamic\ndispatch contributes no edge (a call through a function pointer or callback is an edge only when\nONE function is bound to that variable in scope and the variable never escapes — its address taken\nor reference-bound — and a macro-generated call site — tagged\n`role=\"macro\"` — only when its function-like `#define` is indexed): `count=\"6\"` is a **floor**,\nand the element says so before you read a single row.\n\n## The default ranked map — `--top-k=3`, and what `amb=\"2\"` admits about a resolver guess\n\n**The ranked map** — the default run, capped to three symbols so it fits here:\n\n``` bash\n$ ripwire . --top-k=3\n<!-- files=… symbols=… edges=… shown=3 est_tokens=… ambiguous=… unresolved=…\n     precise=… skipped_oversize=… order=important-first -->\n<r est_tokens=\"435\">\n<f p=\"./src/infra/svector.h\" layer=\"infra\">\n<s t=\"method\" n=\"size\" id=\"./src/infra/svector.h::svector::size\" k=\"…\"></s>\n<s t=\"method\" n=\"push_back\" id=\"./src/infra/svector.h::svector::push_back\" amb=\"2\" k=\"…\">\n<c n=\"buf\"/><c n=\"buf\"/><c n=\"grow\"/></s>\n</f>\n<f p=\"./src/scipoverlay.h\">\n<s t=\"method\" n=\"empty\" id=\"./src/scipoverlay.h::ScipOverlay::empty\" k=\"…\"></s>\n</f>\n</r>\n```\n\n`files=`/` symbols=`/` edges=` and the `k=` rank values are elided: this repository is the corpus here,\nso they move every time README.md itself gains or loses a line, which is not what the example\ndemonstrates. The rest of the header measures the whole corpus, not the excerpt — the `ambiguous=` tally is\nthe call-graph completeness gauge, and `amb=\"2\"` on a row says two of that symbol's calls hit a name\nwith several definitions and the resolver guessed. Read the source when which-target matters.\n\n## `--test-gate` — exit 4 while obligations remain, and why `script_gates_unmodelled=\"332\"` stays nonzero on a clean clone\n\n**The test gate** — `--test-gate` names the obligations and exits 4 while any remain. Captured with an\nuncommitted change in the tree: `changed=\"1\"` and the rows below appear only because something was\nactually pending. A clean clone exits 0 with every changed/impacted/test count at zero — except\n`script_gates_unmodelled=`, which is structural (it counts script-to-binary test runners the call\ngraph cannot see, not git status) and stays nonzero even then:\n\n``` bash\n$ ripwire . --test-gate          # exit code: 4\n<test-gate changed=\"1\" impacted=\"80\" tests=\"2\" untested=\"76\" shown_tests=\"2\" tests_capped=\"0\"\n           shown_untested=\"25\" untested_capped=\"1\" script_gates_unmodelled=\"332\" at=\"9cf0b16f3+dirty\">\n<t p=\"./test/adaptivecutshapefix/adaptive_cut_shape_test.cpp\" run=\"bash test/adaptivecutshapecheck.sh\"/>\n<t p=\"./test/verify_radix.cpp\"/>\n<u sym=\"buildGraph\" p=\"./src/graph.h\" ccx=\"712\"/>\n<u sym=\"dispatchMcpLine\" p=\"./src/mcp.h\" ccx=\"428\"/>\n…\n</test-gate>\n```\n\nA `run=` attribute appears only when a runner is derivable from real evidence — a test-dir script\nwhose stem matches the harness, or whose text names it. No `run=` means *not derivable*, never a\nguessed suite command. `script_gates_unmodelled=\"332\"` is the same discipline: script-to-binary is not\na call edge, so those gates are invisible to this walk, and the number says so rather than letting\n`tests=\"2\"` read as complete. The `<u>` rows are the untested blast radius: impacted symbols that no\ntest in the corpus reaches.\n\n## `--from-trace` — hand it the stack trace, sanitizer report or compiler error itself, not a paraphrase\n\n**From an error, not a paraphrase of one** — `--from-trace` takes a stack trace, sanitizer report or\ncompiler error on stdin or from a file, maps its frames onto indexed symbols innermost-first, and\nreturns the innermost in-corpus body with them:\n\n```\n./build/ripwire . --from-trace=asan_report.txt\ncmake --build build 2>&1 | ./build/ripwire . --from-trace=-\n```\n\n## **New (2026-08-30)** — each measured, dated, and re-derivable; the losses and caveats stated in place\n\n- **`--slice-flow=back|fwd|both` — cross-statement data-flow slicing (ARISE rung 2, arXiv:2605.03117).** A bounded BFS from one variable over line-granular reaching-definition edges — backward to the\nstatements whose values feed it, forward to the statements its value reaches — that**stops at\nfunction boundaries** , exactly as the paper's own slicer does.`--slice-depth=1..32` bounds it\n(default 8, always disclosed as`depth=` ); a bound that cuts a row says`flow_truncated=\"1\"` ;\nwithout the new flags the v1 output is byte-identical. Measured on the registered fix-commit\nprotocol over this repository's own history (**7 commits / 38 instances, cpp only — a thin corpus,\nreported as thin** ): flow rows lift function-level added-line recall**0.163 → 0.198** , at a mean\n4,993 output bytes —**25% of the 20,034-byte `--expand` whole-body baseline** whose recall is 1.0\nby construction. (`bench/slice/run_slicerecall.py` ; protocol, per-instance ledger and caveats in[`docs/EVALS.md`](/redhat-et/ripwire/blob/main/docs/EVALS.md) .)\n- **The `--slice` v1 contract got its first measured numbers** (registered 2026-08-28, no numbers\nuntil now): per-variable line-recall**0.726** , hit-all rate 0.632, over-inclusion 3.77×. The\nmisses were inspected one by one and are dominated by the measurement's own relevance oracle — a\nword-regex that matches short identifiers inside comments and string literals, occurrences the\nslicer correctly refuses to call variable uses — so 0.726 is a**floor under a noisy oracle** ; no\ninspected instance showed a real occurrence the slice dropped.\n- **`--expand` no longer hangs on minified bundles: the secret-redaction sweep was quadratic in LINE\nlength.** Invisible on ~100-byte source lines, fatal on a 2.9 KB-average-line minified bundle: one\nselector in babel's 2.1 MB / 768-line yarn bundle**never completed** (killed at 1,343.9 s of user\nCPU with an empty output file; 196.7 s is the only clean lower bound) and now answers in**0.46 s\nwarm** ; the self-contained 20 KB single-line fixture went**23.02 s → 0.017 s (~1350×)** . A pure\nmemoization, so an output no-op —**120/120 invocations byte-identical** across 24 corpora × 5 verb\nshapes — gated on behaviour only (`test/redactfixcheck.sh` ), with the timings a ledger row in[`bench/PROFILE.md`](/redhat-et/ripwire/blob/main/bench/PROFILE.md) , never a red-CI threshold.\n\n## **New (2026-08-23)** — each measured on this tree's binary, with the command that re-derives it\n\n- **`--pattern` — structural search written in CODE, not in node kinds.**`--pattern='foo($X, ...)'` where`$NAME` binds one node,`$_` binds nothing and`...` is an ellipsis over siblings; comments\nare transparent, everything else is kind- and text-exact. Served across**13 grammar objects (11\nlanguages)** , and it*refuses* rather than lying: a pattern no served grammar resolves, or that\ncollapses to a bare token, exits 1 naming the served and unserved families — never`hits=0` .\n(`./build/ripwire . --pattern='VERIFY($X)'` on this repo:**101 hits over 625 eligible files** ,\neach row carrying its enclosing symbol;`--pattern='$X'` exits**1** with the refusal text.)\n- **`--safe-delete=SYM` — \"can I delete this?\" in ONE call.** Composes the signals the tool already\ncomputes for a resolved symbol: 1-hop callers, the transitive blast radius, every read/write/\nimport/call site, how much of that radius any test reaches, and`--dead-code` 's own shape — then\nreports`risk=` as a**fact** , never a go/no-go verdict. (`--safe-delete=coversOrEquals` :`callers=\"2\" impact_reaches=\"17\" uses=\"2\" radius_tested=\"0\" radius_untested=\"17\" dead_code_candidate=\"0\" risk=\"untested-radius\"` .)\n- **`--impact` grew a disclosed import tier.** A class whose consumers`#include` or`require` the\nfile that defines it used to look like it had no blast radius at all.`importers=` is now a second,\nweaker reach beside`reaches=` — with its own cap pair and`<f via=\"import\">` rows, and**never\nsummed into `reaches=`** , because files and symbols are different units.\n(`--impact=IngestResult` on this repo:`reaches=\"0\" importers=\"70\" shown_importers=\"40\" importers_capped=\"1\"` — a symbol that reads as unreachable by calls and is included by 70 files.)\n- **Conceptual `--for` queries serve a compact map instead of inline bodies.** Bodies were**52.7% of\nevery conceptual-query byte** — the one class that missed the byte target — so the subtoken+body\nroute now ships the ranked map plus one-hop`<hops>` edge context and no body CDATA, disclosed on\nthe root as`bundle=\"compact\" bodies=\"0\" reason=\"compact-route\"` . Across the frozen 15-query class-B\nset that is**184,857 B → 95,256 B, −48.5%** , with all 11 judged-decisive markers still present\n(better than either pre-existing lever's 10/11).`--auto-bodies` is a permanent opt-out, not a\nmigration aid. (One pair on this repo, re-derived 2026-08-23:`--for=\"incremental cache invalidation\"`**8,487 B** compact vs**12,044 B** with`--auto-bodies` ,**−29.5%** ; the 15-query total is[`docs/EVALS.md` §5](/redhat-et/ripwire/blob/main/docs/EVALS.md) .)\n- **`--pack-task` orders callers by corroboration.** A neighbour reached by several of the bundle's\ntop anchors is more likely to be the thing you must touch than one reached by exactly one, so rows\nsort by`shared=` — the count of top-K anchors that reach them — omitted at 1 because that is what\nevery 1-hop row satisfies by construction. (`--pack-task=\"emit the minified xml map\"` puts`escapeXml` first at`shared=\"4\"` .)\n- **`--lint` rolls up per rule, so a capped view stops hiding whole rules.** Every rule gets a`<rule name= count= shown_rows= rows_capped=/>` row, and a rule whose registered languages match\nnothing in the corpus carries`applicable=\"0\"` so its zero reads as structural inertness rather\nthan a measurement. On this repository the default view shows 692 of 3,332 findings — and**14 of\nthe 31 rules that fired contribute zero shown rows** , 368 findings whose only evidence is their`count=` . (`./build/ripwire . --lint` , counted from the`<rule>` rows.)\n- **PHP and Lua joined the language line** , with their floors stated rather than implied: PHP's\ndynamic dispatch (`$fn()` ,`call_user_func` ,`__call` ) names its callee at run time and is a\ndeclared floor; a Lua corpus reports no inheritance edges, because metatable inheritance is a\nruntime call with no syntax to read.\n\n## Earlier — **New (2026-08-15)**, kept with its own dates rather than overwritten\n\n- **`--expand` answers \"show me this function\" in one call, −47.3% tokens** — an exact-name ask now\nskips the ranked-map preamble by default and the body arrives with its file's sibling symbols and\nimports inline (`sibs=` /`inc=` ), so the follow-up \"what else is in this file?\" call never happens.\n(Re-derived 2026-08-15 fix-expand round:`--expand=emitGrepReport` — 47,252 B classic 200-row-map\nbundle vs 24,911 B today's exact-name default, on this repo; the figure moves as the corpus grows\nand is not itself CI-gated, so re-measure before citing it in a future round.)\n- **`--grep` groups, deduplicates, and speaks boolean** — per-file grouping, identical-line collapse\n(\"this exact guard appears at 6 call sites\" is the finding), and`--and=` /`--not=` /`--grep-scope=` :\na two-term ask returns the*complete* answer at**−78%** of the single-term dump. Default view now\nruns at ~clean-grep cost while naming the enclosing symbol for every hit.\n- **C++ maps got materially truer** : out-of-line definitions past one qualifier\n(`Outer::Inner::method(){}` — the house style of large C++ codebases) were silently invisible;\non one 6,600-file production tree that was ~15% of qualified definitions. Now indexed at any depth.\n- **Compound `--graph-query` filters run ~25× faster** via predicate pushdown — an exact algebraic\nidentity, byte-identical output, gate-enforced.\n\nEvery published number lives in **[`docs/EVALS.md`](/redhat-et/ripwire/blob/main/docs/EVALS.md)** with the instrument that produced\nit, the corpus it ran on, and the in-tree file that pins it — alongside a counterexample section and a\nlist of the claims this project deliberately does *not* publish. Read those first if you are here to\ncheck whether the tool is oversold.\n\n**Round 4 (re-run in full 2026-08-08): 58.3% strict file@10 against 40.0% for the best competitor —\na 1.46× margin, at a 0.31 s index.** N = 60 paired instances, zero exclusions, one binary (the\nprofile-guided release build that now ships), one evaluator, all arms re-run on the same day.\n*Strict file@10 = all gold files inside the top 10.*\n\n| Arm | strict file@10 | any@10 | index (median) | query (median) | \n|---|---|---|---|---|\n| **ripwire `--for`** | **58.3%** | **85.0%** | **0.31 s** | 0.108 s | \n| codebase-memory-mcp 0.9.0 | 40.0% | 63.3% | 1.24 s | 0.075 s | \n| repowise 0.37.0 (MCP `search_codebase` , LLM-free wiki) | 33.3% | 53.3% | 34.0 s¹ | 1.159 s¹ | \n| graphify 0.9.34 ( `--code-only --no-cluster` , keyless) | 31.7% | 46.7% | 7.82 s | 0.614 s | \n| Aider repo-map 0.86.2 (ident-personalized) | 20.0% | 35.0% | *(inside query)* | 2.920 s | \n| Aider repo-map 0.86.2 (no-personalization control) | 10.0% | 25.0% | *(inside query)* | 0.818 s | \n| codeseek 0.1.31 (ident-mention convention arm) | 15.0% | 20.0% | 3.37 s | 0.040 s | \n| codeseek 0.1.31 (raw issue text, keyless fallback) | 0.0%² | 0.0% | 3.37 s | 0.024 s | \n\n## Round-4 method and the paired losses — one binary, one evaluator, what re-running cost us, and the three limits that travel with this table\n\n**Round 4 (first run 2026-08-06; re-run in full 2026-08-08 on the profile-guided release build) —\nN = 60 paired instances, zero exclusions, one binary, one evaluator.** Every\ncompetitor was re-run on the same day against the same ripwire binary\nand the same evaluator, so the arms are directly comparable to each other. Earlier rounds could not\nsay that: r1 (2026-07-13/14) and r2 (2026-08-03) each scored a different subset against a different\nbinary, which is why this page used to print two tables and ask the reader not to compare them.\n*Strict file@10 = **all** gold files inside the top 10.* Harness, per-instance JSONL, and a\nreproduction recipe: [`bench/headtohead/r4-2026-08-06/`](/redhat-et/ripwire/blob/main/bench/headtohead/r4-2026-08-06).\n\nPaired, ripwire's losses are **2** instances to codebase-memory-mcp, **2** to repowise, **1** each to\ngraphify and aider; codeseek never beat it. **Cold from nothing to an answer** — parse, rank, reply,\nno cache — ripwire takes **0.213 s**, against a ~34 s index-then-query for repowise, whose worst\nsingle index in this run was **352 s**. That comparison is only possible because ripwire's own index\nwas measured this round; r2 recorded competitor index walls and deliberately refused to tabulate\nthem, since a one-sided cost table is not evidence.\n\n**What re-running cost us, stated because it is the reason to re-run at all.**\n**codebase-memory-mcp is the true runner-up at 40.0%** — r1 credited it with 26.7%, and scoring it\nfairly against today's binary raised it. graphify rose 21.7% → 31.7% and aider 13.3% → 20.0% the same\nway. The margin over the best competitor is therefore **1.46×**, not the 1.75× that two\nseparately-dated tables implied; the old framing flattered us by comparing today's ripwire against\nyear-old competitor runs.\n\nThree limits travel with this table. ¹ repowise's walls include a fresh MCP-server spawn per query;\nresident-server usage is faster. ² codeseek's raw row returned **0 results on 60/60 queries** — its\nkeyless fallback matches function names only, so that row measures a query-protocol boundary, not its\nembedder-backed shipping mode (unbenchmarked here). And the slice is **Python-dominant** — 107 of 134\ngold files are `.py` — so this measures Python localization, not all twelve indexed languages.\n**Vexp and CodeIndexer were excluded, not beaten**: their free tiers (node/project/chunk caps) cannot\nrun a fair 60-instance sweep. An independent adversarial pass attacked the r2 comparison's design, and\nits findings and dispositions ship with that report\n([`VERIFIER.md`](/redhat-et/ripwire/blob/main/bench/headtohead/r2-2026-08-03/VERIFIER.md)); r1's and r2's tables remain in their own\ndirectories as the historical record.\n\n## The open problem — multi-file gold: 21.4% strict against 78.6% any@10, and four pre-registered fixes rejected at ±0.00pp\n\n**Multi-file gold is hard for every arm, including ours, and four attempts have failed to fix it.**\nripwire leads the stratum at 21.4% strict — but its own any@10 there is 78.6%: it finds *a* gold file\nand misses the siblings. If each gold file were an independent draw at the single-file rate (90.6%),\nmulti-file would score 71.2%; it scores 21.4%, so the failures are strongly correlated. The mechanism\nis that a sibling is a file which changed *because* the primary changed — the primary carries the\nissue's vocabulary, the siblings carry the consequences. Four pre-registered rounds\n(`r1_anchorhop`, `r1cpp_anchorhop`, `r4_siblift`, `r5_pooling`) have now been rejected at ±0.00pp\nagainst it, each archived with its grid and its verdict under\n[`bench/locbench/results/`](/redhat-et/ripwire/blob/main/bench/locbench/results).\n\n## Round 3 (2026-08-03) — headroom, the compression-layer competitor: 0 tokens saved on ripwire's output, and 5/12 losses published first\n\n**Round three (2026-08-03): headroom — the compression-layer competitor.** headroom\n(`headroom-ai==0.33.0`, 64k★) compresses context an agent already fetched; it retrieves nothing —\nso this round's instrument is **tokens-to-correct-answer** on 12 pre-registered mid-task questions\n(django @ pinned commit, five arms, one tokenizer), not file@k. **ripwire won every measure the two\ntools share.** headroom's default config passed every code chunk through **byte-identical** — its\nown protective guards fired throughout, netting −410 tokens on a 685,682-token workload (its own\nlimitations page says \"Code — Passthrough\"; this run confirms it live) — and stacking it on\nripwire's output added **exactly 0 tokens** of savings: the map is already past the density\ncompression targets. As measured then, ripwire answered at **7.3%** of the naive grep-and-read\nbaseline's tokens (**1.7%** on the subset both arms fully answered), warm in ~0.14 s per verb. Those\ntwo figures were re-derived on 2026-08-23 and now read **5.0%** and **5.2%** — the block below.\n**The losses in this\nround are ripwire's own, and they are published first**: under the frozen no-human verb ladders it\nstrictly satisfied only **5/12** questions vs the naive baseline's 11/12 — four ranking defects,\none missing symbol kind, two harness artifacts, each bucketed with its fix disposition in the\nreport. What the round does **not** show: headroom's home turf (JSON/log tool-output compression,\nprovider-cache economics) was deliberately not measured — ripwire does not compete there.\nProvenance: [`docs/EVALS.md` §2](/redhat-et/ripwire/blob/main/docs/EVALS.md), full record + adversarial verification (which\nmaterially corrected the draft's arithmetic in headroom's favor) in\n[`bench/headtohead/r3-headroom-2026-08-03/`](/redhat-et/ripwire/blob/main/bench/headtohead/r3-headroom-2026-08-03).\n\n## Round 3, re-derived 2026-08-23 — one headline improved, the other got worse, and both are printed\n\nThe 2026-08-03 round is frozen; this is the same instrument pointed at today's binary. Arms **A**\n(idealized grep-and-read) and **C** (ripwire's pre-registered verb ladders) were re-run from the\ncommitted `harness.py`, importing its metric functions unmodified — same `questions.json`, same\nfrozen `arms_spec.json` ladders, same `tiktoken cl100k_base` counter, same corpus pin\n(`django/django @ 70f39e46`). The headroom arms (B/B′/D) were not re-run; they contribute nothing to\nthe ripwire-vs-naive ratio, and the round's finding about them is a passthrough result that does not\ndepend on ripwire's side.\n\n**The naive arm reproduced exactly** — all 12 questions token-for-token, 685,682 t in total — which\nis what makes the comparison a measurement of ripwire's change and not of drift somewhere else.\nSatisfaction did not move either: **5/12** for ripwire, **11/12** for naive, and the both-satisfied\nset is the identical `{q02, q03, q04, q05, q11}`.\n\n|  | 2026-08-03 | 2026-08-23 |  | \n|---|---|---|---|\n| naive (arm A) total | 685,682 t | 685,682 t | reproduced to the token | \n| ripwire (arm C) total | 50,138 t | **33,948 t** | −32.3% | \n| **overall ripwire ÷ naive** | **7.3%** | **5.0%** | **better** | \n| both-satisfied, naive | 219,223 t | 219,223 t | same 5 questions | \n| both-satisfied, ripwire | 3,775 t | **11,471 t** | +204% | \n| **both-satisfied ripwire ÷ naive** | **1.7% (58×)** | **5.2% (19×)** | **worse** | \n| ripwire tokens spent on its own misses | 92.5% | 66.2% |  | \n\n**Why they moved apart.** The compact conceptual route made the seven questions ripwire *misses*\ndramatically cheaper — `q07` 13,198 → 4,416 t, `q08` 11,192 → 2,016 t, `q01` 11,974 → 3,045 t — which\nis what pulls the overall share down. On the five it *answers*, richer default bundles cost more:\n`q05` 522 → 6,736 t and `q11` 280 → 1,373 t dominate that column. The 58× figure was always the\nflattering framing of a 5-of-12 result, and the honest reading of the pair is that ripwire got much\nbetter at being wrong cheaply and somewhat more expensive at being right. Nothing about the four\nranking defects, the missing symbol kind or the two harness artifacts in the original loss buckets\nhas been re-adjudicated here — the satisfaction column is unchanged, so those losses stand.\n\n**LocBench held-out, N = 243 across 78 repositories.** Strict file@10 **60.9%**, against **27.6%** for\nthe pre-routing baseline — a paired **+33.33pp** with a clustered-bootstrap 95% lower bound of\n**+25.00pp**, bought for +3.4% warm latency and **−39.4%** on the production token ceiling. More\naccurate *and* cheaper, which is why it shipped.\n\nContext is the budget an agent actually spends. Three measurements, each with the instrument that pins it:\n\n| Where the saving comes from | Measured | Pinned by | \n|---|---|---|\n| `--pack-signatures` — body-elided declaration skeletons instead of full bodies | **80.2% fewer element bytes** at top-50 (84.5% at top-10, 80.6% at top-100) — re-derived 2026-08-30 | `test/showcasecapturecheck.sh` , re-derived from this repo every run | \n| Query-shape routing, on the production token ceiling | **−39.4%** p50, while strict file@10 rose +33.33pp | `bench/locbench/` ,[EVALS §3](/redhat-et/ripwire/blob/main/docs/EVALS.md) | \n| A whole-question bundle against a naive agent read | **96.0% fewer tokens (24.9×)** — 14,758 against 367,192, tiktoken`cl100k_base` , six realistic questions | `bench/BENCHMARK.md` —*historical, private corpus, not reproducible from this tree* | \n\n## Read this before quoting those numbers — root-neutralised bytes, the private-corpus caveat, and the two verbs where the saving inverts\n\nRead the first row's methodology before quoting it: element bytes are counted **root-neutralised**,\nwith the corpus-root prefix subtracted from both sides, because the root repeats inside every element,\nis charged in both forms, and is not what this verb elides. Quote the top-50 figure — the signature\npayload is top-50 whatever `--top-k` says, and top-10 is a ten-symbol sample that one one-line\naccessor can move several points. The gate fails if the documentation drifts more than 1.5 points from\nthe binary.\n\nAnd the third row's caveat is not small: it was measured 2026-06-20 on a large private C++ corpus, it\nis not publicly reproducible from this tree, and it proves *cheaper and faster*, not *better\noutcomes*.\n\n**The losses ship next to the wins.** `--grep`'s 2026-06 anti-headline (+19.7% / −11.2% vs raw grep)\nwas re-derived 2026-08-15 after the emission overhaul: the **un-tiered view** — `--grep-in=any`, which\nwas the default when this was measured — lands at **−1.9% / +4.3%**\nof a clean grep dump while carrying enclosing symbols and in-band honesty grep cannot give — one term\neither side of parity, which is not a headline in either direction — but an uncapped dump of that same\nun-tiered view remains token-negative (**+150% / +214%**), and this project still says so. (Span tiers\nlanded 2026-08-19 and made the *default* a filtered view of the same scan; every byte number on this\npage is the un-tiered one, deliberately — the instrument may not move under a display change — so none\nof them describes today's default. See `docs/EVALS.md` §grep.) Nor is that the worst\nregime: on *small-hit uncapped* answers the fixed per-answer legend is not amortized over anything, and\na gated set of twelve such queries on this repository runs **+65.1% median, larger than plain grep on\n12 of 12**. When the agent knows a second term, `--grep=A --and=B` flips hard positive (**−77.7%**\nmeasured, complete both sides). `--pack-signatures` inverts on a short symbol: 303 bytes of\nsignature-plus-doc-comment against a 158-byte body. The headline is a property of large result sets,\nand [the full counterexample list](#in-the-numbers) is part of the contract, not an appendix — the\nfour-fact grep re-derivation lives in [`docs/EVALS.md` §5](/redhat-et/ripwire/blob/main/docs/EVALS.md).\n\n**Every pipeline phase is bracketed by two hardware-counter reads, and the numbers say what\nwall-clock cannot.** Parse and query retire **3.2–3.5 instructions per cycle**; the graph phases\nstream at 32–35 L1D MPKI and still hold IPC above 3.1 (guardrail G2 doing its visible job); and a\nwarm run replaces the dominant phase's 8.74 B instructions with a 52.3 M-instruction cache load,\n**≈167× fewer**. Two opt-in builds go faster still — PGO by **14–25% cold** — with **byte-identical**\noutput on every one.\n\n## How the self-profiler measures — kperf / `perf_event_open`, the per-scope counter table, and the three things it says that wall-clock cannot\n\nThe self-profiler (`-DRIPWIRE_PROFILE=ON`; `src/infra/profileScope.h` + `profilePmc.h`) brackets every\npipeline phase with two hardware-counter reads — kperf on Apple Silicon, a pinned `perf_event_open`\ngroup on Linux. Below: ripwire mapping **its own public tree** on an Apple M5 Pro, cold\n(`--no-cache`) except the last row. Counters are raw integers, never scaled; reproduce steps in\n[`bench/PROFILE.md`](/redhat-et/ripwire/blob/main/bench/PROFILE.md) (arming needs root; unprivileged runs print the same table\nwith timing columns only — the honest degrade). The report renders on **stderr**; stdout stays the\ndeterministic XML map, so redirects and `| xmllint` pipes behave the same in the profile flavour.\n\n| scope | calls | instructions | IPC | L1D MPKI | wall | \n|---|---|---|---|---|---|\n| tree-sitter parse | 803 | 8.74 B | 3.52 | 4.0 | 731 ms | \n| tags query exec + captures | 803 | 8.65 B | 3.22 | 3.0 | 635 ms | \n| resolve refs + build CSR | 1 | 54.6 M | 3.27 | 35.4 | 3.6 ms | \n| PageRank (power iteration) | 1 | 13.7 M | 3.17 | 32.4 | 0.93 ms | \n| serialize ranked map | 1 | 3.24 M | 2.43 | 6.1 | 0.29 ms | \n| **warm run** — loadCache (read + deserialize) | 1 | 52.3 M | 3.18 | 7.5 | 3.6 ms | \n\nThree things the counters say that wall-clock alone cannot. The parse/query phases are\ncompute-dense, not stall-bound: 3.2–3.5 instructions retired per cycle at ~3–4 L1D misses per\nthousand. The graph phases stream hard — 32–35 L1D MPKI — yet hold IPC above 3.1, which is the\nDOD/SoA/CSR layout (G2) doing its visible job. And the auto-cache's whole story in two rows: a warm\nrun replaces the dominant phase's 8.74 B instructions with a 52.3 M-instruction cache load, ≈167×\nfewer instructions. Caveats travel with the table: counters are per-thread and aggregated per\nscope, so rows must not be summed across scopes (a recursive site samples only its outermost\nframe); one machine, one corpus — re-run on yours. Backend contract is gated by\n[`test/pmccheck.sh`](/redhat-et/ripwire/blob/main/test/pmccheck.sh); M5 Pro event names verified (the last-level alias resolves\nvia `PL2_CACHE_MISS_LD`).\n\n## The two opt-in faster builds — LTO on by default, PGO at 14–25% cold, and why the output stays byte-identical\n\n**Every number on this page is the DEFAULT build — and there is a faster one you can opt into.** A\nclang optimization-remarks pass over `src/` (`-DRIPWIRE_OPT_REMARKS=ON`; the whole triage is in\n[`docs/OPTREMARKS.md`](/redhat-et/ripwire/blob/main/docs/OPTREMARKS.md)) found that the phases above spend their time calling\ntree-sitter's C API across a translation-unit boundary — 397 of 636 distinct `inline/NoDefinition`\nremarks in the hot TU name a `ts_*` accessor. Two build options answer that, both **off by default**:\n\n| build | cold | warm |  | \n|---|---|---|---|\n| `-DRIPWIRE_LTO=OFF` | baseline | baseline | the fast edit loop, not the fast binary | \n| **default** (`cmake -S . -B build` , LTO on) | 1–6% faster | 0–3% faster |  | \n| `scripts/pgobuild.sh` (PGO on top of LTO) | **14–25% faster** (6–16% over the default) | 5–10% faster | the fastest binary this tree can produce | \n\nMeasured by interleaved A/B (`A,B,A,B,…`, median **and** min, 9–31 runs per arm, repeated) on this\nrepository *and* on a ~2000-file C++ tree that appears in no training run — the held-out corpus shows\nthe same or larger gain, which is what rules out training on the benchmark. **Output is\nbyte-identical** on all three builds and on both corpora, and the determinism gate passes on every\ntree; these options change how fast the answer arrives, never what it is. Cold gains 2–3× more than\nwarm because cold is a branchy walk over tree-sitter's parse tree, while warm already runs in the\ncache-tuned CSR/SoA/B+tree structures G2 exists to produce. **LTO is on by default** — it costs link\ntime and nothing else, and link time is not what this tool is optimized for. PGO needs a training\nrun, so it stays a driven build (`scripts/pgobuild.sh`) rather than something a bare\n`cmake --build` does behind your back.\n\n## On a vPMU-less cloud VM the columns don't vanish — software counters, a G2 witness at zero page faults, and the 2.17 s subprocess they exposed\n\n**And on machines with no PMU at all — most cloud VMs and CI boxes — the counter columns no longer\nvanish.** A kernel that refuses every hardware event (`ENOENT`; no vPMU is the common cloud case)\nstill offers software counters, so the Linux backend's per-event graceful skip now extends to the\ngroup leader and falls through to two `PERF_TYPE_SOFTWARE` rows — `task-clock` (on-CPU ns) and\n`page-faults` — under their own column names, never as a stand-in for the hardware counts. Below,\nripwire mapping its own tree, cold, on the 2-vCPU vPMU-less Intel Xeon VM this was validated live\non:\n\n| scope | calls | wall | task-clock (on-CPU) | page-faults | \n|---|---|---|---|---|\n| tree-sitter parse | 799 | 1,773 ms | 1,134 ms | 6.4 k | \n| tags query exec + captures | 799 | 1,084 ms | 1,071 ms | 6.7 k | \n| doc post-pass (main-thread wait on the pool) | 1 | 2,174 ms | 0.50 ms | 0 | \n| resolve refs + build CSR | 1 | 12.32 ms | 12.31 ms | 7 | \n\nFour things this buys that wall-clock alone cannot say. The wall−task-clock gap is *off-CPU time*:\nthe parse phase's 36% gap is 2-vCPU oversubscription made visible (a pool parsing 799 files on two\ncores). The CSR row is a live cross-check: `task-clock` is the kernel's clock, the wall column is\nthe profiler's own — two independent clocks agreeing to 0.05% on a single-threaded scope. (Short\nhot scopes diverge by the documented read-bracket overhead instead: the counter bracket encloses\nthe tick bracket.) The `page-faults` column is a **G2 witness**: PageRank's power iteration retires\nwith **zero** page faults and the CSR build with **7**, against ~3,100 in the allocation-heavy\nmodel-build scopes — the no-allocation rule inside the ranked loop, watchable on a box with no PMU.\nAnd the doc post-pass row caught something real: 2.17 s of wall on **0.5 ms** of CPU is work\nhappening *outside the process* — with `markitdown` installed, the showcase PDF and PPTX are\nre-extracted by subprocess on **every** run, cache or no cache, which on this box is ~97% of a warm\nrun's wall (2.05 s of 2.11 s); child CPU is invisible to every per-thread counter, and the\nwall-vs-task-clock gap is precisely the signature that flags it. (The extraction is already\ndocumented in-tree as a pure function of the file bytes — a cache candidate, now measured.) Only a\nkernel that offers nothing at all — `perf_event_paranoid>=3`, seccomp — still degrades to\ntiming-only, and `pmccheck`'s inactive arm now proves that was truly the case.\n\nAlmost none of the ideas here are new; the combination and the constraints are. Lessons folded from\n**41 repositories and 67 papers** into one deterministic executable, alongside a labelled\nsurvey of 237 tools that folded nothing and are catalogued separately — the two sets are disjoint,\nso they add rather than nest. The row-by-row ledger, each with the lesson taken and where it lives, is\n[`docs/LINEAGE.md`](/redhat-et/ripwire/blob/main/docs/LINEAGE.md). Those three counts are derived from that document's own tables\nby `test/readmedriftcheck.sh`, which fails if this sentence and those tables disagree.\n\nOne of those papers measures the alternative architecture directly: an independent study found a\nplanner that delegates exploration to a sub-agent in an isolated context window losing to search over\na pre-built index — 65.2% vs 46.2% accuracy, at less than half the cost — with 41.8% of the delegated\narm's failures happening silently at the planner→sub-agent hand-off\n([arXiv:2608.01507](https://arxiv.org/abs/2608.01507)). A single process answering in one call has no\nhand-off to fail at.\n\nThe differentiator is not a number, it is a discipline: **a measurement you cannot check is a claim,\nand this tool ships the check.**\n\nA name-based call graph cannot see dynamic dispatch, a callback routed through a table, or a symbol\nthat exists only after macro expansion. Every static tool has that horizon; the one that costs you a\nbug is the one that hides it. So the contract is not *it sees everything* — it is **nothing it is\nunsure about reaches your agent unlabelled.**\n\n**Measured against a compiler-grade oracle, its silent-miss count is zero.** Of 68 answers scored\nagainst a `scip-clang` index, six were imperfect and four flagged themselves; the other two were\nright, and the oracle was the one that could not see the files. No imperfect answer arrived unmarked.\n\nWhen a mark says the cheap answer is not enough, escalate on purpose — never by guessing:\n\n1. **`--expand=SYM`** — read the body it ranked, still a fraction of a whole-file read.\n2. **`--uses=SYM` / `--impact=SYM`** — every read, write and import site, and the transitive blast\nradius.`--callers` alone under-counts, and says so.\n3. **`--scip=index.scip`** — hand it a compiler-grade index and precise edges*replace* the\nname-based guesses, tagged`prov=\"scip\"` . Missing or corrupt index degrades; it never fails.\n\n## The six marks the output uses — `amb=`, `ambiguous=`, `counts_floor=`, `unresolved=`, `external=`, `--skipped`\n\nThe map for this repository's own `src/` grades itself in the header before a single answer\n(2026-08-10, at `4e245d7`): `files=109 symbols=3233 edges=10132 ambiguous=4768 unresolved=1097`.\n\n**Read `ambiguous=` as a property of the indexed tree, not a score for the resolver.** It counts call\nnames that hit more than one definition, so it moves when the *corpus* gains same-named symbols — and\nit just did, visibly. On 2026-08-09 this number rose from 2991 to 4507. The cause was attributed by\nswapping one file back and re-measuring: `src/infra/svector.h` alone accounts for 4507 → 2741. The\nsmall-vector there grew a full container interface, so the tree now defines its own `size`, `begin`,\n`end`, `push_back` and `data` — the most-called names in any C++ corpus. Adding a container is a name\ncollision by construction. Expect the number to climb again as that container replaces `std::vector`\nacross the tree; a rise there means the map is disclosing more overloading, not resolving worse.\n\n| the output says | read it as | \n|---|---|\n| `amb=\"K\"` | K of this symbol's calls hit an overloaded name — one target was chosen; read the source if which one matters | \n| `ambiguous=N` | the map's completeness gauge, one number, in the header | \n| `counts_floor=\"1\"` | a floor, not a total — a zero means *none found* , never*none exists* | \n| `unresolved=N` | call sites recognized and deliberately not resolved — counted, never dropped in silence | \n| `external=\"1\"` | no definition anywhere in the indexed tree — stdlib or third-party, not a miss | \n| `--skipped` | every file the index does not contain, itemized with its reason — `oversize` ,`excluded` ,`unsupported-ext` , a pruned vendor tree (`pruned_dirs=` , contents unknown, not zero) — plus the files it DOES contain but cannot vouch for:`degraded-parse` (tree-sitter error spans, with`err_ratio=` ) and`minified-suspect` (`ws_freq=` ) | \n\n## How the zero was measured — the sweep, the two unflagged answers, and the 22.6% it over-hedges\n\nA 34-query sweep of this repository, 68 answers across `--uses` and `--callers`, scored against a\n`scip-clang` index (2026-08-09, the shipping binary). Six answers were imperfect; four carried a\ndiscriminating self-flag (`amb=`, `defs>1`, `external=\"1\"`). The two unflagged were **not misses**:\nthe oracle could not see those files — a Python file under `bench/`, an uncompiled file under\n`test/` — and ripwire's answer was the correct one.\n\nIt errs the other way instead. **14 of 62 correct answers carried a caution flag they did not\nneed — 22.6% over-hedging**, up from 18.6% before this round's resolver fixes, because closing loss\nbuckets added marks faster than it removed them. That bias is deliberate: a flag you did not need\ncosts you one source read; a miss you were never told about costs you a bug.\n\nOne corpus, 34 queries, and the oracle has blind spots of its own — the limits that travel with the\nnumber are listed with it. Receipt, per-query scores and reproduction:\n[`bench/headtohead/r9-2026-08-09/`](/redhat-et/ripwire/blob/main/bench/headtohead/r9-2026-08-09).\n\n**Three rules the output enforces on itself:** a zero reads as *none found*, never *none exists*;\nevery truncation says what it withheld and how to page it; and every count names its unit, because\nthe units differ by verb.\n\n## The three rules in full — `counts_floor=\"1\"`, the `shown_*`/`*_capped=` paging vocabulary, and why two counts can disagree honestly\n\n- **A zero is a measurement, not an absence.**`counts_floor=\"1\"` marks every count that name-based\nresolution cannot prove is a total. Read a zero as*none found* , never*none exists* .\n- **Truncation is disclosed where it happens.**`shown_*` ,`*_capped=` and the paging attributes say\nwhat was withheld and how to page it; the legend comment that leads each document defines the\nvocabulary in full, so the output explains itself without this README.\n- **Units are named, because they differ by verb.** The callers count is distinct symbols, the impact\ncount is a reach set, the uses count is call sites. The legend says which, so two numbers that look\ncontradictory can be read as the different questions they answer.\n\nThe evaluation labels were authored by reading the source and deciding which symbol *is* the on-task\nanswer — never by transcribing the ranker's own output — so the eval is allowed to say the ranker is\nwrong, and it has. These are the results that say so, all in-tree, all published on purpose:\n\n- **`--grep` costs more tokens than it saves** , and**`--pack-signatures` can make output bigger** —\nboth quantified[next to the savings they qualify](#what-it-saves-you-in-tokens) , because saying so\nis cheaper than being caught.\n- **The public C++ number is materially lower than the retired private one was.** SFML: strict file@10\n28.7%, any@10 41.7%, first-hit MRR 0.21 — measured at commit`d411f3de4` (`bench/cppbench/results/sfml_scoreboard.md` ). Until 2026-08-07 this bullet compared against a\nroughly-89%-any@10 private corpus that is no longer reproducible from this tree; that comparison is\nretired. The public number is the baseline going forward.\n- **PageRank is a bad co-change ranker** — 3.8% recall@5 against 40.3% for plain lexical, and fusing\nthe two made it worse. Relatedness is lexical; importance is structural; the tool uses different\nmachinery for each because the measurement said so.\n- **Strict multi-file localization is hard and stays hard.** Held-out LocBench: single-file gold\n73.4%, multi-file 18.2%. Every corpus shows the same cliff.\n\n`test/regression.sh` names **547 gate scripts** and is the authoritative list;\n`python3 test/pargates.py . ./build/ripwire -j 6` runs the same set in parallel. On top of them sit the\ncontracts that do not fit a unit test: two runs byte-identical, warm output identical to cold, output\nthat pipes clean through `xmllint --noout`, a sanitizer build with `-fno-sanitize-recover=all`, and a\ndifferential argv harness that runs a reference binary and the candidate over every argument vector\nand requires stdout, stderr and exit code to match on each.\n\nThe house rule behind all of it: **write the gate before the code it measures.** A ranking, a token\nestimate and a call graph all look plausible whether or not they are correct.\n\n**If your agent can run shell commands, it is already set up.** The CLI is the primary interface:\n`ripwire` on `PATH` costs the agent nothing until the moment it runs a command — no server to\nregister, no tool schemas riding in every request's context. The one step that matters is installing\nthe skills (step 2 below), which teach the agent *when* to reach for which verb.\n\nThe MCP server is the **optional** second interface, for what a shell pipe can't give you: clients\nwithout shell access, lazy body *handles* (fetch a symbol's source only when actually needed), and\nthe same span-addressed edit operations against an already-warm index. The CLI exposes those edits too,\nwith the same staleness/ambiguity refusal and atomic-write contract:\n\n```\nripwire . --replace-symbol-body=SYM --edit-payload=definition.txt\nprintf '%s' \"$BLOCK\" | ripwire . --insert-before-symbol=SYM --edit-payload=-\n```\n\nThat MCP convenience has a cost the CLI doesn't carry — the verb schemas sit in the agent's context every session — so register it when you want those verbs, not as a default.\n\n`ripwire wrap <agent>` prints the recipe for the agent you name. It **prints**; it never edits your\nconfig — you read the line, then run it.\n\n```\nripwire wrap claude      # MCP:      claude mcp add ripwire -- ripwire --mcp\nripwire wrap cursor      # MCP:      the mcpServers stanza for .cursor/mcp.json (or ~/.cursor/mcp.json)\nripwire wrap codex       # CLI-first: optional MCP restricted to audit/health verbs in Codex TOML\nripwire wrap windsurf    # MCP:      that client's stanza\nripwire wrap gemini      # MCP:      that client's stanza\nripwire wrap opencode    # CLI-1st:  the AGENTS.md wiring; its \"mcp\" stanza offered as the alternative\nripwire wrap aider       # no MCP:   a ranked map file, and the aider invocation that reads it\nripwire wrap --all       # detect every installed agent and emit each one's config\n```\n\n**One stdio server, 31 verbs** — 16 read, 12 flagship-reflex, 3 span-addressed edit — and a client\nthat isn't one of the six above can be pointed at the same process by hand.\n\n## What the 31 verbs are — lazy body handles, the edit verbs' safety contract, the pre-print skill scan, and the hand-written stanza for any other MCP client\n\nThat registers one stdio server — `ripwire --mcp` — exposing **31 verbs**: 16 read verbs, 12\nflagship-reflex verbs, and 3 span-addressed edit verbs. Read verbs mirror the CLI (`analyze`, `for`,\n`grep`, `cochange`, `fetch_body`, `lego`, `mentions`, `owners`, `memory_recall`,\n`situational_awareness`, `batch`, …); `find_symbol` and `find_referencing_symbols` attach a stable\n`handle` instead of a body, so the agent fetches source only when it actually needs it. The edit verbs\nmirror the CLI flags and enforce the shared safety contract — staleness refusal, ambiguity refusal,\nmode preservation and atomic writes. Full reference:\n[`skills/ripwire-mcp/`](/redhat-et/ripwire/blob/main/skills/ripwire-mcp).\n\nBefore printing, `wrap` security-scans `./skills` and `.agents/skills` with the same engine as\n`--scan-skills`: a CRITICAL finding blocks the recipe, warnings print and continue.\n\n**If your client is not one of the six**, the server is a plain stdio MCP process and every client\nthat speaks MCP can be pointed at it by hand. The whole configuration is:\n\n```\n{\n  \"mcpServers\": {\n    \"ripwire\": { \"command\": \"ripwire\", \"args\": [\"--mcp\"] }\n  }\n}\n```\n\nUse an absolute path in `command` if `ripwire` is not on the agent's `PATH`. For a client that wants a\nsocket instead of stdio, `ripwire --listen=HOST:PORT` serves the same verbs.\n\n`skills/` ships **eighteen task-shaped skills** that tell an agent *which* verb answers the moment it\nis in — orienting cold, tracing a call, sizing a refactor, checking a diff, hunting a bug, writing\ntests, reviewing security. Without them an agent has 31 verbs and no map of when each applies; the skills name the moment\neach verb is for. Install as symlinks back into this repo, so edits here take effect\nimmediately:\n\n```\nskills/install.sh                 # → ~/.claude/skills\nskills/install.sh --codex         # → ${AGENTS_HOME:-~/.agents}/skills (canonical Codex/agent path)\nskills/install.sh --codex --hook  # → also install Codex's task router, CLI nudge + session primer\nskills/install.sh --codex-legacy  # → ${CODEX_HOME:-~/.codex}/skills (older Codex installs)\nskills/install.sh /some/path      # → an explicit destination\nripwire --scan-skills=skills      # read the security scanner's verdict first, if you would rather\n```\n\nThe script's own header documents its other modes, including the opt-in advisory PreToolUse hook.\n`wrap` also prints a pasteable use-when blurb for your client's rules file (`CLAUDE.md`,\n`AGENTS.md`, `.cursor/rules`, …) and works from a prebuilt install with no checkout — from v0.2.2\nit points at the installer's staged copy of the skills when the cwd is not a checkout.\n\n[`prompts/`](/redhat-et/ripwire/blob/main/prompts) holds eleven **self-contained orchestrator prompts**: the loops this project is\nbuilt with, written so a coding agent can run them. They encode the workflow rather than describing\nit.\n\n## How to run one — build first, paste the file, and it writes a plan and stops for your go-ahead before anything runs\n\nHow to run one:\n\n1. Build the tool first — most loops need a binary to measure against:\n`cmake -S . -B build && cmake --build build -j`\n2. Open your coding agent at the root of a ripwire checkout.\n3. Paste the contents of one prompt file. It needs nothing else from that directory.\n4. **It writes a plan and stops for your go-ahead.** Nothing runs before you approve it — read the\nplan, cut what you disagree with, then say go.\n5. The loop runs its own gates in the foreground and reports what it left green.\n\nThree worth starting with:\n\n| Prompt | What it produces | \n|---|---|\n| [`full-audit.md`](/redhat-et/ripwire/blob/main/prompts/full-audit.md) | A severity-ranked audit across bugs, measured performance, verb-to-moment matching, token efficiency, and an ecosystem scan of papers and repos with real momentum. | \n| [`dogfood-gaps.md`](/redhat-et/ripwire/blob/main/prompts/dogfood-gaps.md) | A real task done using only ripwire for navigation, with every fallback to grep or a whole-file read logged as a product gap at the moment it happened. | \n| [`capture-audit.md`](/redhat-et/ripwire/blob/main/prompts/capture-audit.md) | A fresh showcase capture read by parallel adversarial lenses, and the findings turned into family-wide gates. | \n\n## The other seven — head-to-head, ranking-eval from your own sessions, per-language, onboarding, sibling sweep, command tour, showcase build\n\nThe other seven — a paired head-to-head against a competitor, a ranking-eval loop that mines real\nretrieval misses from your own sessions, a per-language improvement pass, a zero-context onboarding\nstudy, a sibling sweep, a live command tour, a showcase build — are listed with their audiences in\n[`prompts/README.md`](/redhat-et/ripwire/blob/main/prompts/README.md). Each states its own scope and its honesty rules, and most name the gates they must leave green.\n\nC, C++, Objective-C / Objective-C++, **Metal** (Metal Shading Language, `.metal` — indexed with the\nC++ grammar, since MSL is a C++14 dialect, so a dual-compile header's symbols resolve from both the\nGPU and CPU halves), **CUDA** (`.cu`/`.cuh` — indexed with the vendored `tree-sitter-cuda` grammar,\nso `kernel<<<grid, block>>>( … )` launch sites are real call edges and `--callers` of a kernel names\nits host-side launchers; `__constant__` module tables index as symbols even uninitialized — the\n`cudaMemcpyToSymbol` idiom — and SCREAMING_SNAKE `__device__`/`__managed__` globals join them;\ndual-compile `.cuh` headers resolve from both halves), Python, TypeScript,\nJavaScript, Java, Ruby, **PHP** (`.php`/`.phtml` — classes, interfaces, traits, enums, functions,\nmethods and constants; `$o?->m()` null-safe and `A::m()` static calls are edges; `use` directives are\nimports. Dynamic dispatch — `$fn()`, `call_user_func`, `__call` — names its callee at run time and is\na stated floor, not a silence), **Lua** (all five spellings that define a function, including the\n`M.f = function` and table-constructor forms; `function M:f()` is a method. Metatable inheritance is\na runtime call with no syntax to read, so a Lua corpus reports no inheritance edges — stated, not\nimplied), Bash, Go, Rust, Swift, C#, JSON + TOML + YAML (config keys — a\n`[tool.ruff.lint]` table is one symbol under its full dotted name, and\n`pyproject.toml`/` Cargo.toml`/CI workflows become greppable), and **Markdown** (`.md`/`.markdown` —\nthe DOC tier: every heading, ATX or setext, is a section symbol whose span runs to the next\nsame-or-higher heading, so `--for` ranks the section, `--expand` serves the section body, `--recall`\nanswers section-granular, and links/`backtick` mentions are doc→doc and doc→code edges).\nTwenty-one tree-sitter grammars, all vendored.\n\nWant another language? The pipeline is language-agnostic past the parse: a new language is a\nvendored tree-sitter grammar, its query file, and one row in the declarative\n`extension → { grammar, queries }` table (the \"declarative constexpr tables\" rule in\n[`CONTRIBUTING.md`](/redhat-et/ripwire/blob/main/CONTRIBUTING.md)) — open an issue naming the grammar and the repo you'd run it on.\n\nNotebooks, HTML and CSV are indexed as *documents* for `--recall` and the doc↔code edges behind\n`--mentions`; Office and PDF join them through an optional bridge. Markdown graduated from that\ntier: it parses with its own vendored grammar, so its headings are symbols, not just document text.\n\n| Need | File | \n|---|---|\n| Every flag, with a real invocation and its recorded output | [`docs/COMMANDS.md`](/redhat-et/ripwire/blob/main/docs/COMMANDS.md) | \n| The authoritative flag list, always current | `./build/ripwire --help` | \n| Pipeline, data model, determinism contract, output-honesty contract | [`docs/ARCHITECTURE.md`](/redhat-et/ripwire/blob/main/docs/ARCHITECTURE.md) | \n| Every published number, its instrument, and what is *not* published | [`docs/EVALS.md`](/redhat-et/ripwire/blob/main/docs/EVALS.md) | \n| Compiler optimization remarks: the triage, and the two opt-in faster builds | [`docs/OPTREMARKS.md`](/redhat-et/ripwire/blob/main/docs/OPTREMARKS.md) | \n| The method, as something transferable | [`docs/METHODOLOGY.md`](/redhat-et/ripwire/blob/main/docs/METHODOLOGY.md) | \n| Where every idea came from, and where each one lives in the code | [`docs/LINEAGE.md`](/redhat-et/ripwire/blob/main/docs/LINEAGE.md) | \n| C++ house style, the G1–G5 guardrails, gate discipline, the submission checklist | [`CONTRIBUTING.md`](/redhat-et/ripwire/blob/main/CONTRIBUTING.md) | \n| Orientation for a coding agent working *on* this repository | [`CLAUDE.md`](/redhat-et/ripwire/blob/main/CLAUDE.md) /[` AGENTS.md`](/redhat-et/ripwire/blob/main/AGENTS.md) | \n| User-visible capabilities, behaviour changes, known limits | [`CHANGELOG.md`](/redhat-et/ripwire/blob/main/CHANGELOG.md) | \n| Vendored dependencies and their licences | [`THIRD_PARTY.md`](/redhat-et/ripwire/blob/main/THIRD_PARTY.md) | \n| The whole tool in 29 slides — the showcase deck | [`present/ripwire-showcase.pdf`](/redhat-et/ripwire/blob/main/present/ripwire-showcase.pdf) ([pptx](/redhat-et/ripwire/blob/main/present/ripwire-showcase.pptx) , rebuilt by[`present/deck5_ripwire_build.js`](/redhat-et/ripwire/blob/main/present/deck5_ripwire_build.js) ) | \n\nIf a document disagrees with `--help`, the document is the bug.\n\nContributions are welcome — read [`CONTRIBUTING.md`](/redhat-et/ripwire/blob/main/CONTRIBUTING.md) first, and\n[`CODE_OF_CONDUCT.md`](/redhat-et/ripwire/blob/main/CODE_OF_CONDUCT.md). Security reports: [`SECURITY.md`](/redhat-et/ripwire/blob/main/SECURITY.md).\n\nApache License 2.0 — see [`LICENSE`](/redhat-et/ripwire/blob/main/LICENSE) for the full text.\n\nCopyright 2026 David Brewster\n\nVendored third-party code keeps its own licence; every dependency is enumerated with its terms in\n[`THIRD_PARTY.md`](/redhat-et/ripwire/blob/main/THIRD_PARTY.md).", "url": "https://wpnews.pro/news/ripwire-ripgrep-of-ai-context-cli-mcp-giving-coding-agents-a-map-of-any-repo", "canonical_source": "https://github.com/redhat-et/ripwire", "published_at": "2026-09-07 02:11:40+00:00", "updated_at": "2026-09-07 02:59:45.716949+00:00", "lang": "en", "topics": ["developer-tools", "ai-tools", "ai-agents"], "entities": ["Red Hat", "ripwire", "Claude Code", "Codex", "Cursor", "Windsurf", "Gemini", "opencode"], "alternates": {"html": "https://wpnews.pro/news/ripwire-ripgrep-of-ai-context-cli-mcp-giving-coding-agents-a-map-of-any-repo", "markdown": "https://wpnews.pro/news/ripwire-ripgrep-of-ai-context-cli-mcp-giving-coding-agents-a-map-of-any-repo.md", "text": "https://wpnews.pro/news/ripwire-ripgrep-of-ai-context-cli-mcp-giving-coding-agents-a-map-of-any-repo.txt", "jsonld": "https://wpnews.pro/news/ripwire-ripgrep-of-ai-context-cli-mcp-giving-coding-agents-a-map-of-any-repo.jsonld"}}