Repository navigation
ripwire 0.6.9: better answers to the questions agents ask (a --path that prints the bodies it names, call chains and callers that earlier builds left out), and more hedged or declined calls instead of confident wrong ones. This release collects twelve merged batches of reviewed changes (pull requests #424 to #463). The next section shows real before and after output; section 1 is about honesty (hedged or declined calls, floors named, the bodies --path prints); section 2 is the new --defs verb and Owner.name selectors; section 3 is language specifics and callers for error-shaped --for questions. Every item is an entry in the changelog, which states what it was measured on and what it does not show; statements here are in-sample, and this page makes no comparison with any other tool.
Better answers to the questions agents ask
Each line is a question from the sets we developed ripwire against (in-sample, not held out), run with the same command on 0.6.8 and 0.6.9; sizes are the whole answer.
Eight of these nine are questions whose answer spans several files or runs through indirection: an interface or trait call, a message queue, a renamed import.
Across files: call chains, callers and the code on the route
- TypeScript (hono): how does
fetchreachcompose? 0.6.8 named the hops and printed no code (1,005 B). 0.6.9 also prints the middle hop's body, where thecompose( … )call sits, and names each method's class (Hono.#dispatch; 3,973 B). - C (htop):
ScreenManager_runtoLinuxMachine_scanCPUTime. The hedged route was named without its code; now the three bodies come with it (3,444 to 10,699 B). - Go (terraform): the call chain from
Context.PlantoGraph.walk. 0.6.8 printed no chain (22,049 B). 0.6.9 appends the six-hop chain,Context.PlanAndEval,plan,planWalk,walk,Graph.Walk,Graph.walk(24,389 B). - Rust (nushell): the call chain from
maintoeval_block_with_early_returnwhen a script file runs. 0.6.8 printed no chain (23,599 B). 0.6.9 appends the five-hop chain throughrun_file(28,310 B). - Go (cockroach): which non-test functions call
NewReplicaEvalContext? 0.6.8 named none of the four callers (21,812 B). 0.6.9 appends a callers answer that names all four, with their call lines (24,928 B).
Through indirection: dispatch, message queues, renamed imports
- Python (textual): how does
post_messagereach theon_*handlers? 0.6.8 said "not reached", with no route (3,480 B). 0.6.9 prints a route marked as a hedge (proven="0") through the message queue to_dispatch_message, with the four bodies (13,973 B). - Rust (mdbook): what does
execute_build_processcall? A call on a&dyn Rendererwas bound by name to the test stubTestRendererin the same file (render,name; 1,526 B). Now it is declined and says what could run:<dc n="render" l="207" cands="5" via="declined" gate="iface"/>, five renderers (2,000 B). - Python (cpython): who calls
re.sub? Calls writtenfrom re import sub as re_subwere not counted; nowTimeRE.patternandLocaleTime.__calc_date_timeare (73 to 75 callers).
Unused code
- Python (textual): which functions in
_segment_tools.pyhave no caller? 0.6.8 printedcount="0"(1,025 B). 0.6.9 adds<to n="align_lines">, a function only tests call (1,374 B).
On the 163 questions we developed against, same command on both versions, graded against our answer key: complete answers went from 112 to 129, partial from 38 to 31, missing from 13 to 3; total bytes per complete answer went from 16,523 to 15,099 (all answers together are 5% larger). One answer moved from complete to partial (ninja-14): 0.6.8 reached it by guessing one of three implementations by name; 0.6.9 shows the route as a hedge instead. Proving which implementation runs is planned work. These are in-sample numbers, not a prediction for other code.
Read this first
- Caches are rebuilt once. Every ingest cache and quality snapshot an earlier build wrote is refused and re-indexed on first run (parser version 259, cache version 46, quality-snapshot scheme 24).
- Scripts that read
n=: a member row on--callers,--callees,--impact,--pathand signature rows now prints its owner (Pattern.match,Builder::Buildin C, C++ and Rust,Task#executein Ruby); the spelling is one every symbol verb accepts, so a row can be pasted back. On a signature<d>row the owner folds inton=andsc=is dropped.--jsonkeys,<c>callee rows, tail rows and the map are unchanged. - A
file:LINEselector that two definitions start on now refuses and lists each candidate on stderr, instead of binding the innermost one without saying so. --dead-codeprints a new<to>row group after its<d>rows (library functions whose every indexed caller is test code); they are not incount=.--affectedadds a separate hedged group (via="class-new") after the proven rows.--situand--test-gatestart the caller walk from the changed symbols, not from every symbol of every changed file. A hunk that no function body fully holds sends its whole file back to the old behaviour.--forand--pathanswers gain elements and attributes (<callers>,<graph_verb>,<ep>,<alts>,<bodies>,slots=,mem=,ctor_l=,stem="1"on the map); each is documented in its changelog entry and in the answer's own legend. Environment switches that turn a new behaviour off are named in the entries (for exampleRIPWIRE_NO_HOP_BODY=1,RIPWIRE_NO_CTOR_HOP=1,RIPWIRE_NO_MAP_STEM=1).- Contributors: a changelog entry is now one new file under
changelog.d/, folded intoCHANGELOG.mdat release.
1. Honesty first: hedged or declined instead of confidently wrong
Each item says what changes for an agent reading the answer. These are in-sample statements: each changelog entry names the repositories and rows it was checked on and what it does not show.
- A name match is labelled as one. A by-name hedge to a free function or a JavaScript/TypeScript method now needs the caller's file to be, or import, the namesake's file; an override counts as a way in. Hedged hops (
via="name",via="name-key",via="queue",via="slot") are never counted inreaches=,reachable=orhops=, so those counts hold proven rows only. - Fewer wrong edges, stated as unresolved. A JavaScript/TypeScript call through a name written after its declaration (
Eng = Foo; new Eng()) isunresolved=, not the declared class. A Rust&dyn Traitcall never binds a same-file impl by name when the trait's implementor list is open.Widget.runno longer binds a class's method when an object literal namedWidgethas aruntoo; the spelling is refused instead.this.f.m()on a typed field never binds the caller's ownm. A Kotlin trailing-lambdalet,run,with,applyoralsostays off a Java method of the same name (#438, @llvm-x86). - A floor is named instead of a confident zero. A call through an interface-, trait- or abstract-typed receiver names the implementors that can run, and the interface carries
implementors_floor=when the list cannot be complete.--legostops listing an implementor whose own file imports its base from outside the tree.--grepsays when a literal pattern reads as a regex (pattern_meta=) instead of answeringhits="0";--whereisstops labelling comment, prose and string lineskind="def"; afile:LINEselector that two definitions start on refuses and lists each candidate instead of binding the innermost one silently. --dead-codeand--safe-deletestop over-reporting.staticcounts as internal linkage only in C, C++ and Objective-C, and Django and unittest hooks are not reported as dead code (#437, @llvm-x86; reported in #228 by @hnipps). New<to>rows list library functions only tests call, apart fromcount=.--pathprints the bodies that hold its answer. A short--pathprints the body of its middle hop (whole up to 4,000 bytes, then cut and said so). An unreached--paththat names a route prints the bodies of that route's bridge rows and readers, markedproven="0". It lists every reaching definition pair when a name has several, the other definitions of an endpoint whose pick is not provably unique (<alts>), prints first the pair whose owner you named, and says when it bound a test or fixture copy of a name (from_test=,to_test=).- Hedged hops compose.
--impactcounts who reaches a hedged caller, folds a hub in the hedged ring into one row, and--affectedadds tests that construct a reached class as a separate hedged group (via="class-new").
2. --defs and Owner.name selectors
- New verb:
--defs=NAME[,NAME…]. One call answers which definitions up to 16 names bind: each row has the owner-qualified name, kind,file:line, language, source or test, its callers (a floor), andsel=, a spelling that picks that definition alone, so the next call needs no guessing. - Rows name their owner. A member row on
--callers,--callees,--impact,--pathand signature rows printsPattern.match,Builder::Build(C, C++, Rust) orTask#execute(Ruby), and every such spelling can be pasted back as a selector. - Names resolve as the source spells them.
Class.methodin TypeScript,json.dump,batcheval.Put,ns::Outer::Inner::fandHono.#dispatchnow resolve; a JavaScript/TypeScript method row names its class; a baremodule.exports = (…) => …is a function named by its file. A bare name with several definitions is ordered the same way in every verb, and--lspsays when a name has several (#432, @mpapis).
3. Language specifics, and callers for error-shaped questions
- Rust. A receiver written as a type (
&Book,Box<T>, a path, a struct field) proves that type's method. Functions stored in struct fields are joined to the(x.f)( … )calls through them, in--callers,--calleesand--for. Files using an empty+ use<>bound no longer lose every impl owner. - Python. A call through a renamed import (
from m import f as g) reachesm.f, also when the module defines__getattr__. Annotated fields (self.f: T | None) andmod.X( … )constructors type a receiver. A posted message class names its handlers (<handler_rule>on--path,via="name-key"), and a queueputhands off to thegetreaders (via="queue").--forshows how a shown definition is registered through a manifest entry point. - Go, C and C++. A function stored in a struct field (a Go keyed element, field assignment or registrar argument; a C
.exec = cmd_split_window_exec) is joined to the dispatcher that calls it. - JavaScript and TypeScript. A method row names its class;
--useson a literal receiver no longer lists built-in member calls (#411, @lennix1337). - Error-shaped
--for. A--fortask about an error (error, exception, panic, raise, throw, fail, crash, Traceback, or an error type written as one identifier) with no function named carries the same<callers>chain the name-exact route carries, markedby="rank", for the narrow function it ranks in rows 1 to 3. - More of what a
--forquestion points at. It follows a one-line wrapper into the function it calls, names what the top method's class constructor calls (ctor_calls=), the functions a shown table initializer binds (slots=) and the members of a shown owner (mem=), lists the callers of a function it is about, and answers a task spelled as a graph question (path from A to B,callers of X) with that verb's answer, honouring--token-budget. - Contexts that changed.
--situand--test-gatestart from the changed symbols, not every symbol of every changed file (#423, @llvm-x86).
Platforms and safety
- Windows:
--quality-delta=A..Bextracts its per-commit trees through stdin, so a drive-letter path no longer fails (#402, #422, @lennix1337); clang-cl PGO builds link and keep SLP vectorization in the final binary (#410, @lennix1337). - The pattern screen that guards
--regex,--graph-query file(),--archpath rules and#match?refuses an overlapping alternation under an unbounded quantifier, one group deeper than before (#397, @pt-act). - Gates that timed out on slow runners were made cheaper, with no change to the product (macOS impact-partition and C++ localization gates, memguard and pargates arms, the Linux ASan alarm, #421). A test arm of the type-3 clone gate was renamed so the gate checks it (#431, @liziye627-design; part of the cleanup @s0undt3ch asked for in #305).
Known floors
- Every
via="name",via="name-key",via="queue"andvia="slot"row is a hedge, not a proof: it is never counted inreaches=,reachable=orhops=, and the readers or candidates it lists are a floor. - A single pathological regular-expression match remains unbounded on libstdc++, tracked as
regex-screen-multiquantifier. - Not joined: a Rust closure stored in a field, a method-call registrar (
r.set( f )) and a function value inside a macro; Go interface values (Go interfaces are structural); C typedef'd function-pointer fields copied between structs. - Kotlin: a parenthesised lambda (
s.let( { } )) is not a trailing lambda, and a Java method called from Kotlin with a trailing lambda is dropped when its name is a scope-function name. - Python: a package that only re-exports a function, or an import inside another function, keeps the earlier answer for a renamed import.
- In MCP
for, a middle-hop body the answer already prints can print a second time, because that twin has no served-body set to compare (a known divergence, named in the changelog).
Thanks
Code: @lennix1337 (#402, #410, #411, #422: Windows --quality-delta, clang-cl PGO builds, --uses on literal receivers), @llvm-x86 (#423 --situ and --test-gate from changed symbols; #437 Django and unittest hooks; #438 Kotlin scope functions), @mpapis (#432: one order for every multi-definition name answer, and --lsp says when a name has several definitions), @pt-act (#397: pattern-screen hardening) and @liziye627-design (#431: a clone-gate test arm fix). Reports that led to fixes: @hnipps (#228, Django and unittest hooks reported as dead code) and @s0undt3ch (#305, duplicated test helpers).
Versions and caches
Parser version 259, cache version 46, quality-snapshot scheme 24; the legend dictionary is dictv=05b48ed93c807ee2 entries=981. Caches written by 0.6.8 or any earlier build are rebuilt once.
Full notes: CHANGELOG.md, section 0.6.9.