diff --git a/.ripwire_quality_acks b/.ripwire_quality_acks index 81b0c7124..f62212d0f 100644 --- a/.ripwire_quality_acks +++ b/.ripwire_quality_acks @@ -89,6 +89,7 @@ ack api-surface 61a7f958d4bf7e92 4 cid=8329d88eed614818 now=4 was=3 facet=contra ack api-surface 61e5df9e1e40ff70 5 cid=e2155dd6082b880a E2 (terminality round A, lane E): +1 defaulted out-param: the receipt's ONE next= is read off the fold it renders (callers 2, incompatible 0) ack api-surface 6302e2e27e23bcde 6 cid=7202bbc7db7cc1da C1 F-06/F-07/F-10 (the listing-paging round): three listing verbs learn to disclose and page their row listings, and every gating row is that one change. api-surface 14 = ONE trailing DEFAULTED parameter each (an int pageOffset, an McpPageArgs window, a SituPageArgs, or the next= invocation a header now carries) on the emitters that must be TOLD their window — writeFlags/writeGate, writeCappedRows/writeCappedList/writeFlip/writeFlipHeader/writeFlipLights, computeFlip, nearestGateNames (which gains its cap and its TOTAL, the disclosure itself), situShowingNote/writeSituation, and the three MCP twins flagsText/flipText/situationDiffJson; additive by construction, every pre-existing call site compiles unchanged, and the alternative — a second capped emitter per verb — is the drift this repo removes rather than adds, because two emitters that disagree about a window can drop the row that IS the answer. verbosity 3 = kDocDriftLegend +20 lines and writeDocDriftPage +10 are the in-band vocabulary a reader needs to read shown_failed=/failed_capped=/failed_total= where they meet it (the rationale and the next= scan were already hoisted OUT of the body into their own function and comment, which took the complexity row to zero and the LOC row from +48 to +10); dispatchMcpLine +12 is two pagedResult wrappers on a pre-existing 1376-line dispatcher this lane adds to rather than creates. complexity 1 = the same dispatcher, +9 on a base of 518. duplication 1 = flagsText | flipText at 110 tokens, down from 131 after the shared mcpRowCap fold; the residual is getIndex + compute + captureXml, the shape EVERY index-backed MCP twin in this file has, and merging two verbs that return different results behind one entry point would be worse code than the clone. short-horizon-churn 19 = this lane's own footprint across cli.h/docdrift.h/darkflags.h/flipimpact.h/situ.h/mcp*, plus cli.h symbols three other lanes touched the same day; none foreign, none thrash. ack api-surface 6483016e50837e70 3 cid=4c53bf4b5b7d4a0c degrade disclosure: each read seam and the ref pool now take the DISCLOSE sink whose field the document reads +ack api-surface 6511634f0768c591 5 cid=0f75a00dd52bc21d now=5 was=3 facet=contract-change p=src/graphlegend.h:921 impactRollupLegend carries the importer cut and the type rollup clauses. That signature is the one upstream already ships. Local main still has the three argument form. ack api-surface 654551bf984cc299 5 cid=913136f787574436 P9 the folded edit receipt: runEditVerb gains the postCheck opt-out parameter (defaulted true, so every existing call site is source-compatible) and the line-range + post-check splice; editplan's ensureStage and receipt each gain the root parameter they need to spell one identity (M12's root-relative rule, applied to the sibling it missed) and to run the per-op post-check. The +15 LOC in runEditVerb is the copy-out-before-the-index-rebuild discipline the fold requires — every reference into ing dangles once getIndex re-ingests, and that is stated in the code. ack api-surface 68301548c0560f5d 3 cid=07b87ac0d0ae631a interface-dispatch lane (2026-09-16, test/narrowcheck.sh arms 26-36): Narrower::rule2RecvVarType takes the two inputs class identity needs — the ClassIdentity index built once in buildGraph and the chaUp base-name graph C++ lookup walks for inherited nested types (1 -> 3 parameters). Single call site (graph.h buildGraph Rule 2), updated in the same commit. Every other quality-delta row this lane produced was fixed in code rather than acked: the identity logic moved out of Narrower into IdentityNarrower (Narrower verbosity 409 -> 800 undone), nine over-complex functions split, and five clone rows removed by reusing extent::inSet with kHeadRuleLangs/kExtentClassKinds, symbolsByFileInIdOrder, and a flat include index. ack api-surface 69cd4f735374521d 5 cid=27ae3ff0233a5863 now=5 was=4 facet=contract-change p=src/editcheck.h:755 train 30 fcall-forward nit N1 (d62c00e0): ONE trailing span parameter, the graph's keptForwards, so this call-site reader applies the one model.h forwardCounts rule (--uses' own) instead of counting a forward buildGraph dropped; every caller passes g.keptForwards in the same commit; red-first in test/fcallforwardcheck.sh S1-S3. diff --git a/CHANGELOG.md b/CHANGELOG.md index 082d83aab..f8b86bb1e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,77 @@ not published here — see `docs/EVALS.md` for the instruments behind the headli ## [Unreleased] +## [0.6.8] — 2026-10-19 + +Four batches of reviewed changes in one release. What a user sees: +- `--for` stops when its answer is complete, opens one owner body for a "how does X work" question, appends the bodies of its top + ranked rows and names eight different files first. +- `--callers` and `--callees` serve up to 50 rows whole (a callers list up to 16,000 B); `--impact` of a type finds the code + that uses it; declined calls are named as `` rows; `--path` says where an unreached path stopped. +- Fewer false edges: Go receiver scope (#403), call-result typing, Rust standard members (#405), minimum arity, + `f.call()` / `f.apply()`, qualified annotations; `--safe-delete` hedges constructed-name lookups instead of `none-found`. +- Markdown: a `[text](other.md#slug)` link edges to that heading, and a link no call shares is marked `via="link"` (#414, thanks @muayyad-alsadi). +- `--quality-delta=A..B` fits a memory limit (#387); git filter and diff programs are neutralised; Clojure (#349); `--pr-context` is faster. +- Upgrade: caches rebuild once (`kParserVer` 206, `kCacheVersion` 36); re-pin Go quality baselines, acks and notes once (Go entry). +- Measured: on our fixed set of 173 questions over public repositories (10 of them from one repository sealed before any build ran), + graded two ways, a stricter method (a rubric item counts only when the answer names its primary symbol or at least two of its + listed terms) and a more lenient one (any one listed term), the questions answered completely in one call went 50, 83, 98 + (stricter) and 78, 109, 127 (more lenient) for 0.6.5, 0.6.7 and 0.6.8; on the 10 sealed questions, 2, 5, 6 (stricter). The + 0.6.8 figure was measured on the train-28 build of 0.6.8; the later changes in this release were measured only on their own + target questions (release notes). Same machine, mechanical grading. +- Thanks: @LaimaWu (#380, #382), @mpapis (#396, #400, #401, #412), @tonibergholm-codento (#387, #391, #392), @tag-und-nacht (#349), + and @muayyad-alsadi (#409, #414: markdown section links; and the idea behind the help-wanted issue #417). + +### Changed — `--for` stops when its answer is complete; an explicit `--token-budget` is a soft target + +With no `--token-budget`, a `--for` answer used to grow to a fixed length however complete it already was. It now stops when it is +complete: a 7,500 B stair-step target with a 30,000 B runaway guard that discloses itself when it fires (`over_ceiling="1"`). +An explicit `--token-budget` becomes a soft target: the fixed sections are reserved first, the `` carries page 0 of the file +page (`sym=`, `total=`, `next=`), every cut stays recoverable through `next=`, and the shown rows are a rank prefix at every +budget and root length. Rows kept past the target ride `` and a legend clause. Measured on 30 rows of our +question set: no item lost against the old budget, and 6 of 6 rows registered for it reach their missing item. + +### Changed — an explanatory `--for` question gets one owner body + +For a "how does X …" question the answer now opens ONE body chosen by structure, never by score alone: the symbol the question +names, an entry point, or the member the parser scopes to its class (C++, Rust and Python; TypeScript class members and Go +receiver methods are a named floor). The legend says it is the owner by rule, "not necessarily the subject", and that it is not +a claim to be the whole answer; with no structural evidence there is no owner body. On the 3 rows registered for it, fastify-12 +and ninja-11 reach their missing item and koa-12 does not; on 45 how-does questions the median answer grows by 702 B (14,636 to +15,338 B), and none of the 44 complete rows lost an item. + +### Changed — the startup git check reads `.git` pointer files itself + +Every run started one `git rev-parse` child (269 to 351 ms, median of 7, on four repositories) to find the git directories it +must pin. Pointer files (`.git`, `commondir`) are now read directly; any doubt, such as a NUL byte or a symlinked pointer, falls +back to asking git. The check itself takes 0.10 to 0.24 ms. Output is byte-identical on 33 argv over tmux, hono, textual and this +repository. + +### Changed — `--pr-context` builds its include-coupling table once per call (#395) + +The table depends only on the index but was rebuilt for every changed file and again on every pass of the trim ladder. +`--pr-context=HEAD~30` on this repository: 49.45 s and 45.79 s become 8.66 s and 7.67 s (two interleaved runs each, about 5.8 +times faster); the output is byte-identical on 49 argv. + +### Changed — source builds: lifetime fences on GCC 13+ and Clang (no behaviour change) + +GCC 13 and later build our own targets with `-Werror=dangling-reference` (a reference bound to a call's result that took a +temporary, as in `for( x : makeRow().items() )`, reads a dead object on GCC 13 and 14, which have no C++23 P2718 range-for +lifetime extension); `RW_LIFETIMEBOUND` marks the accessors that return a view of an argument for Clang's `-Wdangling`; +`test/danglingwarncheck.sh` plants each shape per compiler, and `portablebuildcheck` #7 holds the range-for rule. The tree had 0 +such sites on GCC 13.4, 14.4 and 16.2 and on Clang, so the fences found no bug and changed no output (33 of 33 argv +byte-identical). CONTRIBUTING.md now says: never rely on P2718, and use signed integers for quantities. + +### Added — markdown links bind the exact heading (#409 item 1, #414) + +A cross-file `[text](path/other.md#slug)` now edges to that file's heading — `--callees` / `--callers` on a doc section +list the sections it links to instead of one file-level edge. The extractor caches the link as written; `buildGraph` joins +the path against the linking file, so `../b/notes.md` never lands on a same-named file in the linker's own directory and a +link to a heading named like one of the linking file's own headings does not bind to the latter. Two headings of the same +slug give one edge (the first). A missing heading falls back to the target file's edge; percent-encoded slugs and GitHub's +`-1` duplicate suffix are not modelled and also fall back. A markdown link that no call shares is marked `via="link"` on +`--callers`, `--callees`, `--path`, `--connect`, and on a map or body callee row. `count=` and `cross_kind` stay the same. A crawl rooted at `.` stores `./docs/a.md`. The link index normalizes that path, so a root of `.` pins the same heading as an absolute root. `find_symbol` and `find_referencing_symbols` mark the link row, and the answer adds `via_link_note` when a row has it. `--json` callers and callees add that note too. `--safe-delete` and `--impact` mark a direct doc caller `via="link"`. `callers=` and `reaches=` still count it. An in-file anchor, a wikilink, and a root-relative link are `via="link"` too. The mark follows the reference, so an unpinned link does not clear a pinned one on the same pair. A link whose file is in the tree and whose slug names no heading carries `anchor="missing"`, unless the slug cannot be judged (non-ASCII, percent-encoded, a `-1` duplicate suffix of a heading that exists, a file that is one heading line, a file with a heading that holds a link or `_emphasis_`, which GitHub slugs from the rendered text): that link keeps the file edge with no mark. Columnar `` is `2` for a direct markdown link and `1` for a name-only edge. Gate: `test/mdsectioncheck.sh`. Thanks to @muayyad-alsadi (#409, #414). + ### Changed — `--for`'s first eight rows name up to eight distinct files before any file takes a second row (head round-robin) A routed (subtoken+body) `--for` answer's head was a list of the highest-scoring rows, and one file often took most of it: on @@ -57,29 +128,6 @@ answer (fold, its legend clause and the `` pair) +3.4 KB median (max 4.9 K `next="--callees=…"`, and the `--for --limit=N` file page legend says `n=` counts this task's symbols, not the file's (`--expand` `sibs_total=`). -### Fixed — closure blocks and bridges: three false hedges a hand-check found - -- `post: function _post( … ) { … }` (and `compile: function compile`) is one definition the parser reports twice; the - property is no longer listed as a closure beside the function expression, which takes its place. -- C, C++ and Objective-C have no named nested function, so a function span the parser reports inside a function (htop's - `else if (…) {` read as a function named `if`) is no longer a closure (a `langfacts.h` column, no language compare). -- A `` no longer routes through a test, bench, demo, generated or minified symbol, nor (with the declaration tier - above) a `.d.ts` / `.pyi` twin whose package ships its implementation (it may still start or end at one), and a chain - longer than 8 hops is not shown. Its namesake crossing joins definitions of the same language and kind; the full legend - now says both. -- A `` no longer crosses a declined member call spelled like a builtin type's method (`ids.join( "," )`, - `keys.push( k )`, `d.get( k )`, Ruby `each`): the builtin-method gate's per-language tables decide, and on a 42-query - hand-check every such crossing was a false chain. -- `` reads neither such a builtin-spelled declined call nor a wrapper decorator (`@property`, `@classmethod`, - `@overload`: the dead-code rule's own list) as a way into a row. -- By-name hedges (`` `why="name"`, a `` `via="namesake"` crossing) now need a call of the name that - is not spelled like a builtin type's method (`prerequisites.clear`, `self._invalid_css.update( … )`) AND a namesake - whose owner (its class in its file, or its file for a free function) owns something the forward search reached. On the - 24-row near_sink hand-check sample this removes all 5 by-name rejects and no accepted row; it is a hedge filter, not - proof that a dropped namesake is never the one called. - A row whose by-name calls either rule drops says so with `withheld="name:N"` (N callers) and never reads `why="external"`: the - index holds a way in it chose not to count (textual's `Input.render_line`, entered by `widget.render_line( y )`). - ### Added — `--path` offers a hedged bridge (``) and glob endpoints A `reachable="0"` `--path` answer (and MCP `path_between`) now carries `` when a chain @@ -129,6 +177,31 @@ lists the first row's callers. On htop, `--path=ScreenManager_run,LinuxMachine_s `Machine_scan` (the sink's only caller) and the declined call in `checkRecalculation` that could have meant it. Every `reachable="1"` answer is byte-identical; the reachable="0" hint and gap clause are unchanged. +### Added — closure blocks and bridges do not hedge on look-alikes + +Rules for the `` blocks, `` and `` entries above, found by a hand-check of their first build and shipped with them: + +- `post: function _post( … ) { … }` (and `compile: function compile`) is one definition the parser reports twice; the + property is no longer listed as a closure beside the function expression, which takes its place. +- C, C++ and Objective-C have no named nested function, so a function span the parser reports inside a function (htop's + `else if (…) {` read as a function named `if`) is no longer a closure (a `langfacts.h` column, no language compare). +- A `` no longer routes through a test, bench, demo, generated or minified symbol, nor (with the declaration tier + above) a `.d.ts` / `.pyi` twin whose package ships its implementation (it may still start or end at one), and a chain + longer than 8 hops is not shown. Its namesake crossing joins definitions of the same language and kind; the full legend + now says both. +- A `` no longer crosses a declined member call spelled like a builtin type's method (`ids.join( "," )`, + `keys.push( k )`, `d.get( k )`, Ruby `each`): the builtin-method gate's per-language tables decide, and on a 42-query + hand-check every such crossing was a false chain. +- `` reads neither such a builtin-spelled declined call nor a wrapper decorator (`@property`, `@classmethod`, + `@overload`: the dead-code rule's own list) as a way into a row. +- By-name hedges (`` `why="name"`, a `` `via="namesake"` crossing) now need a call of the name that + is not spelled like a builtin type's method (`prerequisites.clear`, `self._invalid_css.update( … )`) AND a namesake + whose owner (its class in its file, or its file for a free function) owns something the forward search reached. On the + 24-row near_sink hand-check sample this removes all 5 by-name rejects and no accepted row; it is a hedge filter, not + proof that a dropped namesake is never the one called. + A row whose by-name calls either rule drops says so with `withheld="name:N"` (N callers) and never reads `why="external"`: the + index holds a way in it chose not to count (textual's `Input.render_line`, entered by `widget.render_line( y )`). + ### Changed — Go methods carry their receiver type as scope (#403, slice 9a) Go methods now carry their receiver type as scope (`sc=`), with `*` and type arguments stripped (`*Stack[T]` → `Stack`), @@ -145,7 +218,7 @@ every definition. On fzf, `--path=Matcher::Loop,Pattern::extendedMatch` was "end Known floors: struct fields are not Go definitions, so nothing scopes to a struct; a member of an anonymous interface in a struct field scopes to the struct. Rust and Kotlin keep their own scope walkers; C++ and Python output is byte-identical. -`kParserVer` moves (198 in this release, see the versions note). +`kParserVer` moves (206 in this release, see the versions note). ### Changed — a Rust call named like a standard collection, string or Option member binds no in-tree namesake by name (#405) @@ -181,7 +254,7 @@ overload the too-many check dropped. A call with a spread, splat or C++ pack exp argument count. Fewer calls stay ambiguous, so `ambiguous=` and `graph_ambiguous=` drop. Two residual limits stay: a C/C++ argument that is a macro expanding to several arguments is counted as one, and a default written only on a declaration ripwire does not index (a `.inl`/`.ipp` header, a header outside the tree) is not seen. `kParserVer` and `kCacheVersion` move -(a definition record gains `paramsMin`; 198 and 36 in this release, see the versions note). A cached definition whose +(a definition record gains `paramsMin`; 206 and 36 in this release, see the versions note). A cached definition whose minimum exceeds its parameter count is refused and that file reparses (`test/cachefuzzcheck.sh` Part 3). Gate: `test/aritycheck.sh` §3b. ### Changed — `--impact` of a type starts from the symbols that name it @@ -271,7 +344,7 @@ it now names nothing.) Go's written `*os.File` no longer resolves to an in-tree type that happens to be called `File`. A Java/Kotlin class-name receiver binds an in-tree class only where the caller's file sees it — its package, a single-type import naming it, an on-demand import of its package (`import ext.Util; Util.open()`, `import ext.*; Util.open()`, `Util::open` no longer bind -an in-tree `Util` as proven). `kParserVer` and `kCacheVersion` move (198 and 36 in this release, see the versions note). +an in-tree `Util` as proven). `kParserVer` and `kCacheVersion` move (206 and 36 in this release, see the versions note). ### Fixed — `f.call( … )` and `f.apply( … )` on a proven function are calls of `f` `handleRequest.call( request.server, null, request, reply )` used to be recorded only as a member call named `call`, so @@ -281,7 +354,7 @@ the file declares as a function, or imports from a relative module, now stands f where the resolver proves a function or method: an imported object, const or class (`rpc.call()` on a required module object, `Klass.call( this )`) and a name bound by name alone add nothing. `f.bind( … )` is a value use of `f`, never a call. Floors, shared with the bare call `f()`: a function-local `var f` / `let f` declared after the site does not -shadow it, and a re-assignment `f = …` is not followed. `kParserVer` and `kCacheVersion` move (198 and 36 in this release, see the versions note). +shadow it, and a re-assignment `f = …` is not followed. `kParserVer` and `kCacheVersion` move (206 and 36 in this release, see the versions note). ### Fixed — a method a name-template lookup can name no longer reads `risk="none-found"` `getattr(obj, f"watch_{name}")`, `hasattr`, `cls.__dict__.get(f"on_{n}")` and JS/TS `this['compile' + node.typename]` @@ -295,7 +368,7 @@ each template's candidates (``, first 8, `next=` for the rest) `--safe-delete`, when nothing else was found, it still makes `risk="hedged"` and names its site as a `` row. A template with no literal side at all (`codes[sc + '']`, `f"{m}"`) carries no name and records nothing, like a bare `obj[k]`. Matched by name, never a proven call: `callers=`, `impact_reaches=`, `count=` and the `--dead-code` list are unchanged. The -`--json` and `--format=columnar` roots carry both counts but not the rows; the MCP transport carries neither yet. Cache format and parser version move (the reference role set grew; 36 and 198 in this release, see the versions +`--json` and `--format=columnar` roots carry both counts but not the rows; the MCP transport carries neither yet. Cache format and parser version move (the reference role set grew; 36 and 206 in this release, see the versions note). Gate: `test/safedeletecheck.sh` arms 9–18. ### Changed — `--for` tail rank rows name their file's top symbols @@ -323,7 +396,7 @@ scope only, a Rust `const` / `static` at module, `impl`, trait or `extern` level level; a call in a function-local initializer belongs to the enclosing function. Package-level grouped Go `var ( a = …; b = … )` declarations, which were missed (grouped `const` was already read), are now one symbol per name. On fzf, gotestsum and nfpm (Go), 574 function-local symbols leave the map, no package-level one is lost, and 87 grouped names are added. `kParserVer` moves -(198 in this release, see the versions note). Gate: `test/localdeclcheck.sh`. +(206 in this release, see the versions note). Gate: `test/localdeclcheck.sh`. ### Changed — `--path` hop rows name a member by its owner (`Owner.name`) @@ -350,7 +423,7 @@ method Python calls — `Num`'s in-place `__iadd__` (its own or the nearest base every path (every `return` returns `self` bare, at least one does, and the body cannot fall off its end into an implicit `return None`) or declares `-> Self` / `-> Num`. Measured: 0 call sites change on textual, hono, starlette, flake8, kysely and fastify (`--pin-census`, base vs head); the false proofs reproduce on minimal fixtures (test/receiverevidencecheck.sh arm Q). -`kParserVer` moves (the extraction path changed; 198 in this release, see the versions note). +`kParserVer` moves (the extraction path changed; 206 in this release, see the versions note). ### Changed — Ruby mixins read the shared base-relations path (#400, slice 4a) @@ -365,7 +438,7 @@ Output is byte-identical with ONE stated exception: the map, `--deps --limit=100 comes from the query, which has no such bound — so past that depth (260 nested `module`s with an `include M` innermost) the reference has no directive to join to: `--lego` lists the includer as an implementor, while the existing depth-bound notice on stderr says deeper imports were not captured. No real code nests 256 levels deep. -`kParserVer` moves (the extraction path changed; 198 in this release, see the versions note). +`kParserVer` moves (the extraction path changed; 206 in this release, see the versions note). ### Fixed — a function stored through `??`, `||`, `&&`, `or` or `and` is a value use @@ -375,7 +448,7 @@ value-reference capture walked parentheses and ternary branches but stopped at a value-preserving operators — JS/TS `??`, `||`, `&&` and the logical assignments `??=`, `||=`, `&&=`; Python `or`, `and` — in every position it already read (assignment, return, argument, parameter default). The left operand of `&&`/`and` only decides and stays no row; C/C++ and Go `||`/`&&` yield a truth value and stay no row. Parser version -198 in this release (caches re-parse once; see the versions note). Gate: `test/recallshapecheck.sh` arms OP1–OP17. +206 in this release (caches re-parse once; see the versions note). Gate: `test/recallshapecheck.sh` arms OP1–OP17. ### Added — `--grep` names what a hit's variable carries and who reads it @@ -407,7 +480,7 @@ edge, no `via="name"` hedge and no count: a `[]` or `__getitem__` definition els name. It does NOT mean every subscript of an in-tree class is found: a Ruby instance variable receiver is not typed, a compound assignment (`h[ k ] ||= v`) is recorded as the read form, and `del o[ k ]`, a multiple-assignment target and an annotation record nothing. On rake 93 callers of `[]` appear; on textual 42 of `__getitem__`. C++ `operator[]` could -share the path and is not armed. `--uses` lists no subscript site. Parser version 198 in this release (see the versions note). Gate: +share the path and is not armed. `--uses` lists no subscript site. Parser version 206 in this release (see the versions note). Gate: `test/subscriptopcheck.sh`. ### Added — `--lego=M` lists Ruby's main object when a file's top level mixes M in @@ -587,15 +660,16 @@ server, when the cgroup's charge is the larger reading: restarting the server do ### Changed — the versions this release moves, stated once -`kParserVer` 167 → 198: the Clojure grammar (#349) took 171 on `main` and slice 4a (#400) 173 (its full-use file tag 174); the value-reference walk through +`kParserVer` 167 → 206: the Clojure grammar (#349) took 171 on `main` and slice 4a (#400) 173 (its full-use file tag 174); the value-reference walk through `??`/`||`/`&&`/`or`/`and` carried 173 on its branch, the Ruby/Python subscript references and Ruby's main-object mixins 174 (file tag 175), and the Elixir quote live holes (#380) 145 on theirs; they landed together at 176 (file tag 177). The minimum-arity field (`paramsMin`) carried 178 (file tag 179) on its branch and the Go receiver scope (#403) 182 (file tag 183); those landed at 184 (file tag 185). Call-result typing carried 180 (file tag 181), the `f.call` / `f.apply` forwarders 188 (189), the function-local Go / Rust / GDScript declarations 190 (191) and the name-template references 192 (193) on their branches; those landed at 196 (file tag 197). The qualified-annotation and `x op= v` rebinding change carried 194 (195) on -its branch. This release is 198, above every number a branch build has used, so no cache such a build wrote is read as this -release's; its full-use file tag is 199. +its branch and landed with train 31 at 198 (199); branches not in this release carried 202 (203) and 204 (205). The markdown section +links (#409) carried 185 (186) on their branch and were renumbered when it merged. This release is 206, above every number a branch +build has used, so no cache such a build wrote is read as this release's; its full-use file tag is 207. `kCacheVersion` 31 → 36: a reference record's role byte gained two values, the subscript role (9) and the inert role (10, numbered 9 on #380's branch, which predates the subscript role), and the binding kind byte one (Ruby's main-object mixin) (32); a definition record gained `paramsMin`, the fewest arguments a call can pass (33 on its branch, 34 in the train that landed @@ -605,7 +679,7 @@ it); a binding record gained the call-result position and unwrap depth (33 on it the calls that keep a definition alive moved (proven subscript calls, live `unquote(...)` calls, 21), and a Rust call named like a standard-type member no longer binds an in-tree namesake (22), so the dead kind can differ for an unchanged commit. Every ingest cache written by an earlier build is refused and re-indexed once, and every -cached quality snapshot is recomputed. The session legend dictionary is `dictv=f1e98faa0aeaa1ee entries=902`. +cached quality snapshot is recomputed. The session legend dictionary is `dictv=30173b0aa2d06473 entries=908`. ## [0.6.7] — 2026-10-11 diff --git a/CMakeLists.txt b/CMakeLists.txt index ca4639bc8..fe3282dee 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,7 +1,7 @@ cmake_minimum_required(VERSION 3.24) # The version string lives in exactly ONE place — this project() call. --version (main.cpp) # reads it via the generated version.h below; test/versioncheck.sh asserts the two never drift. -project(ripwire VERSION 0.6.7 LANGUAGES C CXX) +project(ripwire VERSION 0.6.8 LANGUAGES C CXX) # ---- language standards (C++23 for our own code / C11 for the vendored C dependencies) ---- set(CMAKE_CXX_STANDARD 23) diff --git a/README.md b/README.md index c55e45cb7..cf01302a3 100644 --- a/README.md +++ b/README.md @@ -139,7 +139,7 @@ claim cannot quietly drift. The row-by-row ledger is JavaScript · Java · Ruby · PHP · Lua · Elixir · Dart · Kotlin · GDScript · Clojure · Bash · C# · JSON · TOML · YAML · Markdown — see [language support and limits](#languages). -**Latest: 0.6.7** — more complete answers, faster calls, fewer bytes. [Release notes](#release-notes) · +**Latest: 0.6.8** — more complete first answers and fewer false edges. [Release notes](#release-notes) · [the presentation](present/ripwire-showcase.pdf) · [the changelog](CHANGELOG.md) — with thanks to the contributors named there; this release is largely theirs. @@ -1058,7 +1058,7 @@ report back. The exe is not code-signed. `Expand-Archive` does not pass the down files it extracts, so SmartScreen does not prompt for an exe unpacked this way; no prompt is not a verdict. In PowerShell: ```powershell -$v = "0.6.3"; $a = "ripwire-$v-windows-x64"; $u = "https://github.com/redhat-et/ripwire/releases/download/v$v" +$v = "0.6.8"; $a = "ripwire-$v-windows-x64"; $u = "https://github.com/redhat-et/ripwire/releases/download/v$v" Invoke-WebRequest "$u/$a.zip" -OutFile "$a.zip"; Invoke-WebRequest "$u/$a.zip.sha256" -OutFile "$a.zip.sha256" # extracts only when the SHA-256 matches; on a mismatch it throws and nothing is unpacked if ((Get-FileHash "$a.zip" -Algorithm SHA256).Hash -eq (Get-Content "$a.zip.sha256").Split(" ")[0]) { Expand-Archive "$a.zip" -DestinationPath "$env:LOCALAPPDATA\Programs" } else { throw "SHA-256 mismatch: do not run $a.zip" } @@ -3092,6 +3092,8 @@ terms. ## Release notes +**ripwire 0.6.8 — more complete first answers and fewer false edges.** `--for` stops when its answer is complete, names one owner body for a "how does X work" question, appends the whole bodies of its top ranked rows and puts eight different files in its first eight rows; `--callers` and `--callees` serve up to 50 rows whole (a callers list whole while its rows fit 16,000 B); `--impact` of a type starts from the symbols that name it; and the calls the resolver declined are named as hedged rows. Fewer edges are false: Go methods carry their receiver type, a local bound to a call takes the callee's declared result type (Go, Rust, Java, Kotlin, TypeScript), Rust standard-type members no longer bind an in-tree namesake, and a method a constructed-name lookup can reach is `risk="hedged"` in `--safe-delete`, not `none-found`. On our fixed set of 173 questions, graded two ways (a stricter method and a more lenient one), the questions answered completely in one call went from 50 with 0.6.5 to 83 with 0.6.7 to 98 with 0.6.8 under the stricter method, and from 78 to 109 to 127 under the more lenient one (same machine, mechanical grading; 15 answers better and none worse since 0.6.7; on the 60 development questions the first answers are 19.9% larger). The 0.6.8 figure was measured on the train-28 build of 0.6.8; the later changes in this release were measured only on their own target questions. Clojure is a supported language (thanks @tag-und-nacht, #349). `--quality-delta=A..B` holds one commit's index at a time and answers what it can under a memory limit, and the guard counts a Linux cgroup's real charge (thanks @tonibergholm-codento, #387, #391, #392). Ruby mixins and Rails capture moved onto the shared path (thanks @mpapis, #396, #400, #401, #412), Python imports and Elixir `unquote` calls too (thanks @LaimaWu, #382, #380). A markdown section link `[text](other.md#slug)` edges to that heading, and a link no call shares is marked `via="link"` (thanks @muayyad-alsadi, #409, #414). Caches are rebuilt once on first run, and Go quality baselines, acks and notes pinned to a method need re-pinning once (see the changelog). + **ripwire 0.6.7 — more complete answers, faster calls, fewer bytes.** Each `--callers` and `--safe-delete` row now names its call sites as `file:line`, so one answer holds the lines to edit (`--callers=escapeXml` on this repository: its first answer shows 40 of 142 callers, every one with its call lines, 79 in all; 0.6.5 showed none). `--lego` lists implementors below a direct one (a two-level diff --git a/docs/COMMANDS.md b/docs/COMMANDS.md index 9524d08d4..6ea6f5eb7 100644 --- a/docs/COMMANDS.md +++ b/docs/COMMANDS.md @@ -9,7 +9,7 @@ python3 docs/docs_commands_build.py --bin build/ripwire The flag surface below is read from `ripwire --help`, so it cannot disagree with the shipped binary. `test/docscommandscheck.sh` fails if it ever does — in either direction. -Sample output is lifted from a real recorded run (`docs/captures/COMMANDS_showcase_2026-10-18.md`), trimmed to the first few lines and +Sample output is lifted from a real recorded run (`docs/captures/COMMANDS_showcase_2026-10-19.md`), trimmed to the first few lines and scrubbed of local paths. It is illustrative, not a golden: run the command yourself for the current shape. @@ -70,8 +70,8 @@ _Same map, capped to the 5 highest-ranked symbols._ $ ./build/ripwire . --top-k=5 - - + + @@ -106,7 +106,7 @@ _The same capped map without its inventory tier: --top-k=5 leaves files out, so $ ./build/ripwire . --top-k=5 --no-inventory - + @@ -140,7 +140,7 @@ _SHAPE the map to fit ~1500 tokens (binary-search top-K)._ $ ./build/ripwire . --max-tokens=1500 - + @@ -170,7 +170,7 @@ _GATE form: exit 3 if the map's own est_tokens exceeds the budget (over-budget f ``` $ ./build/ripwire . --token-budget=100 - + ``` **Shaped by:** `--top-k`, `--max-tokens`, `--for`, `--sections`, `--recall`, `--handoff`, `--pr-context`, `--from-trace` @@ -215,11 +215,11 @@ _Name-shaped query: the router picks name-exact BM25 (header says which/why)._ ``` $ ./build/ripwire . --for="rankGraphTeleport" - + - + PageRank with an explicit teleport / personalization vector p (Σp = 1). The prior is name-quality-biased through biasPrior() so all rank modes share one weighting seam; the transition matrix (edgesinline RankedGraph rankGraphTeleport( const Graph& g, const std::vector<float>&am … [line truncated: 31 more bytes on this line] #### The convergence disclosure contract ### Wave-2 adversarial verification (2026-08-19) — six probes against `aa97c9e` @@ -252,7 +252,7 @@ _Restore the / sections the --for bundle collapses to a counted s ``` $ ./build/ripwire . --for="tree-sitter parse of a source file" --sections=lego,compose - + @@ -279,11 +279,11 @@ _T3 opt-out: the signatures-only lens (no auto bodies, no bundle="auto" attribut ``` $ ./build/ripwire . --for="rankGraphTeleport" --signatures-only - + - + PageRank with an explicit teleport / personalization vector p (Σp = 1). The prior is name-quality-biased through biasPrior() so all rank modes share one weighting seam; the transition matrix (edgesinline RankedGraph rankGraphTeleport( const Graph& g, const std::vector<float>&am … [line truncated: 31 more bytes on this line] #### The convergence disclosure contract ### Wave-2 adversarial verification (2026-08-19) — six probes against `aa97c9e` @@ -314,7 +314,7 @@ _Opt OUT of compact conceptual serving: restore the rank-first auto wal ``` $ ./build/ripwire . --for="tree-sitter parse of a source file" --auto-bodies - + @@ -346,16 +346,16 @@ _Same query with routing forced OFF (plain subtoken+body BM25) — contrast with ``` $ ./build/ripwire . --for="rankGraphTeleport" --no-route - + - + uniform-teleport PageRank (the defaultinline RankedGraph rankGraph( const Graph& g, float alpha = 0.85f ) - + PageRank with an explicit teleport / personalization vector p (Σp = 1). The prior is name-quali…inline RankedGraph rankGraphTeleport( const Graph& g, const std::vector<float>& p, float alpha = 0.85f ) -inline ChurnRanking churnRankedGraph( const MainDispatch& d ) - +inline ChurnRanking churnRankedGraph( const MainDispatch& d ) + L10 (2026-09-04): the old wording claimed "the same corpus ranked by pagerank orders differently…inline constexpr const char* kChurnRankLegend = "<!-- rank_by=churn: k= is PageRank re-run with the teleport BIASED by git CHANGE-FREQUENCY over window= " "(a c… ... [20 more line(s); run it to see the whole thing] ``` @@ -378,7 +378,7 @@ _Cut the result at the relevance cliff (Adaptive-k) — on a flat ranking nothin ``` $ ./build/ripwire . --for="tree-sitter parse of a source file" --adaptive - + @@ -409,7 +409,7 @@ _Same task with the anchor disabled — the contrast the flag exists for._ ``` $ ./build/ripwire . --for="why does src/lexical.h chooseForRanker pick name-exact BM25" --no-mention-boost - @@ -439,15 +439,15 @@ _The same task with doc-mention surfacing OFF — the contrast the flag exists f ``` $ ./build/ripwire . --for="quality delta acks ledger rubber stamp" --no-doc-mention - + - + L2 stale acks in the range form. A changed-files answer holds no whole target tree to check the …std::vector<rw::quality::StaleAck> refPairStaleAcks( const gtl::btree_map<std::string, rw::quality::AckRecord>& acks, const RefPairDelta& refs ) -inline QualityDeltaOutcome computeQualityDelta( const std::string& root ) -inline const char* kAcksFile = ".ripwire_quality_acks" -inline constexpr const char* kAtStampLegend = "<!-- at= is the git commit these numbers were computed at +inline QualityDeltaOutcome computeQualityDelta( const std::string& root ) +inline const char* kAcksFile = ".ripwire_quality_acks" +inline constexpr const char* kAtStampLegend = "<!-- at= is the git commit these numbers were computed at ... [22 more line(s); run it to see the whole thing] ``` @@ -469,7 +469,7 @@ _Interface -> implementors view: every existing impl of the named interface; the $ ./build/ripwire . --lego=Vehicle - + @@ -500,7 +500,7 @@ _The repo's best-in-class instance to imitate before writing new code (picked by $ ./build/ripwire . --exemplar="format byte sizes for humans" - + f, A&&... a ) @@ -529,9 +529,9 @@ _Most relevant DOCS' full bodies (markdown only) — recall what is already writ ``` $ ./build/ripwire . --recall="quality delta gating exit codes" -ripwire recall — "quality delta gating exit codes" — 114 relevant of 221 document files, best-first — total=114 shown=8 capped=1 truncated=7 generated_demoted=3 max_tokens=8000 share_bytes=2157 est_tokens=4593 +ripwire recall — "quality delta gating exit codes" — 114 relevant of 230 document files, best-first — total=114 shown=8 capped=1 truncated=7 generated_demoted=3 max_tokens=8000 share_bytes=2157 est_tokens=4593 -━━ README.md (relevance 8.060) ━━ [sections: 2 of 25 selected (60 in doc), section-granular; whole doc 220571 B; lines="2747-2759,2760-2767"; dropped_by_budget=23] +━━ README.md (relevance 8.053) ━━ [sections: 2 of 25 selected (60 in doc), section-granular; whole doc 222723 B; lines="2747-2759,2760-2767"; dropped_by_budget=23] #### 6.2 Exit codes This is the part to wire into a script. @@ -594,25 +594,20 @@ and both are yours, because the file you write is the only thing that sets them: **Try it** -_The directory-as-knowledge-base pattern: --recall pointed at a 1375243-byte scratch dir of DUMPED TOOL OUTPUT (a git log, this repo's own generated command reference, --help text, an architecture doc, a fabricated JSON access log) instead of a source repo — no index to build, no daemon._ +_The directory-as-knowledge-base pattern: --recall pointed at a 1359757-byte scratch dir of DUMPED TOOL OUTPUT (a git log, this repo's own generated command reference, --help text, an architecture doc, a fabricated JSON access log) instead of a source repo — no index to build, no daemon._ ``` $ ./build/ripwire /aux/kbcorpus --recall="field affinity cache line data layout which fields are read together" --top-k=3 --max-tokens=1200 -ripwire recall — "field affinity cache line data layout which fields are read together" — 2 relevant of 2 document files, best-first — total=2 shown=1 capped=1 truncated=1 max_tokens=1200 share_bytes=2040 est_tokens=952 +ripwire recall — "field affinity cache line data layout which fields are read together" — 2 relevant of 2 document files, best-first — total=2 shown=1 capped=1 truncated=1 max_tokens=1200 share_bytes=2040 est_tokens=957 -━━ commands.md (relevance 10.996) ━━ [sections: 1 of 164 selected (192 in doc), section-granular; whole doc 587071 B; lines="558-583"; dropped_by_budget=163] [truncated: 1854 of 4257 bytes] -#### Pattern: a directory of dumped tool output as a knowledge base +━━ commands.md (relevance 10.114) ━━ [sections: 1 of 164 selected (192 in doc), section-granular; whole doc 589140 B; lines="3838-3842"; dropped_by_budget=163] [truncated: 1865 of 7738 bytes] +### `--field-affinity[=STRUCT]` -**Answers:** can `--recall` serve as a zero-setup knowledge base over dumped tool output — a -`git log`, an API response dump, a fetched doc, ` --help` text — sitting in a scratch -directory, instead of a source repo? +**Answers:** find fields that are read together but declared far apart — the cache-locality lens the CACHE-LOCALITY lens: which fields are READ TOGETHER but declared FAR APART. -Yes, unmodified. `--recall` never distinguishes "a codebase" from any other directory it can -walk: point it at the scratch dir and query it. No index to build, no daemon, no mutable store -between runs — the whole cost is one cold parse. Two conditions decide whether it works at all, -and both are yours, because the file you write is the only thing that sets them: +Builds a static field CO-ACCESS affinity graph (one observation per indexed C-family function body) and diffs it against the DECLARED field order and 64-byte cache-line geometry, reusing --layout's LP64 offset model. Bare = every aggregate in the repo, ranked by separation cost; =STRUCT narrows the … [line truncated: 1355 more bytes on this line] -... [17 more line(s); run it to see the whole thing] +(capped: 1 of 2 relevant document files omitted — raise --max-tokens/budget_tokens or narrow the query for 1 more (~2548-byte budget)) ``` ### `--tree` @@ -628,7 +623,7 @@ _File-by-file orientation map (top symbols per file)._ ``` $ ./build/ripwire . --tree - + @@ -732,8 +727,8 @@ _--no-stable outside --mcp: what the flag does (or says) when there is no stable $ ./build/ripwire . --no-stable --top-k=3 - - + + @@ -766,8 +761,8 @@ $ ./build/ripwire . --around=rankGraphTeleport - - + + @@ -818,18 +813,18 @@ _What SYM calls (1-hop out-edges)._ ``` $ ./build/ripwire . --callees=rankGraphTeleport - - + + - - - - - - + + + + + + ``` @@ -853,10 +848,10 @@ _The resolvable use-sites (call/read/write/import/extends) with file:line; count ``` $ ./build/ripwire . --uses=rankGraphTeleport - + - - + + @@ -887,16 +882,16 @@ _Composable node-set query: functions within 2 caller-hops of rankGraphTeleport. ``` $ ./build/ripwire . --graph-query='and(callers(name("rankGraphTeleport"),2),kind(all,fn))' - + - + - + - + @@ -924,17 +919,17 @@ _Names referenced but never defined in-corpus (stdlib/third-party surface). The ``` $ ./build/ripwire . --external-surface - - + + - - - - + + + + - + ... [17 more line(s); run it to see the whole thing] @@ -959,12 +954,12 @@ _Shortest directed call-path SRC -> DST. CHANGED: now reports from_p/to_p/from_d ``` $ ./build/ripwire . --path=main,rankGraphTeleport - + - + ``` @@ -981,10 +976,10 @@ _Minimal connecting subgraph over 3 symbols (finds shared-caller joins)._ ``` $ ./build/ripwire . --connect=rankGraphTeleport,runEval,getIndex - + - + @@ -1091,7 +1086,7 @@ _Test files that transitively reach the changed file._ ``` $ ./build/ripwire . --affected=src/graph.h - @@ -1148,7 +1143,7 @@ _Mid-task situational report for the current git diff — recorded against a CLE $ ./build/ripwire . --situ ripwire situational-awareness — 0 changed file(s), 0 symbols in them root: . -at: 2c35be9f0 +at: eb541409e (0 changed files — working tree is clean, nothing to analyze) ``` @@ -1167,18 +1162,18 @@ _The continuation packet for the NEXT session: disk truth (branch/sha ``` $ ./build/ripwire . --handoff - + - + - - - + + + + ... [1 more line(s); run it to see the whole thing] ``` @@ -1202,7 +1197,7 @@ _Pre-PR gate on a CLEAN tree: no obligations, exit 0._ ``` $ ./build/ripwire . --test-gate - ``` @@ -1227,7 +1222,7 @@ _Literal trigram-indexed search. Each hit carries its MATCHED line as the $ ./build/ripwire . --grep=DISCLOSE - + - + - + @@ -1529,7 +1524,7 @@ _Importance-weighted detail: FULL bodies for top-2, signatures for the rest._ ``` $ ./build/ripwire . --for="pagerank power iteration" --detail=2 - + @@ -1564,8 +1559,8 @@ $ ./build/ripwire . --pack-signatures --top-k=10 - - + + @@ -1595,7 +1590,7 @@ $ ./build/ripwire . --outline=rankGraphTeleport --top-k=0 signature rows. est_tokens=: price as emitted (an upper bound under compact). root=: p= relative to it. --> - + & p, float alpha = 0.85f ) { PROFILE_SCOPE_DESCRIBE( "rankGraph: PageRank (power iteration)" ); @@ -1622,11 +1617,11 @@ _NEW since the last capture: --top-k=0 means PAYLOAD-ONLY — no ranked map ride ``` $ ./build/ripwire . --top-k=0 --expand=rankGraphTeleport - + - + + - - + + @@ -1798,9 +1793,9 @@ _File->file dependency graph (god-files, cycles)._ ``` $ ./build/ripwire . --deps - - - + + + @@ -1835,17 +1830,17 @@ _Complexity x recent git churn (maintenance pain)._ $ ./build/ripwire . --hotspots - - - - - - + + + + + + - - + + ... [17 more line(s); run it to see the whole thing] ``` @@ -1863,17 +1858,17 @@ _Token-normalized duplicate bodies._ ``` $ ./build/ripwire . --clones - + - + - + ... [17 more line(s); run it to see the whole thing] @@ -1894,13 +1889,13 @@ _Per-function size ranking, largest Halstead volume/token entropy/lines first ``` $ ./build/ripwire . --biggest-first --limit=8 - - + + - + - + @@ -1927,7 +1922,7 @@ _Per function, the non-local MUTABLE state it can reach (transitively), most wri ``` $ ./build/ripwire . --nonlocal-state --limit=8 - @@ -1962,18 +1957,18 @@ _The family join: per function, which of four orthogonal evidence families fire, ``` $ ./build/ripwire . --ensemble --limit=8 - + - + - + ... [17 more line(s); run it to see the whole thing] ``` @@ -1993,17 +1988,17 @@ _THE single wide-angle quality read: six families in one pass, an eligible/ranke ``` $ ./build/ripwire . --quality-panel - + - + - - + + - + ... [17 more line(s); run it to see the whole thing] @@ -2030,13 +2025,13 @@ _The local-reasoning lens: to understand this symbol, how much must you know tha ``` $ ./build/ripwire . --context-ratio --limit=8 - - - - - - + + + + + + ... [23 more line(s); run it to see the whole thing] ``` @@ -2061,7 +2056,7 @@ _Score the naming-* rules against this repo's own rename history: proxy=old/(old ``` $ ./build/ripwire . --naming-calibration - + @@ -2096,10 +2091,10 @@ _The corpus's OWN case-convention vote per (language, kind) group; off-conventio ``` $ ./build/ripwire . --naming-consistency --limit=8 - - - - + + + + @@ -2133,7 +2128,7 @@ _The opt-in --lint modifier: naming predicates over LOCAL variable names too, C/ ``` $ ./build/ripwire . --lint --naming-locals - + @@ -2142,10 +2137,10 @@ $ ./build/ripwire . --lint --naming-locals - + - + ... [17 more line(s); run it to see the whole thing] ``` @@ -2168,7 +2163,7 @@ _Functions WITH a doc comment, most name-restating first: c_coeff (high = the co ``` $ ./build/ripwire . --comment-coherence --limit=8 - + @@ -2201,16 +2196,16 @@ _Files that change together in git (hidden coupling)._ ``` $ ./build/ripwire . --cochange - - - + + + - - - - + + + + ... [22 more line(s); run it to see the whole thing] ``` @@ -2233,16 +2228,16 @@ _Only pairs whose co-change RECURS in 2+ sub-windows of the mined window (sub_wi ``` $ ./build/ripwire . --cochange --cochange-recur=2 - - - + - + + - - + + + ... [22 more line(s); run it to see the whole thing] ``` @@ -2259,7 +2254,7 @@ _Modularity-violation GROUPS instead of pairs: "X co-changes with {A,B,C}, none ``` $ ./build/ripwire . --cochange --cochange-groups - + @@ -2288,18 +2283,18 @@ _Hotspots scoped to RECENT churn (the regression lens)._ ``` $ ./build/ripwire . --hotspots --since="2 weeks ago" - - - - - + + + + + - - + + - - - + + + ... [17 more line(s); run it to see the whole thing] ``` @@ -2326,7 +2321,7 @@ _Enforce layering rules (exit 2 on violation) — run against the repo's own tes $ ./build/ripwire . --arch=test/archfix/rules.txt - @@ -2356,7 +2351,7 @@ _Enforce layering rules (exit 2 on violation) — run against the repo's own tes $ ./build/ripwire . --arch=test/archfix/rules.txt - @@ -2382,7 +2377,7 @@ _Enforce layering rules (exit 2 on violation) — run against the repo's own tes $ ./build/ripwire . --arch=test/archfix/rules.txt - @@ -2409,7 +2404,7 @@ _Built-in AST checks (c-cast, goto, unsafe-c-fn, ...)._ ``` $ ./build/ripwire . --lint - + @@ -2418,10 +2413,10 @@ $ ./build/ripwire . --lint - + - + ... [17 more line(s); run it to see the whole thing] ``` @@ -2518,7 +2513,7 @@ _DROP two families, applied after selection; the raw select=/ignore= you passed ``` $ ./build/ripwire . --lint --lint-ignore=naming-,cache- - @@ -2527,10 +2522,10 @@ $ ./build/ripwire . --lint --lint-ignore=naming-,cache- - + - + ... [17 more line(s); run it to see the whole thing] ``` @@ -2632,15 +2627,15 @@ _Cluster the call graph into cohesive modules._ ``` $ ./build/ripwire . --communities - + - + - + @@ -2663,7 +2658,7 @@ _Drill into ONE call-graph community by id — the drill= the --communities outp ``` $ ./build/ripwire . --community=0 - ``` @@ -2688,19 +2683,19 @@ _Nested module hierarchy (multi-level Louvain) + cross-module bridges — levels ``` $ ./build/ripwire . --zoom - - + + - + - - + + - - + + ... [17 more line(s); run it to see the whole thing] ``` @@ -2725,16 +2720,16 @@ $ ./build/ripwire . --report # ripwire architecture report -3090 files · 29471 symbols · 28529 edges · 3474 modules (13221 call-graph isolated) +3102 files · 29597 symbols · 28731 edges · 3498 modules (13251 call-graph isolated) Root: `.` -Call-graph isolate provenance: 3604 declaration, 2112 header, 2755 source, 4750 document; 0 connected Louvain singletons +Call-graph isolate provenance: 3612 declaration, 2119 header, 2754 source, 4766 document; 0 connected Louvain singletons -## Modules (call-graph clusters; showing 12 of 3474) -- **src::emitTo@infra/emit.h:53:2760 [write,run,emit]** — 1488 symbols -- **src::kindIs@infra/nodekind.h:46:3271 [push,emit,collect]** — 496 symbols -- **src::DISCLOSE@infra/Diagnostics.h:616:43369 [read,compute,resolve]** — 335 symbols +## Modules (call-graph clusters; showing 12 of 3498) +- **src::emitTo@infra/emit.h:53:2760 [write,run,emit]** — 1519 symbols +- **src::kindIs@infra/nodekind.h:46:3271 [push,emit,collect]** — 497 symbols +- **src::DISCLOSE@infra/Diagnostics.h:616:43369 [read,compute,resolve]** — 333 symbols ... [17 more line(s); run it to see the whole thing] ``` @@ -2751,10 +2746,10 @@ _Cross-module call seams no test reaches. NOW carries seam_pairs/shown/capped._ ``` $ ./build/ripwire . --seams - - + + - + @@ -2782,17 +2777,17 @@ $ ./build/ripwire . --mermaid %% ripwire --mermaid: module (directory) dependency graph — node = dir (symbol count), edge = inter-module calls (>= 3). Render at mermaid.live. flowchart LR subgraph sg0 ["src"] - n101["src
8841"] - n102["src/infra
1294"] + n101["src
8908"] + n102["src/infra
1297"] end subgraph sg1 ["test"] - n103["test
5405"] + n103["test
5414"] n483["test/legochargefix
546"] n394["test/fixtures/recallpassage
314"] - n564["test/ownerhopfix/py/pkg
158"] + n568["test/ownerhopfix/py/pkg
158"] n338["test/expandmodefix
151"] n340["test/expandsibsfix
149"] - n705["test/rubyschemafix
99"] + n709["test/rubyschemafix
99"] ... [17 more line(s); run it to see the whole thing] ``` @@ -2811,17 +2806,17 @@ _Bus-factor: recency-weighted author ownership per file._ ``` $ ./build/ripwire . --owners - - + + - + - + ... [17 more line(s); run it to see the whole thing] @@ -2842,7 +2837,7 @@ _Internal functions with no caller found in the index — a name-based graph rea ``` $ ./build/ripwire . --dead-code=src - + ``` @@ -2907,7 +2902,7 @@ _The same clean verdict with what it folded printed back: every stale acknowledg ``` $ ./build/ripwire . --quality-delta --show-stale - @@ -2936,7 +2931,7 @@ _On a CLEAN tree: nothing got worse, exit 0. The gating shape is in the sandbox ``` $ ./build/ripwire . --quality-delta - ``` @@ -2961,7 +2956,7 @@ _On a CLEAN tree: nothing got worse, exit 0. The gating shape is in the sandbox ``` $ ./build/ripwire . --quality-delta - ``` @@ -2986,7 +2981,7 @@ _A ref range run for two of the twelve kinds only: the named kinds' rows are exa ``` $ ./build/ripwire . --quality-delta=HEAD~1..HEAD --quality-kinds=complexity,dead-code -

@@ -3079,7 +3074,7 @@ _OWNERSHIP partition for a shared tree: every regression here lives in src/infra ``` $ ./build/ripwire . --quality-delta --scope=src/graph.h - @@ -3109,10 +3104,10 @@ _Fast per-symbol post-edit contract check vs git HEAD (unchanged on a clean tree ``` $ ./build/ripwire . --edit-check=rankGraphTeleport - - - + + @@ -3163,12 +3158,12 @@ $ ./build/ripwire . --insert-before-symbol=nonNegativeFloatDescKey --edit-payloa {"n":"benchAdaptive","p":"bench/bench_radix_ab.cpp:157","l":[162]}, {"n":"radixSortNonNegativeFloatsDesc","p":"src/infra/sortutil.h:105","l":[114]}, {"n":"radixSortByScoreDescId","p":"src/infra/sortutil.h:120","l":[180]}], -"graph_ambiguous":2904,"graph_unresolved":14037,"graph_unindexed":266,"counts_floor":true},"tests_to_run":[{"p":"test/verify_radix.cpp","hops":1,"run":"bash test/greptiercheck.sh"}, +"graph_ambiguous":2911,"graph_unresolved":14064,"graph_unindexed":266,"counts_floor":true},"tests_to_run":[{"p":"test/verify_radix.cpp","hops":1,"run":"bash test/greptiercheck.sh"}, {"p":"test/adaptivecutshapefix/adaptive_cut_shape_test.cpp","hops":2,"run":"bash test/adaptivecutshapecheck.sh"}, {"p":"test/includeprecise_unit.cpp","hops":2,"run":"bash test/includeprecisecheck.sh"}, {"p":"test/verify_csr.cpp","hops":2,"run":"bash test/a9disclosurecheck.sh"}, {"p":"test/rustimport_unit.cpp","hops":4,"run":"bash test/rustimportprecisecheck.sh"}], -"order":"evidence","partners":0,"run_first":1,"tests":5,"script_gates_unmodelled":767,"graph_ambiguous":2904,"graph_unresolved":14037,"graph_unindexed":266,"counts_floor":true,"next":"--uses=src/infra/sortutil.h:nonNegativeFloatDescKey"} +"order":"evidence","partners":0,"run_first":1,"tests":5,"script_gates_unmodelled":767,"graph_ambiguous":2911,"graph_unresolved":14064,"graph_unindexed":266,"counts_floor":true,"next":"--uses=src/infra/sortutil.h:nonNegativeFloatDescKey"} ``` ### `--insert-after-symbol=TARGET` @@ -3184,12 +3179,12 @@ _Insert immediately AFTER one uniquely-resolved definition; replaced_bytes=0 bec ``` $ ./build/ripwire . --insert-after-symbol=lessByScoreDescId --edit-payload=/aux/payload_note.h {"applied":"insert_after_symbol","symbol":"lessByScoreDescId","file":"src/infra/sortutil.h","span":{"start":2375,"end":2463},"lines":{"start":56,"end":58},"replaced_bytes":0,"old_file_bytes":10824,"new_file_bytes":10912,"file_eol":"lf","eol_normalized":false,"trailing_newline_folded":true,"separator … [line truncated: 652 more bytes on this line] -"graph_ambiguous":2904,"graph_unresolved":14037,"graph_unindexed":266,"counts_floor":true},"tests_to_run":[{"p":"test/verify_radix.cpp","hops":1,"run":"bash test/greptiercheck.sh"}, +"graph_ambiguous":2911,"graph_unresolved":14064,"graph_unindexed":266,"counts_floor":true},"tests_to_run":[{"p":"test/verify_radix.cpp","hops":1,"run":"bash test/greptiercheck.sh"}, {"p":"test/adaptivecutshapefix/adaptive_cut_shape_test.cpp","hops":2,"run":"bash test/adaptivecutshapecheck.sh"}, {"p":"test/includeprecise_unit.cpp","hops":2,"run":"bash test/includeprecisecheck.sh"}, {"p":"test/verify_csr.cpp","hops":2,"run":"bash test/a9disclosurecheck.sh"}, {"p":"test/rustimport_unit.cpp","hops":4,"run":"bash test/rustimportprecisecheck.sh"}], -"order":"evidence","partners":0,"run_first":1,"tests":5,"script_gates_unmodelled":767,"graph_ambiguous":2904,"graph_unresolved":14037,"graph_unindexed":266,"counts_floor":true,"next":"bash test/greptiercheck.sh"} +"order":"evidence","partners":0,"run_first":1,"tests":5,"script_gates_unmodelled":767,"graph_ambiguous":2911,"graph_unresolved":14064,"graph_unindexed":266,"counts_floor":true,"next":"bash test/greptiercheck.sh"} ``` **Caveats (stated by the binary):** @@ -3235,12 +3230,12 @@ $ ./build/ripwire . --insert-before-symbol=nonNegativeFloatDescKey --edit-payloa {"n":"benchAdaptive","p":"bench/bench_radix_ab.cpp:157","l":[162]}, {"n":"radixSortNonNegativeFloatsDesc","p":"src/infra/sortutil.h:105","l":[114]}, {"n":"radixSortByScoreDescId","p":"src/infra/sortutil.h:120","l":[180]}], -"graph_ambiguous":2904,"graph_unresolved":14037,"graph_unindexed":266,"counts_floor":true},"tests_to_run":[{"p":"test/verify_radix.cpp","hops":1,"run":"bash test/greptiercheck.sh"}, +"graph_ambiguous":2911,"graph_unresolved":14064,"graph_unindexed":266,"counts_floor":true},"tests_to_run":[{"p":"test/verify_radix.cpp","hops":1,"run":"bash test/greptiercheck.sh"}, {"p":"test/adaptivecutshapefix/adaptive_cut_shape_test.cpp","hops":2,"run":"bash test/adaptivecutshapecheck.sh"}, {"p":"test/includeprecise_unit.cpp","hops":2,"run":"bash test/includeprecisecheck.sh"}, {"p":"test/verify_csr.cpp","hops":2,"run":"bash test/a9disclosurecheck.sh"}, {"p":"test/rustimport_unit.cpp","hops":4,"run":"bash test/rustimportprecisecheck.sh"}], -"order":"evidence","partners":0,"run_first":1,"tests":5,"script_gates_unmodelled":767,"graph_ambiguous":2904,"graph_unresolved":14037,"graph_unindexed":266,"counts_floor":true,"next":"--uses=src/infra/sortutil.h:nonNegativeFloatDescKey"} +"order":"evidence","partners":0,"run_first":1,"tests":5,"script_gates_unmodelled":767,"graph_ambiguous":2911,"graph_unresolved":14064,"graph_unindexed":266,"counts_floor":true,"next":"--uses=src/infra/sortutil.h:nonNegativeFloatDescKey"} ``` **Shaped by:** `--insert-after-symbol` @@ -3299,11 +3294,11 @@ _The same plan committed: per-file locks, re-verify-before-write, atomic rename, ``` $ ./build/ripwire . --edit-plan=/aux/edit_plan.json --apply -{"schema":"ripwire.edit-plan/v1","mode":"apply","edits":1,"files":1,"callers_union":4,"graph_ambiguous":2904,"graph_unresolved":14037,"graph_unindexed":266,"counts_floor":true,"applied":1,"atomic_files":1,"atomic_scope":"per-file","rollback_on_write_error":true,"recheck_before_each_write":true,"mult … [line truncated: 537 more bytes on this line] +{"schema":"ripwire.edit-plan/v1","mode":"apply","edits":1,"files":1,"callers_union":4,"graph_ambiguous":2911,"graph_unresolved":14064,"graph_unindexed":266,"counts_floor":true,"applied":1,"atomic_files":1,"atomic_scope":"per-file","rollback_on_write_error":true,"recheck_before_each_write":true,"mult … [line truncated: 537 more bytes on this line] {"n":"benchAdaptive","p":"bench/bench_radix_ab.cpp:157","l":[162]}, {"n":"radixSortNonNegativeFloatsDesc","p":"src/infra/sortutil.h:107","l":[116]}, {"n":"radixSortByScoreDescId","p":"src/infra/sortutil.h:122","l":[182]}], -"graph_ambiguous":2904,"graph_unresolved":14037,"graph_unindexed":266,"counts_floor":true}}]} +"graph_ambiguous":2911,"graph_unresolved":14064,"graph_unindexed":266,"counts_floor":true}}]} ``` **Shaped by:** `--edit-check` @@ -3349,19 +3344,19 @@ _Bare --slice=SYM: the INVENTORY of sliceable locals (), so a calle ``` $ ./build/ripwire . --slice=rankGraphTeleport - - - - - - - - - - - - - + + + + + + + + + + + + + ... [3 more line(s); run it to see the whole thing] ``` @@ -3386,17 +3381,17 @@ _Forward flow: which statements the seed's value reaches, at the default depth b ``` $ ./build/ripwire . --slice=rankGraphTeleport:teleport --slice-flow=fwd - - + + - + teleport( pw.begin(), pw.end() );]]> - + - + ... [22 more line(s); run it to see the whole thing] @@ -3440,10 +3435,10 @@ $ ./build/ripwire . --slice-depth=3 _Hold a LOCATION, not a name: the enclosing-definition chain at FILE:LINE (a compiler error, a diff hunk, a stack frame), outermost -> innermost._ ``` -$ ./build/ripwire . --at=src/graph.h:9673 +$ ./build/ripwire . --at=src/graph.h:10134 - - + + ``` @@ -3467,7 +3462,7 @@ _No-LLM review-evidence bundle for the working-tree diff (clean tree = empty)._ ``` $ ./build/ripwire . --pr-context - ``` @@ -3493,11 +3488,11 @@ _Pairwise cross-arm conflict sites + suggested landing order (any committish sha ``` $ ./build/ripwire . --merge-scout=HEAD~2,HEAD~1 - - + + - + @@ -3572,7 +3567,7 @@ _Which refs of the `lane/` ref family still hold divergent authored work vs HEAD ``` $ ./build/ripwire . --stray-content=lane/ - + @@ -3606,19 +3601,19 @@ _Select the genuinely-unmerged refs of the `lane/` ref family and feed them to m ``` $ ./build/ripwire . --stray-content=lane/ --plan - + - + + - ... [17 more line(s); run it to see the whole thing] ``` @@ -3642,20 +3637,20 @@ _Cross-branch ABI-break gate over the `lane/` ref family: struct byte-contract d ``` $ ./build/ripwire . --stray-content=lane/ --abi - - - + + + + + + + + + - - - - - - - -... [16 more line(s); run it to see the whole thing] + +... [17 more line(s); run it to see the whole thing] ``` **Caveats (stated by the binary):** @@ -3677,11 +3672,11 @@ _Which ref's tree defines or mentions SYM — HEAD first, then every local branc ``` $ ./build/ripwire . --whereis=rankGraphTeleport - - - - - + + + + + ... [14 more line(s); run it to see the whole thing] ``` @@ -3707,10 +3702,10 @@ _The same page with every t= line whole: the page above previews each line over ``` $ ./build/ripwire . --whereis=rankGraphTeleport --whereis-listing=all --whereis-full-text - - - - + + + ... [24 more line(s); run it to see the whole thing] @@ -3735,11 +3730,11 @@ _The whole hit list asked for by name: under the same 60-row cap HEAD's referenc ``` $ ./build/ripwire . --whereis=rankGraphTeleport --whereis-listing=all - - + + - - + + ... [23 more line(s); run it to see the whole thing] @@ -3767,7 +3762,7 @@ _The dark-content dashboard: gates BUILT but OFF. CHANGED: no longer invents gat $ ./build/ripwire . --flags - + @@ -3848,7 +3843,7 @@ _The cache-locality lens over every aggregate: fields READ TOGETHER but declared ``` $ ./build/ripwire . --field-affinity - @@ -3883,15 +3878,15 @@ _Which of this repo's doc claims are now false. CHANGED: row attribute at= renam ``` $ ./build/ripwire . --doc-drift - + - + - - - + + + ... [20 more line(s); run it to see the whole thing] ``` @@ -3917,15 +3912,15 @@ _Which of this repo's doc claims are now false. CHANGED: row attribute at= renam ``` $ ./build/ripwire . --doc-drift - + - + - - - + + + ... [20 more line(s); run it to see the whole thing] ``` @@ -3950,16 +3945,16 @@ _Same report, with git history splitting stale mentions into deleted-by-commit v ``` $ ./build/ripwire . --doc-drift --with-history - - + + - + - - - + + + ... [20 more line(s); run it to see the whole thing] ``` @@ -3984,7 +3979,7 @@ _The house PLAN/DESIGN format's STRUCTURE check — never semantics; exit 2 when ``` $ ./build/ripwire . --plan-lint=test/planlintfix/wave.md - + @@ -4019,8 +4014,8 @@ $ ./build/ripwire . --from-trace=- AddressSanitizer:DEADLYSIGNAL ================================================================= ==41337==ERROR: AddressSanitizer: SEGV on unknown address 0x000000000018 (pc 0x000102f4a1c8 bp 0x00016d2f1a40 sp 0x00016d2f19e0 T0) - #0 0x102f4a1c8 in rw::rankGraphTeleport(Graph const&, std::vector const&, float) src/graph.h:9673 - #1 0x102f3e884 in rw::rankGraph(Graph const&, float) src/graph.h:9714 + #0 0x102f4a1c8 in rw::rankGraphTeleport(Graph const&, std::vector const&, float) src/graph.h:10134 + #1 0x102f3e884 in rw::rankGraph(Graph const&, float) src/graph.h:10175 #2 0x102e11f30 in runDefaultMap(MainDispatch const&) src/main.cpp:1753 #3 0x102e01a44 in main src/main.cpp:4174 #4 0x1a2b3c0dc in start+0x9dc (dyld:arm64e+0x60dc) @@ -4049,7 +4044,7 @@ _A command that exits 0: a minimal success record (exit, measured duration, disc $ ./build/ripwire . --run-trace="true" - + ``` @@ -4139,7 +4134,7 @@ _ONE budget-shared bundle: ranking + top bodies + caller sigs + notes + tests_to ``` $ ./build/ripwire . --pack-task="add a new output format flag to the CLI" - + + Print the authoritative CLI usage and flag catalog to the caller-provided output streaminline void printUsage( std::FILE* out ) noexcept @@ -4176,11 +4171,11 @@ _Fan-out form: one shared core + 3 per-agent slices carved along call-graph comm ``` $ ./build/ripwire . --pack-task="add a new output format flag to the CLI" --partition=3 - wraps a ; bundle role=core|partition i= symbols= modules= bytes= tokens=: one agent's ctx, symbols= ids assigned, bytes= its size; tokens= = est_tokens= = bytes/2.36 (flat, the densest rate; the inner ctx est_tokens= is language-weighted). Inner ctx task= root= budget_tokens= dropped_positive=: the task, p= base, the slice's token target, ranked candidates its budget cut. of_top=: rows ranked (far) / bodies (callers); s rel=caller|callee shared=: 1-hop edge, bodies reached (absent at 1). window: shown= total= capped= (capped=1 cut). run_unknown=1: no runner derivable (a guess would be worse). est_tokens=: price as emitted (an upper bound under compact). : rank N in this ranking, rows in r= order. : cyclomatic/cognitive complexity. : N callers in the index (absent: not measured). route=: the ranker: name-exact(X) = the task names symbol X (anchors: its evidence), subtoken+body = conceptual BM25 (:broad = 1-2 plain words, plain rg may also win; :declined(...) = a name hit refused as a common name). sc=: enclosing scope; the full id is p::sc::n (p= of the row or its ) and selectors take it. next=: the one pasteable follow-up. pure=1: const/constexpr signature (Swift: non-mutating) and no transitive side effect found; a hint. partitions=/requested=: partitions carved / asked for. modules=/split=: call-graph groups found / cuts forced to split one. core_symbols=/surface=: ids in the shared core / core plus the assignable remainder. budget_per_agent_tokens=: one agent's budget, core_budget_tokens= plus partition_budget_tokens=. total_bytes=N: bytes of all bundles together. overlap_mean=/overlap_max=: pairwise Jaccard over the ids partitions name, before trimming. shared_symbols=/union_symbols=: ids two or more partitions name / ids any partition names. core_overlap=: share of the core surface a partition reaches anyway. layer=: built-in arch layer (game|infra|render|math|audio|ai|test) from a dir name in p=; absent if none. --> - + Print the authoritative CLI usage and flag catalog to the caller-provided output streaminline void printUsage( std::FILE* out ) noexcept @@ -4209,7 +4204,7 @@ _Task lens + a compact Mermaid flowchart of the top anchors' 1-hop edges._ ``` $ ./build/ripwire . --for="pagerank power iteration" --with-graph - + @@ -4282,14 +4277,14 @@ _Environment self-check: binary staleness, grammars, cache dir, git, tracked-bin ``` $ ./build/ripwire . --doctor - - + - - + + - - + @@ -4316,14 +4311,14 @@ _--doctor plus a LIVE integration inspection for one agent: PATH binary, install ``` $ ./build/ripwire . --doctor --agent=claude - - + - - + + - - + @@ -4351,7 +4346,7 @@ $ ./build/ripwire . --skipped - @@ -4437,19 +4432,19 @@ _Rank by git change-frequency prior instead of PageRank._ ``` $ ./build/ripwire . --rank-by=churn --top-k=5 - + - - + + - + - + - + ... [7 more line(s); run it to see the whole thing] ``` @@ -4472,22 +4467,22 @@ a trailing slash is ignored; absolute paths and '..' are refused). The global block stays byte-identical, a second page follows it — n=/of= are its counts (of= IS the total, so the paging half carries no total=), capped="1" has_more="1" next_offset= offset= limit= page it, and next= replays THIS run's own corpus flags (--since/--exclude) so the page it names is a page of the same answer. The symbol map collapses to a disclosed stub — the map was not asked for and was not ranked at all, which is why the header carries no pr_iters=. merge_bombs_skipped= stays on the global block: it counts the window's skipped commits, not the directory's._ ``` -$ ./build/ripwire . --rank-by=churn-decay --since=HEAD~3 --exclude=test --exclude=docs --exclude=skills --in=src --limit=3 +$ ./build/ripwire . --rank-by=churn-decay --since=HEAD~6 --exclude=test --exclude=docs --exclude=skills --in=src --limit=3 - - - - - - - - + + + + - - + + + -... [7 more line(s); run it to see the whole thing] + + + +... [4 more line(s); run it to see the whole thing] ``` **Shaped by:** `--external-surface`, `--doctor`, `--legend`, `--max-memory` @@ -4549,7 +4544,7 @@ _The same gating report under --legend=full — same rows, same exit 2; the defa ``` $ ./build/ripwire . --quality-delta --legend=full - @@ -4577,7 +4572,7 @@ _The session legend dictionary the MCP server serves as ripwire://legend-dict/fu ``` $ ./build/ripwire . --legend-dict -ripwire legend dictionary ripwire.dict/v1 dictv=f1e98faa0aeaa1ee entries=902 +ripwire legend dictionary ripwire.dict/v1 dictv=30173b0aa2d06473 entries=908 : the answer's rows come first; its root keeps only task= changed= from= to=, and this LAST child carries every other root attribute unchanged (schema= included); legend="ref": a definition is sent once per session (this dictionary's core, or the first answer that ne … [line truncated: 83 more bytes on this line] schema=ripwire.KEY/v1: the line ripwire.KEY/v1 below reads the answer's rows window: shown= total= capped= has_more= next_offset= offset= limit= page a list (capped=1 cut; next_offset= pastes as offset=) @@ -4629,18 +4624,18 @@ _The family join: per function, which of four orthogonal evidence families fire, ``` $ ./build/ripwire . --ensemble --limit=8 - + - + - + ... [17 more line(s); run it to see the whole thing] ``` @@ -4665,8 +4660,8 @@ _Drop matching paths (repeatable) before ranking._ $ ./build/ripwire . --exclude=present --exclude=bench --top-k=5 - - + + @@ -4696,8 +4691,8 @@ _Full map re-ranked with teleport toward git-changed files — clean tree, so ch $ ./build/ripwire . --map-diff --top-k=5 - - + + @@ -4730,8 +4725,8 @@ _Explicit incremental cache at a path OUTSIDE the repo (first call writes it)._ $ ./build/ripwire . --cache=/aux/warm2.ripwirecache --top-k=3 - - + + @@ -4780,8 +4775,8 @@ _Force a cold parse (bypass the warm TMPDIR cache) — shows the cold-vs-warm co $ ./build/ripwire . --no-cache --top-k=3 - - + + @@ -4811,8 +4806,8 @@ _Crawl paths the repo's own .gitignore covers (default honours it and discloses $ ./build/ripwire . --no-ignore --top-k=3 - - + + @@ -4847,8 +4842,8 @@ _Skip files above a size bound before parsing (note the corpus shrink in the hea $ ./build/ripwire . --max-file-size=8K --top-k=3 - - + + @@ -4927,8 +4922,8 @@ _Eval-only: a per-call-site census of WHICH mechanism resolved each call, and th $ ./build/ripwire . --pin-census=/aux/pin_census.tsv --top-k=3 - - + + @@ -4981,10 +4976,10 @@ _--mcp-legend=inline: every answer of the stdio session carries its own legend ( ``` $ ./build/ripwire '%s\n' '{"jsonrpc":"2.0","id":1,"method":"initialize"}' '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"whereis","arguments":{"path":".","symbol":"rankGraphTeleport"}}}' '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"whereis","arguments":{"path":".","symbol":"computeOnePairOverlap"}}}' | ./build/ripwire --mcp --mcp-legend=inline {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-11-25","serverInfo":{"name":"ripwire","version":"1.0"},"capabilities":{"tools":{},"resources":{}},"instructions":"Map before reading files. Start a new task with explore; use from_trace for an error; use impact plus uses before changing a sym … [line truncated: 735 more bytes on this line] -{"jsonrpc":"2.0","id":2,"result":{"content":[{"type":"text","text":" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [722 more display lines; full output is 23369 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --top-k=5` + +*Same map, capped to the 5 highest-ranked symbols.* + +**wall time: 1.57s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [58 more display lines; full output is 8099 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --top-k=5 --no-inventory` + +*The same capped map without its inventory tier: --top-k=5 leaves files out, so the default map appends (program entries, and the files the ranked rows left out) after its rows; --no-inventory drops that tier, and the ranked rows are the same.* + +**wall time: 1.44s** + +````` + + + + + + + + + + + + + + + + + + + + + +````` + +## `./build/ripwire . --top-k=0 --expand=rankGraphTeleport` + +*NEW since the last capture: --top-k=0 means PAYLOAD-ONLY — no ranked map rides along with the body you asked for.* + +**wall time: 1.60s** + +````` + + + + + + + + + + +````` + +## `./build/ripwire . --token-budget=100` + +*GATE form: exit 3 if the map's own est_tokens exceeds the budget (over-budget failure shape).* + +**exit code: 3** — **wall time: 2.02s** + +````` + + +````` + +stderr: + +````` +ripwire: --token-budget exceeded: withheld_est_tokens=9415 > budget=100 +````` + +## `./build/ripwire . --for="incremental cache invalidation when a file content hash changes"` + +*The task lens: ranked signatures + quality metrics framed for the task.* + +**wall time: 10.00s** + +````` + + + + +incremental cache (--cache): per-file content hash + raw facts so a re-run re-parses ONLY c…constexpr std::uint32_t kCacheMagic = 0x4b505443 + +and every file whose content hash differs is re-parsed — so a stale or foreign blob self-heals…constexpr std::uint32_t kHeadSnapCacheScheme = 1 + +persistent in-memory index (parse once, reuse across MCP calls) ---- The MCP server is long-live…struct McpIndex +inline void prewarmTagsQueries( const std::vector<std::string>& files, const HashMap<std::string, FileFacts>& cache, long long cacheWriteNs, IngestFile … [line truncated: 20 more bytes on this line] +inline bool ingestCommitTree( const std::string& root, const std::string& sha, const std::vector<std::string>& excludes, std::size_t maxFileBytes, IngestResult& ou … [line truncated: 7 more bytes on this line] +inline std::vector<std::vector<NodeId>> legoImplementorsOnSurface( const IngestResult& ing, const std::vector<std::vector<NodeId>>& imple … [line truncated: 62 more bytes on this line] +inline std::string spanTierMemoPath( const std::string& diskPath ) +inline RawFacts runParsePool( IngestResult& result, const char* rootDir, std::string_view cacheFile, bool captureValueUses, HashMap<std::string, FileFacts>& cach … [line truncated: 98 more bytes on this line] +inline McpRebuildBaseline mcpRebuildBaseline( const McpIndex& ix, bool isIncrementalPass ) +inline const McpIndex& getIndex( const std::string& root ) +inline std::uint64_t contentHash64( std::string_view s ) noexcept +struct CacheEntry +inline std::uint64_t blobChecksum( std::string_view s ) noexcept +inline ContentIdIndex contentIdsBySym( const IngestResult& ing, const Graph& g, std::string_view root ) +inline std::vector<CacheWriteRow> buildCacheWritePlan( const std::vector<std::uint32_t>& orderIn, const std::vector<std::uint64_t>& pathHashes … [line truncated: 25 more bytes on this line] +inline std::string reAbsolutize( std::string_view rel, std::string_view root ) +inline CachePathKeys buildCachePathKeys( const std::vector<std::string>& files, std::string_view rootDir ) +inline std::uint64_t indexContentHash( const std::vector<std::string>& files, const std::vector<long long>& fileMtime, const std::vector<std::uint64_t> … [line truncated: 25 more bytes on this line] +inline std::string makeHandle( const std::string& canonId, const std::string& path, const std::string& name, std::uint64_t contentHash ) +struct IngestFileScan +inline void collectExtractPartials( const IngestFileScan& scan, IngestResult& result ) +inline std::uint64_t byteHash( const char* data, std::size_t n ) noexcept +void emitGrepHandleLegend( bool enabled ) +… [121 more display lines; full output is 19577 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --for="rankGraphTeleport"` + +*Name-shaped query: the router picks name-exact BM25 (header says which/why).* + +**wall time: 5.96s** + +````` + + + + + +PageRank with an explicit teleport / personalization vector p (Σp = 1). The prior is name-quality-biased through biasPrior() so all rank modes share one weighting seam; the transition matrix (edgesinline RankedGraph rankGraphTeleport( const Graph& g, const std::vector<float>&am … [line truncated: 31 more bytes on this line] +#### The convergence disclosure contract +### Wave-2 adversarial verification (2026-08-19) — six probes against `aa97c9e` + + + + + + + + +& p, float alpha = 0.85f ) +{ + PROFILE_SCOPE_DESCRIBE( "rankGraph: PageRank (power iteration)" ); + const std::vector pw = biasPrior( g, p ); + const std::size_t N = pw.size(); + std::vector teleport( pw.begin(), pw.end() ); + std::vector rankDouble( N, 0.0 ); + PageRankRun run{}; // an N == 0 graph never enters the kernel: { 0, converged } — see PageRankRun + if( N ) + { + double teleportMass = 0.0; + for( const double value : teleport ) + { + teleportMass += value; +… [15 more display lines; full output is 4564 bytes on 29 raw line(s)] +````` + +## `./build/ripwire . --for="rankGraphTeleport" --no-route` + +*Same query with routing forced OFF (plain subtoken+body BM25) — contrast with the routed run.* + +**wall time: 4.21s** + +````` + + + + + +uniform-teleport PageRank (the defaultinline RankedGraph rankGraph( const Graph& g, float alpha = 0.85f ) + +PageRank with an explicit teleport / personalization vector p (Σp = 1). The prior is name-quali…inline RankedGraph rankGraphTeleport( const Graph& g, const std::vector<float>& p, float alpha = 0.85f ) +inline ChurnRanking churnRankedGraph( const MainDispatch& d ) + +L10 (2026-09-04): the old wording claimed "the same corpus ranked by pagerank orders differently…inline constexpr const char* kChurnRankLegend = "<!-- rank_by=churn: k= is PageRank re-run with the teleport BIASED by git CHANGE-FREQUENCY over window= " "(a c… +struct RankedGraph +inline std::vector<float> takeRank( RankedGraph ranked, RankDisclosure& disclosureOut ) +inline std::vector<float> biasPrior( const Graph& g, const std::vector<float>& p ) +inline std::vector<float> churnTeleportWorkspace( const std::vector<std::string>& rootDirs, const IngestResult& ing, const char* since = "18 months ago& … [line truncated: 20 more bytes on this line] +inline ChurnRanking churnDecayRanking( const MainDispatch& d, const rw::SinceScope& sinceScope, bool isScoped, const char* verbLabel ) +inline std::string rankByText( const std::string& root, std::string_view mode, int topK, bool stable = false ) +PageRankRun pageRankDouble( const sparseCsr<float>& inEdges, std::span<const double> weightedOutDegree, std::span<const double> teleport, std::span<double> rank… +inline std::vector<float> churnDecayTeleport( const std::string& root, const IngestResult& ing, const SinceScope* scope = nullptr, bool* outHasChurnEvidence = n… +inline std::string didYouMean( const IngestResult& ing, std::string_view name ) +PageRankRun pageRankDouble( const sparseCsr<float>& inEdges, std::span<const double> weightedOutDegree, std::span<const double> teleport, std::span< … [line truncated: 45 more bytes on this line] +inline const McpIndex& getIndex( const std::string& root ) +inline std::vector<float> churnDecayTeleportWorkspace( const std::vector<std::string>& rootDirs, const IngestResult& ing, bool* outHasChurnEvidence = n … [line truncated: 12 more bytes on this line] +struct DataSectionsCut +inline int runEval( const std::string& root, const IngestResult& ing, const Graph& g, const std::vector<char>& currentDiff ) +inline std::vector<float> anchoredLexicalRank( const Graph& g, const std::vector<float>& lex ) +### Fixed — `rank_by` over MCP no longer answers a different ranking than `--rank-by` on … [line truncated: 35 more bytes on this line] +### 2. Gates for A +inline constexpr std::size_t kChurnMergeBombMaxFiles = 100 +#### The convergence disclosure contract +… [149 more display lines; full output is 24042 bytes on 72 raw line(s)] +````` + +## `./build/ripwire . --for="rankGraphTeleport" --signatures-only` + +*T3 opt-out: the signatures-only lens (no auto bodies, no bundle="auto" attribute) — contrast with the terminal default above.* + +**wall time: 5.58s** + +````` + + + + + +PageRank with an explicit teleport / personalization vector p (Σp = 1). The prior is name-quality-biased through biasPrior() so all rank modes share one weighting seam; the transition matrix (edgesinline RankedGraph rankGraphTeleport( const Graph& g, const std::vector<float>&am … [line truncated: 31 more bytes on this line] +#### The convergence disclosure contract +### Wave-2 adversarial verification (2026-08-19) — six probes against `aa97c9e` + + + + + + + +````` + +## `./build/ripwire . --for="tree-sitter parse of a source file" --adaptive` + +*Cut the result at the relevance cliff (Adaptive-k) — on a flat ranking nothing is cut and the header says so ([adaptive: kept N of N]).* + +**wall time: 4.77s** + +````` + + + + +inline bool topLevelEvidence( std::string_view source, const TSLanguage* language, const Predicate& predicate ) +The crawl's per-file byte ceiling. A text file larger than this is skipped: at this size it is o…constexpr std::size_t kDefaultMaxFileBytes = 4u * 1024u * 1024u +TSTree* parseTree( TSParser* parser, std::string_view src ) + +Exercise every registered grammar and its embedded query, reporting loaded and expected totalsinline DoctorGrammarProbe doctorProbeGrammars() +struct FileHealth +constexpr std::array<LangEntry, 57> kLangTable = +inline RevSide sliceAtRev( const std::string& root, const std::string& sha, const std::string& rel, const Symbol& sym, SliceFam fam, const ::TSLanguage* grammar…struct SliceScan +enum class AstWalk : std::uint8_t +inline bool astroFenceTailIsBlank( std::string_view src, std::size_t from, std::size_t to ) noexcept +constexpr std::uint32_t kMaxKotlinStringNestDepth = 128u +std::vector<LocalNameFact> collectGatedLocalNames( std::string_view defBytes, std::uint32_t defStartLine, Lang lang ) +inline ModuleScan scanModule( std::string_view source, std::string_view path ) +inline bool hasPhantomScopeSeparator( TSNode qualified ) noexcept +bool jsonNestsTooDeep( std::string_view bytes ) noexcept +constexpr std::uint32_t kMaxYamlNestDepth = 64u +inline bool hasMainGuard( std::string_view source ) +SpanTierBatch spanTiersOfFiles( std::span<const std::string> diskPaths, bool useMemo ) +inline SliceScan sliceScanDefinition( const std::string& src, const Symbol& sym, SliceFam fam, const ::TSLanguage* grammar, std::string_view varName ) +inline void runParseWorker( ParsePoolShared& sh, unsigned t ) +enum class DisclosureWhy : std::uint8_t +struct LintRule +inline std::string buildMemoryStopAttr( const IngestResult& ing, bool json ) +inline bool hasPytestTable( std::string_view source ) +… [137 more display lines; full output is 19683 bytes on 44 raw line(s)] +````` + +## `./build/ripwire . --for="why does src/lexical.h chooseForRanker pick name-exact BM25"` + +*Mention anchoring (default-on): a path and a Symbol literally named in the task get lifted; the header says what anchored.* + +**wall time: 3.82s** + +````` + + + + +The name-exact ranker AS THE RETRIEVAL LENS SERVES IT: whole-name BM25 plus the definition-over-…inline std::vector<float> lexicalScoresNameExactRanked( const IngestResult& ing, std::string_view query, const std::vector<float>* symbolScoreMul ) + +P11.12: the interpretive footer for --eval's ranker table, pulled into its own function so the 9…inline void printEvalRankerNote() +inline int runEvalRetrieval( const IngestResult& ing, const Graph& g ) + +calling agent knows to reformulate rather than trust the ranking. Calibrated empirically (2026-0…inline constexpr float kWeakLexicalScoreThreshold = 1.0f +inline RouteChoice chooseForRanker( const IngestResult& ing, std::string_view query ) +inline std::string candidatesRootTag( std::size_t keep, std::size_t corpusCount, const CandidateProvenance& prov ) +inline std::vector<float> lexicalScoresNameExactTiered( const IngestResult& ing, std::string_view query, const std::vector<float>* symbolScoreMul ) +inline int runEvalMined( const std::string& root, const IngestResult& ing, const Graph& g, const std::string& path ) +struct NameCorpusStats +inline int runEvalSkills( const std::string& root, const IngestResult& ing, const Graph& g, const std::string& labelsPath ) +inline bool hasIdentifierShape( std::string_view w ) noexcept +struct LensRanking +struct Bm25Params +inline int runEval( const std::string& root, const IngestResult& ing, const Graph& g, const std::vector<char>& currentDiff ) +inline double bm25ImpactBound( double idf, double T, const Bm25Params& p ) noexcept +rw::LensRanking computeLensRanking( const MainDispatch& d, std::string_view task, bool compactCandidate = false, bool fullDistribution = false ) // deep-tail: the file-grain tail rea … [line truncated: 65 more bytes on this line] +struct CandidateProvenance +inline constexpr int kLexWeightDoc = 2 +std::optional<int> runEvalViews( const MainDispatch& d ) +inline std::vector<float> lexicalScoresNameExact( const IngestResult& ing, std::string_view query ) +inline std::string_view takeQualifiedIdent( std::string_view src, std::size_t& i ) +struct RecallSectionPick +inline double binomialChoose( std::size_t n, std::size_t k ) +… [146 more display lines; full output is 21490 bytes on 18 raw line(s)] +````` + +## `./build/ripwire . --for="why does src/lexical.h chooseForRanker pick name-exact BM25" --no-mention-boost` + +*Same task with the anchor disabled — the contrast the flag exists for.* + +**wall time: 2.84s** + +````` + + + + +The name-exact ranker AS THE RETRIEVAL LENS SERVES IT: whole-name BM25 plus the definition-over-…inline std::vector<float> lexicalScoresNameExactRanked( const IngestResult& ing, std::string_view query, const std::vector<float>* symbolScoreMul ) + +P11.12: the interpretive footer for --eval's ranker table, pulled into its own function so the 9…inline void printEvalRankerNote() +inline int runEvalRetrieval( const IngestResult& ing, const Graph& g ) + +calling agent knows to reformulate rather than trust the ranking. Calibrated empirically (2026-0…inline constexpr float kWeakLexicalScoreThreshold = 1.0f +inline std::string candidatesRootTag( std::size_t keep, std::size_t corpusCount, const CandidateProvenance& prov ) +inline std::vector<float> lexicalScoresNameExactTiered( const IngestResult& ing, std::string_view query, const std::vector<float>* symbolScoreMul ) +inline int runEvalMined( const std::string& root, const IngestResult& ing, const Graph& g, const std::string& path ) +struct NameCorpusStats +inline int runEvalSkills( const std::string& root, const IngestResult& ing, const Graph& g, const std::string& labelsPath ) +inline bool hasIdentifierShape( std::string_view w ) noexcept +struct LensRanking +struct Bm25Params +inline RouteChoice chooseForRanker( const IngestResult& ing, std::string_view query ) +inline int runEval( const std::string& root, const IngestResult& ing, const Graph& g, const std::vector<char>& currentDiff ) +inline double bm25ImpactBound( double idf, double T, const Bm25Params& p ) noexcept +rw::LensRanking computeLensRanking( const MainDispatch& d, std::string_view task, bool compactCandidate = false, bool fullDistribution = false ) // deep-tail: the file-grain tail rea … [line truncated: 65 more bytes on this line] +struct CandidateProvenance +inline constexpr int kLexWeightDoc = 2 +std::optional<int> runEvalViews( const MainDispatch& d ) +inline std::vector<float> lexicalScoresNameExact( const IngestResult& ing, std::string_view query ) +inline std::string_view takeQualifiedIdent( std::string_view src, std::size_t& i ) +struct RecallSectionPick +inline double binomialChoose( std::size_t n, std::size_t k ) +… [154 more display lines; full output is 21492 bytes on 18 raw line(s)] +````` + +## `./build/ripwire . --lego=Vehicle` + +*Interface -> implementors view: every existing impl of the named interface; the method contract is extracted for the C-family/Java/TS/Python tiers — for a Rust trait (this fixture) it discloses caveat="not-extracted-for-lang" rather than an empty list.* + +````` + + + + + + + + + +````` + +## `./build/ripwire . --exemplar="format byte sizes for humans"` + +*The repo's best-in-class instance to imitate before writing new code (picked by ROLE).* + +**wall time: 2.18s** + +````` + + + + + f, A&&... a ) +{ + // A fixed char[] must arrive as rw::cstr( buf ). printf's %s always meant "bytes to the first NUL"; + // `{}` on a char[N] is a different question that library versions answer differently, and an + // implementation that formats the ARRAY emits the trailing NUL and the uninitialised bytes after it. + // A regex sweep missed sites twice, so the compiler enforces it instead of a reviewer. + // Only a MUTABLE char[N] is rejected. A string literal is const char[N]; every implementation formats + // that as a string, and the hazard here is the reusable buffer that was written short. + static_assert( ( ... && !( std::is_array_v> + && !std::is_const_v>> ) ), + "pass rw::cstr( buf ) for a fixed char buffer: {} on a char[N] is not printf's %s" ); + std::fputs( std::format( f, std::forward( a )... ).c_str(), stream ); +}]]> +````` + +## `./build/ripwire . --help-task="calls(runDefaultMap, rankGraphTeleport)"` + +*Deterministic enhanced help: a closed claim in the task is a structured shape, so the router recommends the ONE command that answers it (--verify) with the evidence behind the pick. Advice only — nothing executes.* + +**wall time: 1.96s** + +````` + + + + +ripwire '.' --verify='calls(runDefaultMap, rankGraphTeleport)' + + +````` + +## `./build/ripwire . --help-task="write a cheerful release announcement"` + +*The honest half of the contract: a task with no ripwire-shaped evidence ABSTAINS with zero commands rather than guessing.* + +**wall time: 2.40s** + +````` + + + + +````` + +## `./build/ripwire . --recall="quality delta gating exit codes"` + +*Most relevant DOCS' full bodies (markdown only) — recall what is already written down.* + +**wall time: 1.46s** + +````` +ripwire recall — "quality delta gating exit codes" — 114 relevant of 230 document files, best-first — total=114 shown=8 capped=1 truncated=7 generated_demoted=3 max_tokens=8000 share_bytes=2157 est_tokens=4593 + +━━ README.md (relevance 8.053) ━━ [sections: 2 of 25 selected (60 in doc), section-granular; whole doc 222723 B; lines="2747-2759,2760-2767"; dropped_by_budget=23] +#### 6.2 Exit codes + +This is the part to wire into a script. + +| Code | Meaning | +| --- | --- | +| 0 | The command completed. | +| 1 | The command refused the request. A refusal names the reason on stderr. When `--callers`, `--callees`, `--uses`, `--impact` or `--path` refuse a selector that matches no indexed definition, they also print an answer on stdout: the verb's element with `found="0"` and the name to retry with. | +| 2 | A policy gate fired: `--arch` found a layering violation, `--scan-skill` found a CRITICAL, `--quality-delta` found new debt. | +| 3 | The output exceeded the token budget that you set. | +| 4 | `--test-gate` found an open obligation. | +| 5 | The memory guard stopped the run: over `--max-memory` (default 65% of the machine's memory) with no partial answer possible, or a verb other than the default map facing a partial index. One stderr line names the limit and the override. | + +#### 6.3 JSON output + +`--json` emits JSON instead of XML, with keys mirroring the XML attribute names one to one. It is an +allow-list: the default map, `--for`, `--pack-task`, `--callers`, `--callees`, `--impact`, +`--quality-delta`, `--test-gate`, `--metrics`, and `--plan-lanes`, which is JSON-native. Every other +verb refuses on stderr with exit 1 rather than silently falling back to XML, so a verb added tomorrow +refuses by default. + + + +━━ CHANGELOG.md (relevance 7.541) ━━ [sections: 1 of 109 selected (451 in doc), section-granular; whole doc 997808 B; lines="554-569"; dropped_by_budget=108] [truncated: 1597 of 3826 bytes] +### Changed — `--quality-delta=A..B` answers the kinds it can under memory pressure, and exits 6 when that answer has no gating row (#387 follow-up) + +A tripped memory guard in the range form used to give exit 5 and nothing, so on a large repository a four-file change +… [129 more lines, 11714 bytes total] +````` + +## `./build/ripwire /aux/kbcorpus --recall="field affinity cache line data layout which fields are read together" --top-k=3 --max-tokens=1200` + +*The directory-as-knowledge-base pattern: --recall pointed at a 1359757-byte scratch dir of DUMPED TOOL OUTPUT (a git log, this repo's own generated command reference, --help text, an architecture doc, a fabricated JSON access log) instead of a source repo — no index to build, no daemon.* + +````` +ripwire recall — "field affinity cache line data layout which fields are read together" — 2 relevant of 2 document files, best-first — total=2 shown=1 capped=1 truncated=1 max_tokens=1200 share_bytes=2040 est_tokens=957 + +━━ commands.md (relevance 10.114) ━━ [sections: 1 of 164 selected (192 in doc), section-granular; whole doc 589140 B; lines="3838-3842"; dropped_by_budget=163] [truncated: 1865 of 7738 bytes] +### `--field-affinity[=STRUCT]` + +**Answers:** find fields that are read together but declared far apart — the cache-locality lens the CACHE-LOCALITY lens: which fields are READ TOGETHER but declared FAR APART. + +Builds a static field CO-ACCESS affinity graph (one observation per indexed C-family function body) and diffs it against the DECLARED field order and 64-byte cache-line geometry, reusing --layout's LP64 offset model. Bare = every aggregate in the repo, ranked by separation cost; =STRUCT narrows the … [line truncated: 1355 more bytes on this line] + +(capped: 1 of 2 relevant document files omitted — raise --max-tokens/budget_tokens or narrow the query for 1 more (~2548-byte budget)) +````` + +stderr: + +````` +ripwire: redacted 1 secret from emitted context (keyword-gated-secret=1) — pass --no-redact to disable +````` + +## `./build/ripwire . --tree` + +*File-by-file orientation map (top symbols per file).* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [368 more display lines; full output is 11234 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --html=/aux/map2.html` + +*Self-contained HTML force-directed call graph.* + +**wall time: 4.25s** + +````` +(empty) +````` + +Artifact written: + +````` + 156578 /aux/map2.html +````` + +## `./build/ripwire . --order=stable --top-k=5` + +*Stable (path/id) emit order — provider KV-cache hits across re-runs.* + +**wall time: 1.11s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [58 more display lines; full output is 8117 bytes on 1 raw line(s)] +````` + + +--- + +# navigate / answer a question + +## `./build/ripwire . --around=rankGraphTeleport` + +*Ego graph around one symbol — depth 1 BY DEFAULT now (the root's depth= says so): ~6 KB where the 2-hop neighbourhood is ~64 KB on this repo.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [163 more display lines; full output is 7764 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --around=rankGraphTeleport --around-depth=2` + +*The restoring knob: --around-depth=2 brings back the whole 2-hop neighbourhood (depth="2" on the root) — pay for it only when the 1-hop view was not enough.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [1920 more display lines; full output is 52411 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --around=rankGraphTeleport --around-fanout=4` + +*The other knob: --around-fanout=4 keeps only the 4 strongest edges per node (default 32) — the same 1-hop depth, a quarter of the rows.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +````` + +## `./build/ripwire . --callers=rankGraphTeleport` + +*Who calls SYM (1-hop in-edges).* + +````` + + + + + + + + + + +````` + +## `./build/ripwire . --callers=rankGraphTeleport --legend=full` + +*The same rows under --legend=full: the default is the compact dialect (one compact comment plus schema="ripwire.callers/v1" on the root), and --legend=full restores the prose legend — every row byte and every completeness attribute (counts_floor=, graph_ambiguous=, next=) identical, ~3 KB more legend. Works on EVERY XML verb.* + +````` + + + + + + + + + + + +````` + +## `./build/ripwire . --callers=DoesNotExist` + +*Unknown-symbol REFUSAL shape (exit 1) with a did-you-mean from real edit distance.* + +**exit code: 1** — **wall time: 2.04s** + +````` + + +````` + +stderr: + +````` +ripwire: --callers symbol not found: DoesNotExist +````` + +## `./build/ripwire . --callees=rankGraphTeleport` + +*What SYM calls (1-hop out-edges).* + +````` + + + + + + + + + + + + + + +````` + +## `./build/ripwire . --uses=rankGraphTeleport` + +*The resolvable use-sites (call/read/write/import/extends) with file:line; count= is a floor.* + +**wall time: 1.20s** + +````` + + + + + + + + + + + + +````` + +## `./build/ripwire . --graph-query='and(callers(name("rankGraphTeleport"),2),kind(all,fn))'` + +*Composable node-set query: functions within 2 caller-hops of rankGraphTeleport.* + +**wall time: 1.01s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [28 more display lines; full output is 3650 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --external-surface` + +*Names referenced but never defined in-corpus (stdlib/third-party surface). The root carries names/shown/capped; the default is a 100-row window now, so total= and a pasteable next= join them when it bites (the explicit --limit form carries the same quintet). The sh BUILTINS (cd/echo/set…) are dropped and COUNTED as builtins_excluded= — grep/sed/git stay, they ARE the surface.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [73 more display lines; full output is 5588 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --external-surface --include-builtins` + +*The restoring knob: --include-builtins keeps the shell builtins in the same 100-row window (they now compete for rows, so the window's tail differs).* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [73 more display lines; full output is 5454 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --path=main,rankGraphTeleport` + +*Shortest directed call-path SRC -> DST. CHANGED: now reports from_p/to_p/from_defs and resolves the right `main` (was reachable="0").* + +**wall time: 1.10s** + +````` + + + + + + + + +````` + +## `./build/ripwire . --connect=rankGraphTeleport,runEval,getIndex` + +*Minimal connecting subgraph over 3 symbols (finds shared-caller joins).* + +````` + + + + + + + + + + +````` + +## `./build/ripwire . --impact=rankGraphTeleport` + +*Transitive blast radius — everything that reaches SYM. NOW carries shown/capped.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [69 more display lines; full output is 6414 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --mentions=rankGraphTeleport` + +*Markdown docs that name SYM in a backtick (doc<->code edges).* + +**wall time: 1.18s** + +````` + + + + + + + + + + +````` + +## `./build/ripwire . --affected=src/graph.h` + +*Test files that transitively reach the changed file.* + +**wall time: 1.24s** + +````` + + + + + + + + + +````` + +## `./build/ripwire . --situ` + +*Mid-task situational report for the current git diff — recorded against a CLEAN tree (contrast with the sandbox run below).* + +**wall time: 2.44s** + +````` +ripwire situational-awareness — 0 changed file(s), 0 symbols in them +root: . +at: eb541409e + (0 changed files — working tree is clean, nothing to analyze) +````` + +## `./build/ripwire . --test-gate` + +*Pre-PR gate on a CLEAN tree: no obligations, exit 0.* + +**wall time: 4.88s** + +````` + + +````` + +## `./build/ripwire . --grep=DISCLOSE` + +*Literal trigram-indexed search. Each hit carries its MATCHED line as the element's own CDATA (the wrapper is gone), plus shown/capped/hits_capped and a pasteable next= on the root.* + +**wall time: 1.08s** + +````` + +if data.get( "schema" ) != SCHEMA: raise SystemExit( f"{path}: unexpected schema {data.get('schema')!r} (expecte +if not str( data.get( "tasks_lock_content_sha256", "" ) ).startswith( "questions:" ): train_repos = select_tasks +if train_repos: raise SystemExit( f"{path}: records from repo(s) that re-derive to LocBench +if base and ctx and base["status"] == "ok" and ctx["status"] == "ok": paired.append( ( instance_id, base["re +if not repos: return 0.0, [] +if base["localization_hit"] is None or ctx["localization_hit"] is None: return 0.0 +if bv: ratios.append( cv / bv - 1 ) +if not ratios: return None, None +if rw is None or native is None: return None +if not paired: out["note"] = "zero complete paired (baseline,ripwire_cli) runs — nothing to analyze yet" +if contaminated: print( f" ** {contaminated} baseline run(s) invoked ripwire despite the no-ripwire contract " +if "note" in out: print( f" {out['note']}" ); return +if out["n_pairs"] != 27: failures.append( f"expected 27 paired runs, got {out['n_pairs']}" ) +if out["n_incomplete"] != 2: failures.append( f"expected 2 incomplete pairs (the orphan + the contaminated-basel +if out["n_repos"] != 3: failures.append( f"expected 3 repos, got {out['n_repos']}" ) +if out.get( "n_contaminated_baseline" ) != 1: failures.append( f"expected exactly 1 contaminated baseline run co +if not ( out["resolved_delta_mean"] > 0 ): failures.append( "expected a positive resolved-rate delta" ) +if not ( out["resolved_delta_bootstrap_95_lower"] > 0 ): failures.append( "expected a POSITIVE bootstrap 95% low +if out["tokens_out_ratio_p50"] is None or abs( out["tokens_out_ratio_p50"] - 0.08 ) > 1e-6: failures.append( f"e +if out.get( "n_resolved_pairs" ) != 27: failures.append( f"expected all 27 pairs resolution-scored, got {out.get +if out.get( "substitution_rate_baseline" ) != 0.0: failures.append( f"expected baseline substitution rate 0.0 (n +if out.get( "substitution_rate_ripwire" ) is None or abs( out["substitution_rate_ripwire"] - 0.75 ) > 1e-9: fail +if out.get( "n_substitution_ripwire" ) != 27: failures.append( f"expected 27 substitution-scored ripwire runs, g +if out3.get( "substitution_rate_ripwire" ) is not None or out3.get( "n_substitution_ripwire" ) != 0: failures.ap +if out2["n_pairs"] != 27: failures.append( f"evaluator-none: expected 27 pairs, got {out2['n_pairs']}" ) +if out2["n_resolved_pairs"] != 0: failures.append( f"evaluator-none: expected 0 resolution-scored pairs, got {ou +if out2["resolved_delta_mean"] is not None or out2["resolved_delta_bootstrap_95_lower"] is not None: failures.ap +if out2["tokens_out_ratio_p50"] is None or abs( out2["tokens_out_ratio_p50"] - 0.08 ) > 1e-6: failures.append( " +… [73 more display lines; full output is 16956 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --match='(if_statement) @i'` + +*The same shape query WITH an explicit capture — identical hits, no auto_captured= attribute.* + +**wall time: 1.90s** + +````` + +if data.get( "schema" ) != SCHEMA: raise SystemExit( f"{path}: unexpected schema {data.get('schema')!r} (expecte +if not str( data.get( "tasks_lock_content_sha256", "" ) ).startswith( "questions:" ): train_repos = select_tasks +if train_repos: raise SystemExit( f"{path}: records from repo(s) that re-derive to LocBench +if base and ctx and base["status"] == "ok" and ctx["status"] == "ok": paired.append( ( instance_id, base["re +if not repos: return 0.0, [] +if base["localization_hit"] is None or ctx["localization_hit"] is None: return 0.0 +if bv: ratios.append( cv / bv - 1 ) +if not ratios: return None, None +if rw is None or native is None: return None +if not paired: out["note"] = "zero complete paired (baseline,ripwire_cli) runs — nothing to analyze yet" +if contaminated: print( f" ** {contaminated} baseline run(s) invoked ripwire despite the no-ripwire contract " +if "note" in out: print( f" {out['note']}" ); return +if out["n_pairs"] != 27: failures.append( f"expected 27 paired runs, got {out['n_pairs']}" ) +if out["n_incomplete"] != 2: failures.append( f"expected 2 incomplete pairs (the orphan + the contaminated-basel +if out["n_repos"] != 3: failures.append( f"expected 3 repos, got {out['n_repos']}" ) +if out.get( "n_contaminated_baseline" ) != 1: failures.append( f"expected exactly 1 contaminated baseline run co +if not ( out["resolved_delta_mean"] > 0 ): failures.append( "expected a positive resolved-rate delta" ) +if not ( out["resolved_delta_bootstrap_95_lower"] > 0 ): failures.append( "expected a POSITIVE bootstrap 95% low +if out["tokens_out_ratio_p50"] is None or abs( out["tokens_out_ratio_p50"] - 0.08 ) > 1e-6: failures.append( f"e +if out.get( "n_resolved_pairs" ) != 27: failures.append( f"expected all 27 pairs resolution-scored, got {out.get +if out.get( "substitution_rate_baseline" ) != 0.0: failures.append( f"expected baseline substitution rate 0.0 (n +if out.get( "substitution_rate_ripwire" ) is None or abs( out["substitution_rate_ripwire"] - 0.75 ) > 1e-9: fail +if out.get( "n_substitution_ripwire" ) != 27: failures.append( f"expected 27 substitution-scored ripwire runs, g +if out3.get( "substitution_rate_ripwire" ) is not None or out3.get( "n_substitution_ripwire" ) != 0: failures.ap +if out2["n_pairs"] != 27: failures.append( f"evaluator-none: expected 27 pairs, got {out2['n_pairs']}" ) +if out2["n_resolved_pairs"] != 0: failures.append( f"evaluator-none: expected 0 resolution-scored pairs, got {ou +if out2["resolved_delta_mean"] is not None or out2["resolved_delta_bootstrap_95_lower"] is not None: failures.ap +if out2["tokens_out_ratio_p50"] is None or abs( out2["tokens_out_ratio_p50"] - 0.08 ) > 1e-6: failures.append( " +… [73 more display lines; full output is 16840 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --query="teleport pagerank" --top-k=5` + +*Raw BM25 ranking (debug lens; --for is the real verb).* + +**wall time: 1.63s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [21 more display lines; full output is 3219 bytes on 1 raw line(s)] +````` + + +--- + +# zoom the detail ladder + +## `./build/ripwire . --for="pagerank power iteration" --detail=2` + +*Importance-weighted detail: FULL bodies for top-2, signatures for the rest.* + +**wall time: 3.62s** + +````` + + + + + +NodeId order. Renumbering an otherwise-identical graph (the exact "same probe file, different so…inline constexpr float kScoreTieAbsEps = 1e-6f + +Render one form of the disclosure. Empty string whenever there is nothing to say — no power it…inline std::string renderDisclosure( const RankDisclosure& d, DiscloseAs as ) + +The PageRank power iteration itself — the numeric kernel every ranked document's order comes f…PageRankRun pageRankDouble( const sparseCsr<float>& inEdges, std::span<const double> weightedOutDegree, std::span<const double> teleport, std::span<double> r … [line truncated: 10 more bytes on this line] +inline std::string rankByText( const std::string& root, std::string_view mode, int topK, bool stable = false ) +struct RankedGraph +inline constexpr const char* kChurnRankLegend = "<!-- rank_by=churn: k= is PageRank re-run with the teleport BIASED by git CHANGE-FREQUENCY over window= " "(a c… +inline std::uint32_t sliceRdIterationCeiling() noexcept +struct PageRankRun + +What a ranked document discloses about the power iteration that ordered it. `isPageRank == false…struct RankDisclosure +inline std::uint32_t navRelevanceWeight( const Graph& g, NodeId n ) noexcept +inline RankedGraph rankGraphTeleport( const Graph& g, const std::vector<float>& p, float alpha = 0.85f ) +inline constexpr std::string_view kPowerShellPathPrependScope = "in PowerShell, for this window +inline std::string powerShellPathPrependHint( std::string_view programDir ) noexcept +HOT_FILES = ( "src/pagerank.cpp", # the power-iteration loop — G2's no-allocation scope "src/infra/radixSort.h", # LSD radix entry points "src/infra/radixSort.inl", # the counting … [line truncated: 76 more bytes on this line] +inline std::string powerShellSingleQuote( std::string_view s ) noexcept +inline std::string path_prepend_hint( std::string_view dir ) +handle_t leftmost_leaf() const +inline void discloseUniformChurnFallback( bool hasChurnEvidence, bool stubbed, const char* verbLabel, const std::string& windowStamp ) +inline std::string doctorWhichRipwire( rw::os::stat_t& st ) +inline std::string doctorPathPrependLine( const std::string& selfPath ) +std::uint32_t testIterationCeiling() noexcept +inline const char* rankByLegendFor( const char* label ) noexcept +… [156 more display lines; full output is 22218 bytes on 72 raw line(s)] +````` + +## `./build/ripwire . --pack-signatures --top-k=10` + +*Body-elided decl skeletons — recounted on this corpus. Measured as element bytes: the signature+doc elements --pack-signatures emits, against the SAME symbols' full bodies from --expand, with the CORPUS-ROOT PREFIX SUBTRACTED FROM BOTH SIDES. That subtraction is the whole methodology and the figure is meaningless without it: the root repeats inside every element's id= and p=, it is not what this verb elides, and counting it makes the headline a function of how deep the checkout happens to sit on disk — on one corpus, three spellings of the same root read 18.6 points apart before the subtraction and agree exactly after it. Root-neutralised on THIS repo: 88.8% fewer bytes at top-10, 86.2% at top-50, 87.4% at top-100 (re-derived 2026-10-10 when the C++ standard-member gate stopped std::vector/std::string calls such as out.push_back() and s.empty() from binding to in-tree namesakes by name: the ranked top-10/50 lost one-line container accessors (svector::push_back, WidePath::c_str, every empty) and gained one-line forwarding helpers and macros (kindIs, ASSUME, formatTo, emitTo) whose signature is nearly the whole body, so less of the set is elidable, from 92.7/88.7/87.6; before that, re-derived 2026-09-20 when the native Windows port landed: src/infra/os_win32.cpp and os_win32_logic.h are ~3,900 lines of new, heavily commented infra that the ranked top-10/50/100 now reach, and their bodies are this ratio's DENOMINATOR, so the figure rises without --pack-signatures eliding anything new, from top-50 87.1. Measured by test/showcasecapturecheck.sh's own recount arm on the combined tree, which is the gate that would otherwise report the drift; before that, re-derived 2026-09-17 at the self-check macro vocabulary rename: VERIFY/VERIFY_TEXT/VERIFY_DEBUG_ONLY/VERIFY_NOT_REACHED/VERIFY_SAME_THREAD/VERIFY_NO_ALIAS*/DYNMAP_VERIFY/DEGRADED_PATH_ALERT renamed to ASSUME/EXPECTS/ENSURES/DASSERT/UNREACHABLE/ASSUME_SAME_THREAD*/ASSUME_NO_ALIAS*/DYNMAP_ASSUME/DISCLOSE (plus new VALIDATE sites) across 839 identifier renames in 179 files, and a hand-written Diagnostics.h/diagnostics.cpp replacing the old ones outright: this moves both WHICH symbols the ranked top-10/50/100 hold and how large their bodies are, from top-50 85.2. A real re-derivation of a corpus that changed, not a tolerance edit; previously re-derived 2026-09-12 when the rows dropped the path-repeating id= for the short sc= scope: the signature side shrank, the bodies did not; before that, 89.5% / 81.8% / 84.3% re-derived 2026-09-10 at the sibs= cap raise: kMaxExpandSibs went 8 -> 100, so --expand's bodies now carry the file context the old cap hid — 89.3% of all sibling names — and the body side is this ratio's DENOMINATOR, so the figure rises without --pack-signatures eliding anything new. Measured on a fixed tree with the top-50 membership and the signature side unchanged: top-50 from 71.0. A real re-derivation of a corpus that changed, not a tolerance edit; previously re-derived 2026-09-09 at the printf-family -> std::print conversion: converting ~1,500 emitter call sites to rw::emitTo/emitRaw/formatTo across 93 files changes how large the ranked symbols' BODIES are, and the body side is this ratio's denominator — top-50 from 72.3. A real re-derivation of a corpus that changed, not a tolerance edit; previously re-derived 2026-09-08 at the confident-zero round, issues #62/#63/#66: that change adds symbols to src/graphlegend.h and the new src/preprocdead.h and re-homes two long comment blocks from call sites onto the helpers they explain, which moves both WHICH symbols the ranked top-50 holds and how large their bodies are — top-50 from 74.0. A real re-derivation of a corpus that changed, not a tolerance edit; previously re-derived 2026-09-06 at the stranger-audit fix round: the doctor, cache-sweep and html-provenance bodies grew this corpus's BODY side, moving top-50 from 75.6 — a real re-derivation, not a tolerance edit; before that, re-derived 2026-09-05 at the capture-audit close: lane L7's P16 caps --expand's sibs= at 8 names, which SHRINKS the body side of this ratio and moved the figure down from 84.5/80.2/80.6 — the V1 2026-08-15 re-center, when sibs=/inc= first grew the body side from 70.0/61.0/63.8, in reverse; both were real re-derivations, not tolerance edits). top-50 is the number to quote, because the sigs payload is top-50 regardless of --top-k and is therefore what THIS command emits. A single small/trivial body can still invert it (signature+doc bigger than the body), like the --format=columnar sibling below. test/showcasecapturecheck.sh (C) re-derives all three from this repo every run, in the same quantity, and fails if the caption and the recount drift apart.* + +**wall time: 1.54s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [145 more display lines; full output is 15018 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --outline=rankGraphTeleport --top-k=0` + +*Control-flow skeleton of one symbol, payload-only via the new --top-k=0.* + +**wall time: 1.41s** + +````` + + + + +& p, float alpha = 0.85f ) +{ + PROFILE_SCOPE_DESCRIBE( "rankGraph: PageRank (power iteration)" ); + const std::vector pw = biasPrior( g, p ); + const std::size_t N = pw.size(); + std::vector teleport( pw.begin(), pw.end() ); + std::vector rankDouble( N, 0.0 ); + PageRankRun run{}; // an N == 0 graph never enters the kernel: { 0, converged } — see PageRankRun + if( N ) + { + ... + } + std::vector r( N, 0.f ); + std::transform( rankDouble.begin(), rankDouble.end(), r.begin(), []( double value ) { return float( value ); } ); + return { std::move( r ), run.iterationCount, run.hasConverged }; +} +]]> +````` + +## `./build/ripwire . --outline=rankGraphTeleport:1-10 --top-k=0` + +*CHANGED: a line range on --outline is now STRIPPED with a stderr note (it used to refuse).* + +**wall time: 1.51s** + +````` + + + + +& p, float alpha = 0.85f ) +{ + PROFILE_SCOPE_DESCRIBE( "rankGraph: PageRank (power iteration)" ); + const std::vector pw = biasPrior( g, p ); + const std::size_t N = pw.size(); + std::vector teleport( pw.begin(), pw.end() ); + std::vector rankDouble( N, 0.0 ); + PageRankRun run{}; // an N == 0 graph never enters the kernel: { 0, converged } — see PageRankRun + if( N ) + { + ... + } + std::vector r( N, 0.f ); + std::transform( rankDouble.begin(), rankDouble.end(), r.begin(), []( double value ) { return float( value ); } ); + return { std::move( r ), run.iterationCount, run.hasConverged }; +} +]]> +````` + +stderr: + +````` +ripwire: --outline=rankGraphTeleport:1-10: --outline has no line-range form — outlining the whole symbol (use --expand=rankGraphTeleport:1-10 for a body slice) +````` + +## `./build/ripwire . --expand=rankGraphTeleport --top-k=0` + +*Full body + inline callee signatures.* + +**wall time: 1.29s** + +````` + + + + +: files the ranked rows show / do not (sum = files=), each named or counted below. : a program entry by its language's convention (C/Go/Rust main, Java/C# static main, a script main its module scope calls, __main__.py), callees as c rows; manifest entries not read. : n= unshown files under dir p=; f= names its code files, the rest (tests, docs, config, test/doc/example dirs) counted. declined=K: K call sites without edges, including inert Elixir quote sites after unquote/unquote_splicing re-entry. external=K: K calls proven outside the tree, no edge. locality_pinned=K: K calls pinned by locality alone (a guess). extent_suspect_syms=K: K defs failed containment, corpus-wide. macro_blanked_files=K: K files indexed from a macro-blanked re-parse. prov=scip|binding|import|split|final-segment: how that edge bound (absent: one unique name); split = one arm of an amb= pick; final-segment = a qualified type matched by last name only. files=/symbols=: files and symbols indexed; edges= distinct call edges; shown= symbols printed, a merged row counting each def; ambiguous= calls split over several defs, corpus-wide; unresolved= calls with in-tree evidence and no edge (every def language-filtered or unreachable, or binding refused); order= rows by rank (important-first, important-last; (auto:fill) = flipped past a size threshold) or by path (stable). skipped_oversize=K: K files over a size ceiling, not indexed. unindexed=ext:N: N text files of that extension no grammar reads (6 extensions at most). unindexed_exts=E: E such extensions in all, the list cut. : a non-test row an indexed test transitively reaches (absent otherwise, never 0). : that callee matched by name alone (receiver unproven); it does NOT mean the edge is false. sc=: enclosing scope; the full id is p::sc::n (p= of the row or its ) and selectors take it. amb=K: K calls split over several defs. locals_floor=1: locals= is a floor. role=hub: in= is 8 or more. s in= out= cx= ccx= loc= params= nest= cbo= amp=: callers, callees, cyclomatic/cognitive complexity, lines, parameters, nesting depth, coupled types, callers + co-changed files. ev=/ev_why=: essential complexity (2+: jumps block extract-method; absent: 1) / the jumps behind it, tag:count. ev_floor=1: ev= is a FLOOR; noreturn calls, macro-hidden exits and unresolved gotos are unseen. layer=: built-in arch layer (game|infra|render|math|audio|ai|test) from a dir name in p=; absent if none. l=N: start line; only on a same-name overload's row, one row per body (bodyless decls fold into overloads=). --> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [89 more display lines; full output is 10648 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --deps` + +*File->file dependency graph (god-files, cycles).* + +**wall time: 1.23s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [894 more display lines; full output is 23865 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --hotspots` + +*Complexity x recent git churn (maintenance pain).* + +**wall time: 4.77s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [14 more display lines; full output is 5768 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --clones` + +*Token-normalized duplicate bodies.* + +**wall time: 3.00s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [308 more display lines; full output is 16216 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --cochange` + +*Files that change together in git (hidden coupling).* + +**wall time: 4.10s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +````` + +## `./build/ripwire . --hotspots --since="2 weeks ago"` + +*Hotspots scoped to RECENT churn (the regression lens).* + +**wall time: 4.47s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [13 more display lines; full output is 5133 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --arch=test/archfix/rules.txt` + +*Enforce layering rules (exit 2 on violation) — run against the repo's own test fixture rules.* + +**wall time: 1.04s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [809 more display lines; full output is 117717 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --lint` + +*Built-in AST checks (c-cast, goto, unsafe-c-fn, ...).* + +**wall time: 2.29s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [685 more display lines; full output is 70655 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --lint-rules=test/lintrulesfix/rules` + +*User lint rules (YAML, ast-grep style) from a directory.* + +**wall time: 1.21s** + +````` + + + + +use LOG() instead of printf +use LOG() instead of printf +use LOG() instead of printf +use LOG() instead of printf +use LOG() instead of printf + +````` + +stderr: + +````` +ripwire: lint-rules: test/lintrulesfix/rules/malformed.yaml:5: expected 'key: value' — file skipped +[math degraded] lint-rules: malformed rule file skipped (lintrules.h:399, auto rw::parseLintRuleFile(const std::string &, std::string_view, std::vector &)::(anonymous class)::operator()(std::size_t, const char *) const — logged once per site) +ripwire: AST query did not compile for any grammar: (this_is_not_a_real_node @x @@@ ((( ) +````` + +## `./build/ripwire . --lint --with-profile=report.txt` + +*Join MEASURED heat onto --lint findings — runs in a tiny fabricated demo corpus (one cache-pointer-chase-loop finding under a PROFILE_SCOPE site) because a real report needs a RIPWIRE_PROFILE build; the finding inside the profiled scope gains heat_* columns from the report's #PROF_TSV row.* + +Input file: + +````` +#PROF_TSV_BEGIN one row per scope, aggregated across threads; counters are RAW integers +scope file line calls total_ms l1d_mpki +walk: chase pass x.cpp 9 12 48.500 7.250 +#PROF_TSV_END +````` + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [13 more display lines; full output is 3428 bytes on 1 raw line(s)] +````` + +The joined finding — past the display cut above, extracted so the join is visible: + +````` +p = p->next +````` + +## `./build/ripwire . --communities` + +*Cluster the call graph into cohesive modules.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [189 more display lines; full output is 16887 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --zoom` + +*Nested module hierarchy (multi-level Louvain) + cross-module bridges — levels_shown="2" of levels= BY DEFAULT over the 40 largest top modules (~8 KB, where the whole tree is ~220 KB); a module AT the cut carries children=.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [135 more display lines; full output is 7371 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --zoom --zoom-levels=3` + +*The restoring knob: --zoom-levels=N prints N levels (0 = the whole tree); here three, ~11 KB.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [263 more display lines; full output is 12069 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --report` + +*Architecture summary (modules, god-files, cycles) as markdown.* + +````` + + +# ripwire architecture report + +3102 files · 29597 symbols · 28731 edges · 3498 modules (13251 call-graph isolated) + +Root: `.` + +Call-graph isolate provenance: 3612 declaration, 2119 header, 2754 source, 4766 document; 0 connected Louvain singletons + +## Modules (call-graph clusters; showing 12 of 3498) +- **src::emitTo@infra/emit.h:53:2760 [write,run,emit]** — 1519 symbols +- **src::kindIs@infra/nodekind.h:46:3271 [push,emit,collect]** — 497 symbols +- **src::DISCLOSE@infra/Diagnostics.h:616:43369 [read,compute,resolve]** — 333 symbols +- **docs::`--top-k=N`@COMMANDS.md:59:9077 [pack,allow,insert]** — 181 symbols +- **test/ownerhopfix/py/pkg::big_handler_001@broadcast.py:3:72** — 106 symbols +- **src/infra::fail@os_win32.cpp:63:2553 [open,get,close]** — 89 symbols +- **test::no@estchargecheck.sh:52:4108 [skip,set]** — 63 symbols +- **src::rowOffsets@infra/sparseCsr.h:248:8990 [connect,compute,expand]** — 55 symbols +- **src::sliceKindIs@slice.h:368:24239 [match,collect,push]** — 48 symbols +- **src/infra::DYNMAP_ASSUME@dynamic_map.hpp:90:4411 [insert,erase,find]** — 44 symbols +- **src::child@ingest_valuerefs.h:387:20429 [emit,open]** — 44 symbols +- **src::tokText@defectshape.h:128:5308 [scan,find,join]** — 41 symbols + +## God files (most depended-on; showing 10 of 222) +- `src/model.h` — 97 dependents +- `src/infra/emit.h` — 87 dependents +- `src/infra/Diagnostics.h` — 63 dependents +- `src/serialize.h` — 43 dependents +- `src/graph.h` — 42 dependents +… [35 more lines, 3954 bytes total] +````` + +## `./build/ripwire . --seams` + +*Cross-module call seams no test reaches. NOW carries seam_pairs/shown/capped.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [60 more display lines; full output is 9253 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --mermaid` + +*Module (directory) dependency graph as a Mermaid diagram.* + +````` +%% ripwire --mermaid: module (directory) dependency graph — node = dir (symbol count), edge = inter-module calls (>= 3). Render at mermaid.live. +flowchart LR + subgraph sg0 ["src"] + n101["src
8908"] + n102["src/infra
1297"] + end + subgraph sg1 ["test"] + n103["test
5414"] + n483["test/legochargefix
546"] + n394["test/fixtures/recallpassage
314"] + n568["test/ownerhopfix/py/pkg
158"] + n338["test/expandmodefix
151"] + n340["test/expandsibsfix
149"] + n709["test/rubyschemafix
99"] + n413["test/forbodyfoldfix/py
98"] + n691["test/rubyattrsfix
85"] + n540["test/massfix
77"] + end + n73["docs
847"] + n2[".github
621"] + n0["(root)
606"] + subgraph sg5 ["bench"] + n4["bench
545"] + n5["bench/agentloop
331"] + n49["bench/locbench/results/r5_pooling
237"] + n50["bench/locbench/results/r6_expansion
187"] + n59["bench/recalleval
113"] + n43["bench/locbench
107"] + n38["bench/headtohead/r3-headroom-2026-08-03
102"] + n65["bench/skillrater/results/2026-09-07/arms
100"] +… [19 more lines, 1572 bytes total] +````` + +## `./build/ripwire . --owners` + +*Bus-factor: recency-weighted author ownership per file.* + +**wall time: 3.53s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [941 more display lines; full output is 102656 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --dead-code=src` + +*Internal functions with no caller found in the index — a name-based graph reading, not a confidence score. NOTE the filter is a path-COMPONENT match: 'src' matches any .../src/... segment; use ./src to pin the root directory.* + +````` + + + + +````` + +## `./build/ripwire . --exercises=test/regression.sh` + +*Which symbols a TEST FILE exercises — the reverse direction of --affected.* + +````` + + + +````` + +## `./build/ripwire . --community=0` + +*Drill into ONE call-graph community by id — the drill= the --communities output itself advertises.* + +````` + + + +````` + +## `./build/ripwire . --quality-delta` + +*On a CLEAN tree: nothing got worse, exit 0. The gating shape is in the sandbox section below.* + +**wall time: 19.70s** + +````` + + +````` + +stderr: + +````` +ripwire: no ./.ripwire_quality_baseline — auto-comparing the working tree vs git HEAD (commit the baseline with --quality-baseline to pin it) +````` + +## `./build/ripwire . --quality-delta --show-stale` + +*The same clean verdict with what it folded printed back: every stale acknowledgement as an row and the full header. The regression-free answer above keeps only stale= and stale_by_kind= (and stale_next= names this call), and folds only when that saves a quarter of the bytes. Refused without --quality-delta.* + +**wall time: 19.69s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [137 more display lines; full output is 16053 bytes on 1 raw line(s)] +````` + +stderr: + +````` +ripwire: no ./.ripwire_quality_baseline — auto-comparing the working tree vs git HEAD (commit the baseline with --quality-baseline to pin it) +````` + +## `./build/ripwire . --edit-check=rankGraphTeleport` + +*Fast per-symbol post-edit contract check vs git HEAD (unchanged on a clean tree).* + +**wall time: 3.54s** + +````` + + + + + + + + + +````` + +## `./build/ripwire . --pr-context` + +*No-LLM review-evidence bundle for the working-tree diff (clean tree = empty).* + +**wall time: 2.31s** + +````` + + + +````` + +## `./build/ripwire . --pr-context=HEAD~1` + +*The BASEREF form: diffed against merge-base(BASEREF, HEAD), never the ref tip — here the previous commit on the current line (a ref with NO merge base falls back to a disclosed two-dot diff: anchor="ref-tip-two-dot").* + +**wall time: 6.25s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + +````` + +## `./build/ripwire . --merge-scout=HEAD~2,HEAD~1` + +*Pairwise cross-arm conflict sites + suggested landing order (any committish sharing a merge base with HEAD works as an arm; one that does not is reported ok="0", never compared).* + +**wall time: 15.28s** + +````` + + + + + + + + + + + +````` + +## `./build/ripwire . --stray-content=lane/` + +*Which refs of the `lane/` ref family still hold divergent authored work vs HEAD, with verdicts.* + +**wall time: 26.66s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [239 more display lines; full output is 31494 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --stray-content=arm/for-howitworks-067-f2add` + +*A second selection, the one branch `arm/for-howitworks-067-f2add` (no ref family left to select on this checkout), picked at capture time from the refs this checkout really has and never the selection above: merged refs are OMITTED from the rows and counted in merged=; refs sharing no merge base with HEAD (a shallow clone, or a pre-rewrite history) land in unknown= with ok="0" — the counters always reconcile against refs=.* + +**wall time: 4.75s** + +````` + + + + + + + + + + + + + + + + + + +````` + +## `./build/ripwire . --stray-content=lane/ --plan` + +*Select the genuinely-unmerged refs of the `lane/` ref family and feed them to merge-scout for a landing order (a fully merged selection yields an empty landing set — still a measurement, disclosed on the root).* + +**wall time: 98.83s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [32413 more display lines; full output is 4509251 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --stray-content=zzzz-no-such-ref --plan` + +*A --plan filter that selects NO ref REFUSES (exit 1) naming the substring — before the wave-3 close this fell through to the '>512 refs match' sentence, and --abi under the same filter answered an empty measurement at exit 0.* + +**exit code: 1** — **wall time: 3.76s** + +````` +(empty) +````` + +stderr: + +````` +ripwire: --plan: --stray-content=zzzz-no-such-ref matches no local ref — a zero here would be a failure, not a measurement + (the filter is a substring match against refs/heads names; run bare --stray-content to list them, e.g. --stray-content=feat/) +````` + +## `./build/ripwire . --stray-content=lane/ --abi` + +*Cross-branch ABI-break gate over the `lane/` ref family: struct byte-contract drift on each ref's AUTHORED paths — exit 2 when any drift row is found (the only kind that gates), 0 when the compared refs are clean, and exit 1 if the --stray-content filter matches no ref at all.* + +**exit code: 2** — **wall time: 204.22s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [13 more display lines; full output is 3078 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --whereis=rankGraphTeleport` + +*Which ref's tree defines or mentions SYM — HEAD first, then every local branch. The default lists the kind="def" rows (here the first 60, most on branches, each carrying tip= and date=) and counts the references in : on this symbol HEAD's references would fill the 60-row cap before the branch definitions arrive, so the whole list (below) shows fewer definitions, and the default serves the page that shows more of them, whatever its bytes.* + +**wall time: 13.66s** + +````` + + + + + + + + + + + + + + + + + + + + +````` + +## `./build/ripwire . --whereis=rankGraphTeleport --whereis-listing=all` + +*The whole hit list asked for by name: under the same 60-row cap HEAD's references fill the list, so it shows far fewer definitions (here HEAD's one) than the default above (here 60). The default serves the all page only when it shows the same definitions as the defs page in strictly fewer bytes (a tie serves it too).* + +**wall time: 14.15s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [36 more display lines; full output is 13361 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --quality-delta=HEAD~1..HEAD --quality-kinds=complexity,dead-code` + +*A ref range run for two of the twelve kinds only: the named kinds' rows are exactly their rows in the full answer, the header names the others (kinds_skipped=, skipped_why="selector"), and a pass no named kind needs does not run. The replay a memory-stopped range answer hands back (kinds_next=) is this flag. Refused without --quality-delta=A..B.* + +**wall time: 14.04s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [35 more display lines; full output is 14374 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --whereis=computeOnePairOverlap --with-history` + +*Same, plus a git-history row (never / removed-by-commit) for names no tree carries.* + +**wall time: 23.31s** + +````` + + + + + + + +````` + +## `./build/ripwire . --flags` + +*The dark-content dashboard: gates BUILT but OFF. CHANGED: no longer invents gates from comments/heredocs, so the count only reflects real ifndef/define, CMake option(), and getenv gates.* + +**wall time: 1.82s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [388 more display lines; full output is 23815 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --flags --flip=RIPWIRE_ASAN` + +*Blast radius of turning ONE gate on: live code, symbols, transitive reach, covering tests.* + +**wall time: 1.70s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +````` + +## `./build/ripwire . --flags --flip=RIPWIRE_ASA` + +*Unknown-gate refusal (exit 1) with a did-you-mean from a real edit distance (one character off RIPWIRE_ASAN).* + +**exit code: 1** — **wall time: 1.46s** + +````` +(empty) +````` + +stderr: + +````` +ripwire: --flip: no gate named 'RIPWIRE_ASA' in . (did you mean 'RIPWIRE_ASAN'?) +ripwire: run `ripwire . --flags` for the gate table +````` + +## `./build/ripwire . --plan-lanes=3 --task="add a --since filter to the doc-drift verb and cover it with tests"` + +*NEW VERB: pre-hoc lane plan — which of 3 parallel worktrees would COLLIDE, before a line is written. JSON on stdout.* + +**wall time: 5.53s** + +````` +{"v":1,"verb":"plan-lanes","at":"eb541409e","root":".","task":"add a --since filter to the doc-drift verb and cover it with tests","source":"partition","requested":3,"lane_count":3,"claim_key":"path+scope+name","on_conflict":"producing-lane-rebases","corpus":{"files":3102,"symbols":29597,"edges":287 … [line truncated: 338 more bytes on this line] +"symbols":[{"p":"./src/docdrift.h","n":"DriftResult","scope":"DriftResult","l":286,"id":"./src/docdrift.h::DriftResult::DriftResult"}, +{"p":"./src/docdrift.h","n":"computeDocDrift","scope":"docdrift","l":2441,"id":"./src/docdrift.h::docdrift::computeDocDrift"}, +{"p":"./src/docdrift.h","n":"kDocDriftLegend","scope":"docdrift","l":2739,"id":"./src/docdrift.h::docdrift::kDocDriftLegend"}, +{"p":"./src/docdrift.h","n":"writeDocDriftPage","scope":"docdrift","l":2909,"id":"./src/docdrift.h::docdrift::writeDocDriftPage"}, +{"p":"./src/mcpverbs.h","n":"docDriftText","scope":"rw","l":655,"id":"./src/mcpverbs.h::rw::docDriftText"}, +{"p":"./src/verbs_change.h","n":"runDocDrift","scope":"","l":1678,"id":null}]},"lanes":[{"id":"lane-0","task":"add a --since filter to the doc-drift verb and cover it with tests","claims":{"symbols":[{"p":"./src/verbs_change.h","n":"runAbiCheck","scope":"","key":"183da9aaf74a4a92","id":null,"id_addr … [line truncated: 113 more bytes on this line] +{"p":"./src/editcheck.h","n":"editCheckPriceRoot","scope":"rw","key":"33219bb04158a6a8","id":"./src/editcheck.h::rw::editCheckPriceRoot","id_addressable":true,"id_collides_with":0,"l":1202,"ord":0,"overloads":1,"amb":0,"cx":8,"ccx":10,"churn":55,"tested":0}, +{"p":"./src/docdrift.h","n":"writeGateability","scope":"docdrift","key":"42c456dfb55faee4","id":"./src/docdrift.h::docdrift::writeGateability","id_addressable":true,"id_collides_with":0,"l":2685,"ord":0,"overloads":1,"amb":2,"cx":6,"ccx":6,"churn":36,"tested":0}, +{"p":"./src/darkflags.h","n":"writeFlags","scope":"darkflags","key":"5c2c4a2b311b8dba","id":"./src/darkflags.h::darkflags::writeFlags","id_addressable":true,"id_collides_with":0,"l":1298,"ord":0,"overloads":1,"amb":1,"cx":8,"ccx":8,"churn":32,"tested":0}, +{"p":"./src/docdrift.h","n":"docDriftNextAttr","scope":"docdrift","key":"7723ed789a1c9c09","id":"./src/docdrift.h::docdrift::docDriftNextAttr","id_addressable":true,"id_collides_with":0,"l":2864,"ord":0,"overloads":1,"amb":0,"cx":6,"ccx":5,"churn":36,"tested":0}, +{"p":"./src/docdrift.h","n":"recordWeakDisclosures","scope":"docdrift","key":"972779210e49c0cf","id":"./src/docdrift.h::docdrift::recordWeakDisclosures","id_addressable":true,"id_collides_with":0,"l":2430,"ord":0,"overloads":1,"amb":0,"cx":2,"ccx":1,"churn":36,"tested":0}, +{"p":"./src/testmap.h","n":"scriptGatesUnmodelledCount","scope":"rw","key":"98c4487e2e9e08bc","id":"./src/testmap.h::rw::scriptGatesUnmodelledCount","id_addressable":true,"id_collides_with":0,"l":1344,"ord":0,"overloads":1,"amb":0,"cx":4,"ccx":4,"churn":48,"tested":0}, +{"p":"./src/main.cpp","n":"scanReportVerbPrecedence","scope":"","key":"9ab981e987e8108e","id":null,"id_addressable":false,"id_collides_with":0,"l":3031,"ord":0,"overloads":1,"amb":0,"cx":8,"ccx":11,"churn":451,"tested":0}, +{"p":"./src/pageview.h","n":"secondaryCutAttrs","scope":"rw","key":"a10ea8d3bdca1dfb","id":"./src/pageview.h::rw::secondaryCutAttrs","id_addressable":true,"id_collides_with":0,"l":399,"ord":0,"overloads":1,"amb":0,"cx":3,"ccx":2,"churn":28,"tested":0}, +{"p":"./src/main.cpp","n":"emitNoteAddUnprovenDefs","scope":"","key":"a950332ccfce6b8d","id":null,"id_addressable":false,"id_collides_with":0,"l":691,"ord":0,"overloads":1,"amb":1,"cx":2,"ccx":1,"churn":451,"tested":0}, +{"p":"./src/docdrift.h","n":"sortDocsByLiveDrift","scope":"docdrift","key":"d5de682411b9a9a9","id":"./src/docdrift.h::docdrift::sortDocsByLiveDrift","id_addressable":true,"id_collides_with":0,"l":2390,"ord":0,"overloads":1,"amb":0,"cx":2,"ccx":2,"churn":36,"tested":0}, +{"p":"./src/wrap.h","n":"wrapEmitCliFirst","scope":"rw","key":"f5ef05d9574d9516","id":"./src/wrap.h::rw::wrapEmitCliFirst","id_addressable":true,"id_collides_with":0,"l":699,"ord":0,"overloads":1,"amb":7,"cx":10,"ccx":5,"churn":52,"tested":0}], +"files":[{"p":"./src/darkflags.h","symbols":1,"churn":32,"ccx":8,"hotspot_rank":53}, +{"p":"./src/docdrift.h","symbols":4,"churn":36,"ccx":14,"hotspot_rank":35}, +{"p":"./src/editcheck.h","symbols":1,"churn":55,"ccx":10,"hotspot_rank":41}, +{"p":"./src/main.cpp","symbols":2,"churn":451,"ccx":12,"hotspot_rank":4}, +{"p":"./src/pageview.h","symbols":1,"churn":28,"ccx":2,"hotspot_rank":127}, +{"p":"./src/testmap.h","symbols":1,"churn":48,"ccx":4,"hotspot_rank":39}, +{"p":"./src/verbs_change.h","symbols":1,"churn":80,"ccx":9,"hotspot_rank":23}, +{"p":"./src/wrap.h","symbols":1,"churn":52,"ccx":5,"hotspot_rank":66}]},"blast_radius":{"reaches":49,"files_total":17,"capped":false,"files":["./src/darkflags.h","./src/docdrift.h","./src/editcheck.h","./src/editplan.h","./src/editpreview.h","./src/flipimpact.h","./src/main.cpp","./src/mcp.h","./src … [line truncated: 189 more bytes on this line] +"tests_total":0,"tests_capped":false,"tests_granularity":"claimed-symbols","untested":49,"module_span":1,"notes":[], +"execution":{"policy":"codex-lane/v1","model":"gpt-5.6-sol","reasoning":"high","rule":"complex-or-wide","basis":"structural-only","signals":{"claims":12,"files":8,"module_span":1,"max_ccx":11,"sum_ccx":64,"ambiguous_calls":13,"blast_reaches":49,"contract_touches":0,"conflicts":0,"untested":49,"tests … [line truncated: 135 more bytes on this line] +{"id":"lane-1","task":"add a --since filter to the doc-drift verb and cover it with tests","claims":{"symbols":[{"p":"./src/cli.h","n":"kViewFlags","scope":"rw","key":"2a2f2487082cc6d8","id":"./src/cli.h::rw::kViewFlags","id_addressable":true,"id_collides_with":0,"l":3326,"ord":0,"overloads":1,"amb" … [line truncated: 42 more bytes on this line] +{"p":"./src/main.cpp","n":"kJsonRideAlongFlags","scope":"","key":"2a90806fa5a580c1","id":null,"id_addressable":false,"id_collides_with":0,"l":3243,"ord":0,"overloads":1,"amb":0,"cx":0,"ccx":0,"churn":451,"tested":0}, +… [72 more display lines; full output is 20559 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --plan-lanes --brief=/aux/lanes_brief.txt` + +*NEW VERB, explicit form: one line per lane, lane boundaries are the ones you wrote (the defensible mode).* + +**wall time: 5.16s** + +Input file: + +````` +add a --since filter to the doc-drift verb +add the CLI parse arm and help text for the new filter +write regression tests for the new filter +````` + +````` +{"v":1,"verb":"plan-lanes","at":"eb541409e","root":".","task":null,"source":"brief","requested":3,"lane_count":3,"claim_key":"path+scope+name","on_conflict":"producing-lane-rebases","corpus":{"files":3102,"symbols":29597,"edges":28731,"ambiguous":2911,"unresolved":14064},"carve":null,"core":{"files" … [line truncated: 4 more bytes on this line] +"symbols":[]},"lanes":[{"id":"lane-0","task":"add a --since filter to the doc-drift verb","claims":{"symbols":[{"p":"./src/docdrift.h","n":"recordUnchecked","scope":"docdrift","key":"12641dab14abc8fd","id":"./src/docdrift.h::docdrift::recordUnchecked","id_addressable":true,"id_collides_with":0,"l":2 … [line truncated: 72 more bytes on this line] +{"p":"./src/docdrift.h","n":"sortWeakGroupsByPath","scope":"docdrift","key":"1944ba506280ff80","id":"./src/docdrift.h::docdrift::sortWeakGroupsByPath","id_addressable":true,"id_collides_with":0,"l":2401,"ord":0,"overloads":1,"amb":0,"cx":1,"ccx":0,"churn":36,"tested":0}, +{"p":"./src/mcpverbs.h","n":"docDriftText","scope":"rw","key":"1fa68e8d93c05a59","id":"./src/mcpverbs.h::rw::docDriftText","id_addressable":true,"id_collides_with":0,"l":655,"ord":0,"overloads":1,"amb":0,"cx":1,"ccx":0,"churn":435,"tested":0}, +{"p":"./src/cli.h","n":"kViewFlags","scope":"rw","key":"2a2f2487082cc6d8","id":"./src/cli.h::rw::kViewFlags","id_addressable":true,"id_collides_with":0,"l":3326,"ord":0,"overloads":1,"amb":0,"cx":0,"ccx":0,"churn":508,"tested":0}, +{"p":"./src/recall.h","n":"docFileMask","scope":"rw","key":"3149a219f599664c","id":"./src/recall.h::rw::docFileMask","id_addressable":true,"id_collides_with":0,"l":110,"ord":0,"overloads":1,"amb":0,"cx":4,"ccx":4,"churn":27,"tested":0}, +{"p":"./src/docdrift.h","n":"writeDocDriftPage","scope":"docdrift","key":"380b7de5df1cfd73","id":"./src/docdrift.h::docdrift::writeDocDriftPage","id_addressable":true,"id_collides_with":0,"l":2909,"ord":0,"overloads":1,"amb":10,"cx":14,"ccx":15,"churn":36,"tested":0}, +{"p":"./src/docdrift.h","n":"computeDocDrift","scope":"docdrift","key":"3b19cc3d8996c3b2","id":"./src/docdrift.h::docdrift::computeDocDrift","id_addressable":true,"id_collides_with":0,"l":2441,"ord":0,"overloads":1,"amb":1,"cx":26,"ccx":38,"churn":36,"tested":0}, +{"p":"./src/docdrift.h","n":"DriftResult","scope":"DriftResult","key":"422ab39546b6e0bd","id":"./src/docdrift.h::DriftResult::DriftResult","id_addressable":true,"id_collides_with":0,"l":286,"ord":0,"overloads":1,"amb":0,"cx":0,"ccx":0,"churn":36,"tested":0}, +{"p":"./src/verbs_change.h","n":"runDocDrift","scope":"","key":"904831e5c0f66b39","id":null,"id_addressable":false,"id_collides_with":0,"l":1678,"ord":0,"overloads":1,"amb":1,"cx":5,"ccx":4,"churn":80,"tested":0}, +{"p":"./src/ensemble.h","n":"kEnsembleChurnSince","scope":"ensemble","key":"a34fc78c05bf56a8","id":"./src/ensemble.h::ensemble::kEnsembleChurnSince","id_addressable":true,"id_collides_with":0,"l":120,"ord":0,"overloads":1,"amb":0,"cx":0,"ccx":0,"churn":23,"tested":0}, +{"p":"./src/mcpverbs.h","n":"unknownSubVerbRefusal","scope":"rw","key":"e336d39b0a6addea","id":"./src/mcpverbs.h::rw::unknownSubVerbRefusal","id_addressable":true,"id_collides_with":0,"l":5758,"ord":0,"overloads":1,"amb":0,"cx":3,"ccx":2,"churn":435,"tested":0}, +{"p":"./src/docdrift.h","n":"kDocDriftLegend","scope":"docdrift","key":"ff2ba74637bbbebf","id":"./src/docdrift.h::docdrift::kDocDriftLegend","id_addressable":true,"id_collides_with":0,"l":2739,"ord":0,"overloads":1,"amb":0,"cx":0,"ccx":0,"churn":36,"tested":0}], +"files":[{"p":"./src/cli.h","symbols":1,"churn":508,"ccx":0,"hotspot_rank":6}, +{"p":"./src/docdrift.h","symbols":6,"churn":36,"ccx":54,"hotspot_rank":35}, +{"p":"./src/ensemble.h","symbols":1,"churn":23,"ccx":0,"hotspot_rank":103}, +{"p":"./src/mcpverbs.h","symbols":2,"churn":435,"ccx":2,"hotspot_rank":5}, +{"p":"./src/recall.h","symbols":1,"churn":27,"ccx":4,"hotspot_rank":63}, +{"p":"./src/verbs_change.h","symbols":1,"churn":80,"ccx":4,"hotspot_rank":23}], +"symbols_total":3358,"symbols_capped":true},"blast_radius":{"reaches":13,"files_total":7,"capped":false,"files":["./src/docdrift.h","./src/main.cpp","./src/mcp.h","./src/mcpserver.h","./src/mcpverbs.h","./src/recall.h","./src/verbs_for.h"]},"tests_to_run":[], +"tests_total":0,"tests_capped":false,"tests_granularity":"claimed-symbols","untested":13,"module_span":7,"notes":[], +"execution":{"policy":"codex-lane/v1","model":"gpt-5.6-sol","reasoning":"xhigh","rule":"wide-contract-work","basis":"structural-only","signals":{"claims":12,"files":6,"module_span":7,"max_ccx":38,"sum_ccx":64,"ambiguous_calls":12,"blast_reaches":13,"contract_touches":1,"conflicts":1,"untested":13,"t … [line truncated: 139 more bytes on this line] +{"id":"lane-1","task":"add the CLI parse arm and help text for the new filter","claims":{"symbols":[{"p":"./scripts/optremarks.py","n":"main","scope":"","key":"01b3b880f77d1512","id":null,"id_addressable":false,"id_collides_with":124,"l":279,"ord":0,"overloads":1,"amb":0,"cx":23,"ccx":31,"churn":14, … [line truncated: 12 more bytes on this line] +{"p":"./src/cli.h","n":"HelpLine","scope":"rw","key":"1489844dbc310b82","id":"./src/cli.h::rw::HelpLine","id_addressable":true,"id_collides_with":0,"l":2947,"ord":0,"overloads":1,"amb":0,"cx":0,"ccx":0,"churn":508,"tested":0}, +{"p":"./src/cli.h","n":"kViewFlags","scope":"rw","key":"2a2f2487082cc6d8","id":"./src/cli.h::rw::kViewFlags","id_addressable":true,"id_collides_with":0,"l":3326,"ord":0,"overloads":1,"amb":0,"cx":0,"ccx":0,"churn":508,"tested":0}, +{"p":"./src/cli.h","n":"HelpFilter","scope":"HelpFilter","key":"3103a5e8bfc33d38","id":"./src/cli.h::HelpFilter::HelpFilter","id_addressable":true,"id_collides_with":0,"l":3026,"ord":0,"overloads":1,"amb":0,"cx":0,"ccx":0,"churn":508,"tested":0}, +{"p":"./docs/docs_commands_build.py","n":"main","scope":"","key":"4015853681ded3bc","id":null,"id_addressable":false,"id_collides_with":124,"l":865,"ord":0,"overloads":1,"amb":0,"cx":19,"ccx":28,"churn":17,"tested":0}, +{"p":"./src/mcprefusal.h","n":"kMcpValueFields","scope":"rw::mcprefuse","key":"4a7106e488a2aa80","id":"./src/mcprefusal.h::rw::mcprefuse::kMcpValueFields","id_addressable":true,"id_collides_with":0,"l":304,"ord":0,"overloads":1,"amb":0,"cx":0,"ccx":0,"churn":47,"tested":0}, +{"p":"./src/cli.h","n":"serveHelpSelector","scope":"rw","key":"4b247ee064fc4e94","id":"./src/cli.h::rw::serveHelpSelector","id_addressable":true,"id_collides_with":0,"l":3091,"ord":0,"overloads":1,"amb":1,"cx":4,"ccx":3,"churn":508,"tested":0}, +{"p":"./src/query.h","n":"tryParsePredicateOnAll","scope":"query","key":"786acc4dbfec612b","id":"./src/query.h::query::tryParsePredicateOnAll","id_addressable":false,"id_collides_with":1,"l":113,"ord":0,"overloads":2,"amb":0,"cx":1,"ccx":0,"churn":20,"tested":0}, +… [62 more display lines; full output is 18945 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --plan-lanes=99 --task=x` + +*Out-of-range refusal shape for the lane count.* + +**exit code: 1** + +````` +(empty) +````` + +stderr: + +````` +ripwire: --plan-lanes=99 is out of range — N must be 2..16 (1 is not a fan-out) +````` + +## `./build/ripwire . --layout=Symbol` + +*CPU/GPU contract view of one struct: computed offsets/sizes/padding + mirror check.* + +**wall time: 1.06s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +````` + +## `./build/ripwire . --layout=Lang` + +*The honest refusal (exit 1): Lang is an `enum class`, not a struct — no offsets are fabricated.* + +**exit code: 1** — **wall time: 1.12s** + +````` +(empty) +````` + +stderr: + +````` +ripwire: --layout: 'Lang' is an enum, --layout models structs (a scoped/unscoped enum's underlying type is not a byte layout) +````` + +## `./build/ripwire . --doc-drift` + +*Which of this repo's doc claims are now false. CHANGED: row attribute at= renamed to tgt= (at= is now only the root sha stamp).* + +**wall time: 2.98s** + +````` + + + +
+ + + + + + + + + + + + +
+ + + + + + + + + + + + + +… [227 more display lines; full output is 23874 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --doc-drift --gateability` + +*The finishable to-do list: docs whose LIVE failing anchors a date-stamp would reclassify.* + +**wall time: 2.95s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [253 more display lines; full output is 25072 bytes on 1 raw line(s)] +````` + +Tail of the same output — the `` section: + +````` + + + + + + + + + + + + + + + + + + + + + + + + + +````` + +## `./build/ripwire . --doc-drift --with-history` + +*Same report, with git history splitting stale mentions into deleted-by-commit vs never-existed.* + +**wall time: 2.63s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [207 more display lines; full output is 23853 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --from-trace=-` + +*Map a pasted stack trace onto indexed symbols. CHANGED: in_corpus= now reports the real count (was 0).* + +Input file: + +````` +AddressSanitizer:DEADLYSIGNAL +================================================================= +==41337==ERROR: AddressSanitizer: SEGV on unknown address 0x000000000018 (pc 0x000102f4a1c8 bp 0x00016d2f1a40 sp 0x00016d2f19e0 T0) + #0 0x102f4a1c8 in rw::rankGraphTeleport(Graph const&, std::vector const&, float) src/graph.h:10134 + #1 0x102f3e884 in rw::rankGraph(Graph const&, float) src/graph.h:10175 + #2 0x102e11f30 in runDefaultMap(MainDispatch const&) src/main.cpp:1753 + #3 0x102e01a44 in main src/main.cpp:4174 + #4 0x1a2b3c0dc in start+0x9dc (dyld:arm64e+0x60dc) +==41337==ABORTING +````` + +````` + + + + + + + + + + + +PageRank with an explicit teleport / personalization vector p (Σp = 1). The prior is name-quality-biased through biasPrior() so all rank modes share one weighting seam; the transition matrix (edgesinline RankedGraph rankGraphTeleport( const Graph& g, const std::vector<float>&am … [line truncated: 31 more bytes on this line] + +uniform-teleport PageRank (the defaultinline RankedGraph rankGraph( const Graph& g, float alpha = 0.85f ) +int runDefaultMap( const MainDispatch& d ) +int main( int argc, char** argv ) + + + +& p, float alpha = 0.85f ) +{ + PROFILE_SCOPE_DESCRIBE( "rankGraph: PageRank (power iteration)" ); + const std::vector pw = biasPrior( g, p ); + const std::size_t N = pw.size(); + std::vector teleport( pw.begin(), pw.end() ); + std::vector rankDouble( N, 0.0 ); + PageRankRun run{}; // an N == 0 graph never enters the kernel: { 0, converged } — see PageRankRun + if( N ) + { + double teleportMass = 0.0; +… [18 more display lines; full output is 4915 bytes on 29 raw line(s)] +````` + +## `./build/ripwire . --notes` + +*List all field notes (write-side memory) — the committed .ripwire_notes at the repo root, each with the sha/branch it was recorded at.* + +````` + + + + + + + + + + + gate scripts' claim (~line 1305) is NOT enforced — the derived-vs-stated sibling loop here covers docs/EVALS.md only. It drifted 407→451 unnoticed (fixed 2026-08-23). To close: grep both files ('file:line:' parsing) in the gateCountClaims arm.]]> + + + + +````` + +## `./build/ripwire . --pack-task="add a new output format flag to the CLI"` + +*ONE budget-shared bundle: ranking + top bodies + caller sigs + notes + tests_to_run. CHANGED: rows now carry n=/id=.* + +**wall time: 1.07s** + +````` + + + + +Print the authoritative CLI usage and flag catalog to the caller-provided output streaminline void printUsage( std::FILE* out ) noexcept + +claude's `mcp add … -- ripwire --mcp`: the flag goes on the end of that command line, ahead of its newlineinline std::string wrapCliAddPost( const AgentTarget& row, std::string_view toolsArg ) + +ONE recipe path for every agent that can shell out, and the point is that the RECOMMENDATION does not vary by agent — only the MCP alternative does. Before this there were four near-identical CLI-fiinline void wrapEmitCliFirst( const AgentTarget& row, const std::string& token, c … [line truncated: 83 more bytes on this line] + +mcp-tools pass-through. Written into the server command only where its argument shape is known to be one more plain argument: the JSON args arrays (cursor/windsurf/gemini), opencode's command arrayinline bool wrapWritesToolsArg( const AgentTarget& row ) noexcept + +L8 (train 29): the NEXT PAGE of a callers listing — the CLI --callers root's next=, find_referencing_symbols' next and find_symbol's calledBy_next all name this one call, so the three cannot spell tinline std::string callHierarchyPageNext( bool wantCallers, std::string_vi … [line truncated: 98 more bytes on this line] + +F7: a hostile/garbled frame line number (e.g. a fuzzed or truncated trace) can exceed UINT32_MAX; unchecked `v*10+d` wraps mod 2^32 (4294967297 -> 1), which then confidently maps to a REAL line in theinline std::uint32_t toUint( std::string_view s, bool& overflowed ) noexcept + + + + + + + + +… [47 more display lines; full output is 10054 bytes on 38 raw line(s)] +````` + +## `./build/ripwire . --pack-task="add a new output format flag to the CLI" --partition=3` + +*Fan-out form: one shared core + 3 per-agent slices carved along call-graph communities.* + +**wall time: 1.38s** + +````` + wraps a ; bundle role=core|partition i= symbols= modules= bytes= tokens=: one agent's ctx, symbols= ids assigned, bytes= its size; tokens= = est_tokens= = bytes/2.36 (flat, the densest rate; the inner ctx est_tokens= is language-weighted). Inner ctx task= root= budget_tokens= dropped_positive=: the task, p= base, the slice's token target, ranked candidates its budget cut. of_top=: rows ranked (far) / bodies (callers); s rel=caller|callee shared=: 1-hop edge, bodies reached (absent at 1). window: shown= total= capped= (capped=1 cut). run_unknown=1: no runner derivable (a guess would be worse). est_tokens=: price as emitted (an upper bound under compact). : rank N in this ranking, rows in r= order. : cyclomatic/cognitive complexity. : N callers in the index (absent: not measured). route=: the ranker: name-exact(X) = the task names symbol X (anchors: its evidence), subtoken+body = conceptual BM25 (:broad = 1-2 plain words, plain rg may also win; :declined(...) = a name hit refused as a common name). sc=: enclosing scope; the full id is p::sc::n (p= of the row or its ) and selectors take it. next=: the one pasteable follow-up. pure=1: const/constexpr signature (Swift: non-mutating) and no transitive side effect found; a hint. partitions=/requested=: partitions carved / asked for. modules=/split=: call-graph groups found / cuts forced to split one. core_symbols=/surface=: ids in the shared core / core plus the assignable remainder. budget_per_agent_tokens=: one agent's budget, core_budget_tokens= plus partition_budget_tokens=. total_bytes=N: bytes of all bundles together. overlap_mean=/overlap_max=: pairwise Jaccard over the ids partitions name, before trimming. shared_symbols=/union_symbols=: ids two or more partitions name / ids any partition names. core_overlap=: share of the core surface a partition reaches anyway. layer=: built-in arch layer (game|infra|render|math|audio|ai|test) from a dir name in p=; absent if none. --> + + + + + +Print the authoritative CLI usage and flag catalog to the caller-provided output streaminline void printUsage( std::FILE* out ) noexcept + +claude's `mcp add … -- ripwire --mcp`: the flag goes on the end of that command line, ahead of…inline std::string wrapCliAddPost( const AgentTarget& row, std::string_view toolsArg ) + +ONE recipe path for every agent that can shell out, and the point is that the RECOMMENDATION doe…inline void wrapEmitCliFirst( const AgentTarget& row, const std::string& token, const std::string_view executablePath, const WrapListing& listing ) noexcept + +mcp-tools pass-through. Written into the server command only where its argument shape is known t…inline bool wrapWritesToolsArg( const AgentTarget& row ) noexcept + + + + + +inline bool printUsageTier( std::FILE* out, HelpTier tier, std::string_view want ) noexcept + + + + + + + + +NodeId order. Renumbering an otherwise-identical graph (the exact "same probe file, different so…inline constexpr float kScoreTieAbsEps = 1e-6f + +Render one form of the disclosure. Empty string whenever there is nothing to say — no power it…inline std::string renderDisclosure( const RankDisclosure& d, DiscloseAs as ) + +The PageRank power iteration itself — the numeric kernel every ranked document's order comes f…PageRankRun pageRankDouble( const sparseCsr<float>& inEdges, std::span<const double> weightedOutDegree, std::span<const double> teleport, std::span<double> r … [line truncated: 10 more bytes on this line] +inline std::string rankByText( const std::string& root, std::string_view mode, int topK, bool stable = false ) +struct RankedGraph +inline constexpr const char* kChurnRankLegend = "<!-- rank_by=churn: k= is PageRank re-run with the teleport BIASED by git CHANGE-FREQUENCY over window= " "(a c… +inline std::uint32_t sliceRdIterationCeiling() noexcept +struct PageRankRun + +What a ranked document discloses about the power iteration that ordered it. `isPageRank == false…struct RankDisclosure +inline std::uint32_t navRelevanceWeight( const Graph& g, NodeId n ) noexcept +inline RankedGraph rankGraphTeleport( const Graph& g, const std::vector<float>& p, float alpha = 0.85f ) +inline constexpr std::string_view kPowerShellPathPrependScope = "in PowerShell, for this window +inline std::string powerShellPathPrependHint( std::string_view programDir ) noexcept +HOT_FILES = ( "src/pagerank.cpp", # the power-iteration loop — G2's no-allocation scope "src/infra/radixSort.h", # LSD radix entry points "src/infra/radixSort.inl", # the counting … [line truncated: 76 more bytes on this line] +inline std::string powerShellSingleQuote( std::string_view s ) noexcept +inline std::string path_prepend_hint( std::string_view dir ) +handle_t leftmost_leaf() const +inline void discloseUniformChurnFallback( bool hasChurnEvidence, bool stubbed, const char* verbLabel, const std::string& windowStamp ) +inline std::string doctorWhichRipwire( rw::os::stat_t& st ) +inline std::string doctorPathPrependLine( const std::string& selfPath ) +std::uint32_t testIterationCeiling() noexcept +inline const char* rankByLegendFor( const char* label ) noexcept +… [124 more display lines; full output is 18961 bytes on 10 raw line(s)] +````` + +## `./build/ripwire . --export=cc.json:/aux/ripwire2.cc.json` + +*Per-file metrics as CodeCharta cc.json.* + +**wall time: 2.50s** + +````` +(empty) +````` + +Artifact written: + +````` + 475571 /aux/ripwire2.cc.json +{"projectName":"project","apiVersion":"1.3","attributeDescriptors":{"loc":{"title":"Lines of Code","description":"Physical line count","direction":-1},"symbols":{"title":"Symbols","description":"Definitions in the file","direction":-1},"cx":{"title":"Cyclomatic Complexity","description":"Sum of per-symbol cyclomatic complexity","direction":-1},"cognitive_cx":{"title":"Cognitive Complexity","descri +````` + +## `./build/ripwire . --batch=/aux/batch2.txt` + +*One-turn sweep: 4 newline-delimited verb:arg sub-queries answered in ONE deduped .* + +**wall time: 4.36s** + +Input file: + +````` +for:incremental cache invalidation +callers:rankGraphTeleport +grep:DISCLOSE +lego:Vehicle +````` + +````` + + + + + + + + + +incremental cache (--cache): per-file content hash + raw facts so a re-run re-parses ONLY c…constexpr std::uint32_t kCacheMagic = 0x4b505443 + +Composed exactly the way every OTHER blob family is (quality.h): one fixed-width identity hex pe…inline std::string spanTierMemoPath( const std::string& diskPath ) + +Ingest the tree at `sha`, materialized out of `root`'s object store. The HEAD side reuses the SA…inline bool ingestCommitTree( const std::string& root, const std::string& sha, const std::vector<std::string>& excludes, std::size_t maxFileBytes, IngestResult&… … [line truncated: 4 more bytes on this line] + +P1-15 — the `_reingest` envelope field for a response whose handling ran an INCREMENTAL pass, …inline bool mcpRefreshedThisRequest( std::uint64_t passesAtEntry ) +struct FileHealth +inline std::vector<std::vector<NodeId>> legoImplementorsOnSurface( const IngestResult& ing, const std::vector<std::vector<NodeId>>& implementors, const std::ve … [line truncated: 8 more bytes on this line] +inline std::pair<Snapshot, bool> computeHeadSnapshot( const std::string& root, const std::string_view* cacheNever = nullptr, std::size_t maxFileBytes = kDefault… +inline RawFacts runParsePool( IngestResult& result, const char* rootDir, std::string_view cacheFile, bool captureValueUses, HashMap<std::string, FileFacts>& cache, const CacheLoadStats& cac … [line truncated: 66 more bytes on this line] +inline McpRebuildBaseline mcpRebuildBaseline( const McpIndex& ix, bool isIncrementalPass ) +inline const McpIndex& getIndex( const std::string& root ) +inline std::string mcpFreshFields( std::uint64_t passesAtEntry ) +struct McpIndex +inline std::string receiptNextFor( const std::string& fileIdentity, const std::string& symbolName, const std::string& foldJson, const std::string& firstTestRun ) +IngestResult ingest( const char* rootDir, const std::vector<std::string>& excludeSubstr, std::string_view cacheFile, std::size_t maxFileBytes, bool captureValueUses, std::string_view excludeLabel, bool … [line truncated: 37 more bytes on this line] +const char* cacheArtifactVerdict( const std::string& path, bool captureValueUses ) +static bool cachePathIsDirectory( const std::string& cachePath ) +inline std::string oracleCachePath( const std::string& root, const std::string& headSha ) +inline constexpr std::array<std::string_view, 8> kCacheRuleNames = +… [106 more display lines; full output is 50524 bytes on 1 raw line(s)] +````` + + +--- + +# self-diagnosis + +## `./build/ripwire . --doctor` + +*Environment self-check: binary staleness, grammars, cache dir, git, tracked-binary staleness — exit 1 when any check fails (here: the PATH install is older than ./build).* + +**exit code: 1** — **wall time: 21.49s** + +````` + + + + + + + + + + +````` + + +--- + +# security + +## `./build/ripwire --scan-skill=skills/ripwire-orient/SKILL.md` + +*Scan a single skill file for injection/exfiltration patterns before installing.* + +````` + + + +````` + +stderr: + +````` +ripwire scan: 0 finding(s) in skills/ripwire-orient/SKILL.md +````` + +## `./build/ripwire --scan-skills=skills` + +*Scan a whole skills directory (exit 2 = CRITICAL, 1 = WARN). Explicit-DIR form only.* + +````` + + + +````` + +stderr: + +````` +ripwire scan: 0 finding(s) total (26 skill file(s) scanned, 0 unscannable file(s) skipped, 0 denylisted subtree(s) not descended) +````` + + +--- + +# knobs / modes + +## `./build/ripwire . --rank-by=churn --top-k=5` + +*Rank by git change-frequency prior instead of PageRank.* + +**wall time: 3.65s** + +````` + + + + + + + + + + + + + + + + + + + + + +````` + +## `./build/ripwire . --rank-by=churn-decay --since=HEAD~6 --exclude=test --exclude=docs --exclude=skills --in=src --limit=3` + +*Scope the recent-changes answer to ONE directory. The global block stays byte-identical, a second page follows it — n=/of= are its counts (of= IS the total, so the paging half carries no total=), capped="1" has_more="1" next_offset= offset= limit= page it, and next= replays THIS run's own corpus flags (--since/--exclude) so the page it names is a page of the same answer. The symbol map collapses to a disclosed stub — the map was not asked for and was not ranked at all, which is why the header carries no pr_iters=. merge_bombs_skipped= stays on the global block: it counts the window's skipped commits, not the directory's.* + +**wall time: 4.94s** + +````` + + + + + + + + + + + + + + + + + + +````` + +## `./build/ripwire . --rank-by=bogus --top-k=5` + +*An unknown value REFUSES (exit 1), NAMED, with the supported set listed.* + +**exit code: 1** + +````` +(empty) +````` + +stderr: + +````` +ripwire: --rank-by: unknown value 'bogus' (supported: pagerank|authority|hub|rrf|churn|churn-decay) +````` + +## `./build/ripwire . --callers=rankGraphTeleport --format=columnar` + +*Columnar output: paths table + parallel arrays, ~15-60% fewer tokens on MANY-row lists — small results can be LARGER (the columnar legend is a fixed cost).* + +**wall time: 1.03s** + +````` + + +0=src/mcpindex.h 1=src/graph.h 2=src/eval.h 3=src/main.cpp + +0,1,1,2,3,3,3 +getIndex,rankGraph,anchoredLexicalRank,runEval,churnDecayRanking,churnRankedGraph,runDefaultMap +1184,10173,10819,171,1425,1464,1751 +fn,fn,fn,fn,fn,fn,fn +0,0,0,0,0,0,0 +src/mcpindex.h:1288,src/graph.h:10176,src/graph.h:10870,src/eval.h:328,src/main.cpp:1439,src/main.cpp:1478 src/main.cpp:1479 src/main.cpp:1493,src/main.cpp:1871 + + +````` + +## `./build/ripwire . --for="cache invalidation" --format=candidates --top-k=5` + +*Flat top-K export for an external reranker.* + +**wall time: 2.78s** + +````` + + + +inline std::string spanTierMemoPath( const std::string& diskPath ) + + +inline std::vector<std::vector<NodeId>> legoImplementorsOnSurface( const IngestResult& ing, const std::vector<std::vector<NodeId>>& implementors, const std::vector<NodeId>& surfaceIds ) + + +### See the map — not just the numbers + + +inline std::string receiptNextFor( const std::string& fileIdentity, const std::string& symbolName, const std::string& foldJson, const std::string& firstTestRun ) + + +const char* cacheArtifactVerdict( const std::string& path, bool captureValueUses ) + + +````` + +## `./build/ripwire . --callers=rankGraphTeleport --format=bogus` + +*An unknown --format value REFUSES (exit 1), named, with the supported set listed.* + +**exit code: 1** + +````` +(empty) +````` + +stderr: + +````` +ripwire: --format: unknown value 'bogus' (supported: xml|columnar|rows|candidates) +````` + +## `./build/ripwire . --callers=rankGraphTeleport --json` + +*Machine-parseable JSON, same content, keys mirror the XML attrs.* + +**wall time: 1.16s** + +````` +{"of":"rankGraphTeleport","defs":1,"count":7,"root":".","hop_tested":0,"hop_untested":7,"graph_ambiguous":2911,"graph_unresolved":14064,"graph_unindexed":266,"counts_floor":true,"callers":[{"t":"fn","n":"getIndex","p":"src/mcpindex.h:1184","sites_at":["src/mcpindex.h:1288"]}, +{"t":"fn","n":"rankGraph","p":"src/graph.h:10173","sites_at":["src/graph.h:10176"]}, +{"t":"fn","n":"anchoredLexicalRank","p":"src/graph.h:10819","sites_at":["src/graph.h:10870"]}, +{"t":"fn","n":"runEval","p":"src/eval.h:171","sites_at":["src/eval.h:328"]}, +{"t":"fn","n":"churnDecayRanking","p":"src/main.cpp:1425","sites_at":["src/main.cpp:1439"]}, +{"t":"fn","n":"churnRankedGraph","p":"src/main.cpp:1464","sites_at":["src/main.cpp:1478","src/main.cpp:1479","src/main.cpp:1493"]}, +{"t":"fn","n":"runDefaultMap","p":"src/main.cpp:1751","sites_at":["src/main.cpp:1871"]}]} +````` + +## `./build/ripwire . --hotspots --json` + +*JSON refusal shape: an unsupported verb refuses loudly instead of silently falling back to XML.* + +**exit code: 1** + +````` +(empty) +````` + +stderr: + +````` +ripwire: --json is not yet supported for --hotspots — supported: the default map, --for, --pack-task, --callers/--callees, --impact, --quality-delta, --test-gate, --metrics, and --plan-lanes which is JSON-native (e.g. ripwire

--callers=SYM --json) +````` + +## `./build/ripwire . --hotspots --limit=3 --offset=3` + +*Pagination: 3 items, skipping the first 3 (deterministic seams).* + +**wall time: 3.38s** + +````` + + + + + + + +````` + +## `./build/ripwire . --ignore-tests --top-k=5` + +*Drop test paths from the corpus before ranking.* + +**wall time: 1.13s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [59 more display lines; full output is 7832 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --exclude=present --exclude=bench --top-k=5` + +*Drop matching paths (repeatable) before ranking.* + +**wall time: 1.37s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [56 more display lines; full output is 7937 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --map-diff --top-k=5` + +*Full map re-ranked with teleport toward git-changed files — clean tree, so changed=0 and it degrades to the plain map.* + +**wall time: 2.23s** + +````` + + + + + + + + + + + + + + + + + + + + + +````` + +## `./build/ripwire . --no-cache --top-k=3` + +*Force a cold parse (bypass the warm TMPDIR cache) — shows the cold-vs-warm cost.* + +**wall time: 2.79s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [55 more display lines; full output is 7834 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --cache=/aux/warm2.ripwirecache --top-k=3` + +*Explicit incremental cache at a path OUTSIDE the repo (first call writes it).* + +**wall time: 3.02s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [55 more display lines; full output is 7834 bytes on 1 raw line(s)] +````` + +Artifact written: + +````` + 26937586 /aux/warm2.ripwirecache +````` + +## `./build/ripwire . --max-file-size=8K --top-k=3` + +*Skip files above a size bound before parsing (note the corpus shrink in the header).* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [17 more display lines; full output is 4224 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --scip=does_not_exist.scip --callers=rankGraphTeleport` + +*SCIP overlay with a missing index REFUSES (exit 1) naming the file — never silently serves the name-based map you named a precision index to improve on (it used to degrade in silence).* + +**exit code: 1** + +````` +(empty) +````` + +stderr: + +````` +ripwire: --scip=does_not_exist.scip: cannot open the index — refusing rather than serving the name-based map you named a precision index to improve on (generate one with scip-clang/scip-python, or drop --scip) +````` + +## `./build/ripwire src test --top-k=5` + +*Multi-root workspace: ONE merged graph over two roots, paths labeled /.* + +**wall time: 1.30s** + +````` + + + + + + + + + + + + + + + + + + + + + + + +````` + +## `./build/ripwire . --eval` + +*Self-eval: co-change recall vs BM25.* + +**wall time: 21.02s** + +````` +ripwire --eval (co-change recovery, averaged over 80 historical commits) + ranker recall@5 recall@10 recall@20 + ripwire 3.2% 6.2% 12.7% + BM25 9.7% 12.1% 12.8% + BM25sub 17.3% 21.6% 30.0% + BM25body 14.0% 19.9% 35.1% + fused 8.8% 18.2% 24.1% + anchored 14.0% 19.9% 35.1% + same-dir 0.0% 5.3% 8.5% + random 0.2% 0.3% 0.6% <- floor (random ranking over F=3102 files) + note: `ripwire` here is the DEFAULT MAP's structural-only PageRank (importance, not + relatedness) — it is NOT what a --for/--query retrieval call ranks with. BM25 / + BM25sub / BM25body are QUERY-TIME lexical rankers (whole-name / subtoken / + subtoken+body); fused = RRF(ripwire, BM25sub); anchored = BM25body + anchored PPR + expansion (--for --anchor, EXPERIMENTAL). The SHIPPED default for --for/--query is + the subtoken+body lexical family (routed to name-exact only for an identifier-shaped + query — lexical.h chooseForRanker), so a gap between the ripwire and BM25* rows here + is structural-importance-vs-lexical-relatedness on a co-change task, not the shipped + retrieval path losing to an alternative it was never running. +````` + +## `./build/ripwire . --eval-retrieval` + +*Known-item retrieval eval: MRR + recall@k per ranker per query mode.* + +**wall time: 147.90s** + +````` +ripwire --eval-retrieval (known-item, 4004 doc-commented symbols; gold is in-corpus by construction) + sample: population=7488 scored=4004 rule=smallest-key CAPPED — a SUBSET, not the population + (smallest fnv1a64(scope::name) over the population, cut on the key so an identity is never split; + path- and order-independent, but a corpus this size is NOT graded exhaustively — say so when citing it) + ingest: lex=rich (persisted subtoken stats; no per-query corpus re-tokenize) + ranker query-mode MRR recall@1 recall@5 recall@10 + subtoken name 0.680 54.6% 84.6% 90.0% + subtoken doc-phrase 0.909 88.9% 92.8% 93.8% + name-exact name 0.897 81.9% 95.9% 97.3% + name-exact doc-phrase 0.014 0.6% 1.9% 2.9% + anchored name 0.685 56.1% 83.8% 88.5% + anchored doc-phrase 0.904 88.1% 92.8% 93.5% + routed name 0.899 82.1% 96.1% 97.5% + routed doc-phrase 0.907 88.8% 92.6% 93.5% + note: routing chose name-exact on 3992/4004 NAME queries (a NAME query is always identifier-shaped); + the confidence gate routes doc-phrase queries to name-exact ONLY when EVERY content word names a symbol + (or an explicit camel/snake token appears) AND every matched name is specific enough to anchor on — + a common name (many definitions, or a subtoken carried by many symbol names) declines the route — so + conceptual prose falls back to subtoken+body; routed tracks the better ranker on BOTH modes + (routed==name-exact on name, ~=subtoken+body on doc-phrase). +````` + +## `./build/ripwire . --eval-stray=/aux/stray_labels2.tsv` + +*Labelled verdict-accuracy eval for --stray-content — three labels over REAL local refs (names resolved at capture time); exit 3 when accuracy is under the floor. Read got= against v= in the stray-content run: a ref the verb could not analyse (unknown) must never be credited as a merged hit.* + +**exit code: 3** — **wall time: 39.99s** + +Input file: + +````` +# refverdict labels for --eval-stray (the first three local branches, resolved at capture time; a missing branch is padded with a nonexistent name on purpose) +arm/for-howitworks-067-f2add merged +arm/for-howitworks-067-placebo unmerged +arm/orient-map-067-narrow merged +````` + +````` + + + + + + +````` + +## `./build/ripwire skills --eval-skills=/aux/skills_labels2.tsv` + +*Labelled skill-ROUTING eval over the repo's own skills/ directory (4 hand-labelled prompts).* + +Input file: + +````` +orient in an unfamiliar codebase fast ripwire-orient judged +who calls this function and what is the blast radius ripwire-navigate judged +plan parallel worktrees so the lanes do not collide ripwire-change-check judged +what is the weather in Paris none neg +````` + +````` +ripwire --eval-skills (skill routing over K=16 candidate skills [ripwire-router excluded]; 3 positive + 1 negative prompts; corpus '/aux/skills_labels2.tsv'; split test=4 dev=0) + arm hit@1 hit@2 mrr sep-auc fire/abstain@ORACLE-th (upper bound) + overlap 66.7% 66.7% 0.689 0.000 50.0% (th=-1.000) + name 33.3% 66.7% 0.537 0.667 50.0% (th=0.000) + bm25-desc 66.7% 66.7% 0.689 0.667 75.0% (th=2.865) + bm25-full 33.3% 66.7% 0.611 1.000 50.0% (th=0.426) + for-routed 33.3% 33.3% 0.556 1.000 50.0% (th=0.663) + random 6.2% 12.5% 0.211 0.500 <- floor (uniform-random ranking; auc 0.5 by definition) + provenance hit@1 (bm25-desc): router 0/0, desc 0/0, judged 2/3 (desc rows quote the descriptions - expect them easiest; judged is the honest number) + judged-only hit@1 per arm: overlap 2/3, name 1/3, bm25-desc 2/3, bm25-full 1/3, for-routed 1/3 + router-magnet: with ripwire-router ADMITTED as a candidate it takes top-1 on 1/3 positive prompts (bm25-desc arm) - why it is excluded above + per-skill (bm25-desc): name / permitted-rows / won / pos-fires / false-fires / neg-fires + ripwire-before-you-build 0 0 1 1 0 + ripwire-change-check 1 0 0 0 0 + ripwire-find-bug 0 0 0 0 0 + ripwire-fresh-eyes 0 0 0 0 1 + ripwire-graph-query 0 0 0 0 0 + ripwire-handoff 0 0 0 0 0 + ripwire-layers 0 0 0 0 0 + ripwire-mcp 0 0 0 0 0 + ripwire-navigate 1 1 1 0 0 + ripwire-opt-remarks 0 0 0 0 0 + ripwire-orient 1 1 1 0 0 + ripwire-perf-target 0 0 0 0 0 + ripwire-quality-bar 0 0 0 0 0 + ripwire-reuse-first 0 0 0 0 0 + ripwire-security-scan 0 0 0 0 0 + ripwire-write-tests 0 0 0 0 0 + misses (overlap): + line 3 want=ripwire-change-check got=ripwire-before-you-build "plan parallel worktrees so the lanes do not collide" +… [19 more lines, 3665 bytes total] +````` + +## `./build/ripwire wrap claude` + +*Print the recipe to wire ripwire into Claude Code as an MCP server.* + +````` +# ripwire -> Claude Code (CLI-first) +# RECOMMENDED — this agent can run shell commands, so call the CLI directly. It costs +# nothing until you invoke it, and it reads CLAUDE.md, so the paste block below IS the wiring: +ripwire . --for="" +# +# ...every follow-up call answers with the compact legend by default (terse definitions of only the +# attributes the answer carries; the payload is byte-identical either way). Add --legend=full when a +# definition's reasoning is needed -- `ripwire --help` carries the measured saving (one place, +# gate-held) -- this line does not repeat it, because two copies of a number is one that goes stale: +# ripwire . --callers=SYM --legend=full +# +# ALTERNATIVE — register the MCP server instead, for a warm index across calls: +claude mcp add ripwire -- ripwire --mcp +# verbs the agent can then call mid-task (33 total): +# read: analyze, rank_by, find_symbol, find_referencing_symbols, grep, cochange, memory_recall, situational_awareness, mentions, for, lego, owners, fetch_body, batch, flags, doc_drift, slice +# flagship reflex: exemplar, quality_delta, quality_baseline, impact, uses, affected, path_between, connect, explore, from_trace, edit_check, whereis, stray_content +# edit: replace_symbol_body, insert_before_symbol, insert_after_symbol +bash skills/install.sh # deploy to ${CLAUDE_CONFIG_DIR:-~/.claude}/skills (drift-gated) +bash skills/install.sh --hook # RECOMMENDED: advisory Read/Grep -> ripwire CLI nudge + session primer (opt-in, never blocks) +# +# context wiring — a binary on PATH is invisible to an agent until its rules file says when +# to reach for it. Paste the block below into CLAUDE.md: +# --- paste into CLAUDE.md --- +## ripwire — deterministic codebase maps (on PATH as `ripwire`) +Reach for it BEFORE blind grep + whole-file reads. First call ~1s cold; after that warm, ~0.1s. +- Orient on a task: `ripwire --for=""` — ranked, quality-annotated + signatures. Paste symbol/file names from the issue verbatim; named mentions get anchored. +- One task: `--pack-task="" --legend=compact`; before parallel agents: `--plan-lanes=N --task=""`, then read `lanes[].execution`. +- Have a stack trace / build error: `ripwire --from-trace=FILE --legend=compact` (`-` = stdin) — + paste the error, don't paraphrase it into a query. +… [14 more lines, 4058 bytes total] +````` + +## `./build/ripwire --version` + +*Version + short build info.* + +````` +ripwire 0.6.8 (dev, AppleClang 21.0.0.21000101, emit=std::print, built_from=eb541409e) +````` + + +--- + +# navigate — seeds, claims, slices, shapes + +## `./build/ripwire . --at=src/graph.h:10134` + +*Hold a LOCATION, not a name: the enclosing-definition chain at FILE:LINE (a compiler error, a diff hunk, a stack frame), outermost -> innermost.* + +**wall time: 1.20s** + +````` + + + + +````` + +## `./build/ripwire . --callers=@src/graph.h:10134` + +*The same seed in a SELECTOR position: @FILE:LINE resolves to the innermost enclosing definition, then --callers runs on it.* + +**wall time: 1.06s** + +````` + + + + + + + + + + +````` + +## `./build/ripwire . --at=src/graph.h:999999` + +*A seed past the end of the file — the refusal shape for a faulted location.* + +**exit code: 1** + +````` +(empty) +````` + +stderr: + +````` +ripwire: the at flag's seed 'src/graph.h:999999' named no location (./src/graph.h has only 14470 lines — the seed asked for line 999999) +````` + +## `./build/ripwire . --verify="calls(runDefaultMap, rankGraphTeleport)"` + +*VERIFY a closed claim in one call: three-valued verdict (confirmed / refuted / not-established) with the evidence rows inline.* + +**wall time: 1.05s** + +````` + + + + + +````` + +## `./build/ripwire . --verify="unused(rankGraphTeleport)"` + +*A claim that is FALSE — the refuted shape, with the references that refute it.* + +````` + + + + + + + + + + + + +````` + +## `./build/ripwire . --verify="contains(src/graph.h, \"no such literal anywhere\")"` + +*A literal-scan absence: refuted only with complete= evidence, never on a partial scan.* + +````` + + + +````` + +## `./build/ripwire . --verify="frobnicate(x)"` + +*An unparseable claim — the refusal names the accepted shapes.* + +**exit code: 1** — **wall time: 1.20s** + +````` +(empty) +````` + +stderr: + +````` +ripwire: --verify claim not recognized: 'frobnicate(x)' — unknown shape word. The claim language is CLOSED; the shapes are: calls(A, B) does A transitively call B · uses(SYM) is SYM referenced anywhere · unused(SYM) is SYM referenced nowhere · contains(FILE, "LITERAL") do FILE's bytes contain t … [line truncated: 218 more bytes on this line] +````` + +## `./build/ripwire . --slice=rankGraphTeleport` + +*Bare --slice=SYM: the INVENTORY of sliceable locals (), so a caller can pick VAR.* + +**wall time: 2.27s** + +````` + + + + + + + + + + + + + + + + + +````` + +## `./build/ripwire . --slice=rankGraphTeleport:teleport` + +*Intra-procedural def-use slice of ONE variable: one row per line touching it, k=def|use|both, reaching definitions flow-sensitive (reach=cfg).* + +**wall time: 2.29s** + +````` + + + + + + + teleport( pw.begin(), pw.end() );]]> + + + + + + + + +````` + +## `./build/ripwire . --slice=rankGraphTeleport:teleport --slice-flow=back --slice-depth=3` + +*TRANSITIVE backward value-flow from the seed variable, bounded BFS (depth= disclosed; a cut frontier says flow_truncated=1).* + +**wall time: 2.61s** + +````` + + + + + + + teleport( pw.begin(), pw.end() );]]> + + + + + + + + + pw = biasPrior( g, p );]]> + + +& p, float alpha = 0.85f )]]> + + +& p, float alpha = 0.85f )]]> + + +````` + +## `./build/ripwire . --slice=rankGraphTeleport:teleport --slice-flow=fwd` + +*Forward flow: which statements the seed's value reaches, at the default depth bound.* + +**wall time: 1.89s** + +````` + + + + + + + teleport( pw.begin(), pw.end() );]]> + + + + + + + + + + + + + + + + + + + + + 0.0 )]]> + + + + + + + + +````` + +## `./build/ripwire . --slice=rankGraphTeleport:nosuchvar` + +*A variable the definition does not bind — the refusal shape, naming the inventory.* + +**exit code: 1** + +````` +(empty) +````` + +stderr: + +````` +ripwire: --slice: no occurrence of 'nosuchvar' in rankGraphTeleport — sliceable locals: alpha, g, p, pw, N, teleport, rankDouble, run, teleportMass, value, inverseMass, r (bare --slice=rankGraphTeleport lists them with first-def lines) +````` + +## `./build/ripwire . --slice-depth=3` + +*--slice-depth without --slice-flow is refused loudly rather than silently ignored.* + +**exit code: 1** + +````` +(empty) +````` + +stderr: + +````` +ripwire: --slice-depth bounds the --slice-flow BFS — pass both (e.g. ripwire --slice=parseArgs:argIndex --slice-flow=fwd --slice-depth=4) +````` + +## `./build/ripwire . --slice=rankGraphTeleport:teleport --legend=compact` + +*The compact legend posture: rows byte-identical, a versioned schema id replaces the repeated explanatory prose — for a many-small-calls loop.* + +**wall time: 1.45s** + +````` + + + + + + + teleport( pw.begin(), pw.end() );]]> + + + + + + + + +````` + +## `./build/ripwire . --pattern='rankGraphTeleport($A, $B, $C)'` + +*Structural search written in CODE: $NAME binds one node; grammars=/shapes= disclose what the pattern became per grammar (a 3-argument call shape — the 2-argument spelling has no call site in this repo and correctly reports hits=0).* + +**wall time: 1.64s** + +````` + +rankGraphTeleport( g, std::vector<float>( N, N ? 1.0f / float( N ) : 0.f ), alpha ) + +````` + +## `./build/ripwire . --pattern='DISCLOSE(...)'` + +*The ellipsis form over a macro-shaped call site; unsupported= names the families this verb does not serve.* + +**wall time: 1.89s** + +````` + +DISCLOSE( result, AbiResult::DisclosureWhy::NoMergeBase, "abi: no merge-base for a ref (unrelated history?) — that ref +DISCLOSE( Diagnostics::answerRefused, "--arch exits 1 with path:line and the reason on stderr; no report is printed", +DISCLOSE( baseline, ArchBaselineRead::DisclosureWhy::SymlinkRefused, "arch: refusing to read the arch baseline sidecar t +DISCLOSE( Diagnostics::answerRefused, "--baseline exits 1: pathguard names the refused link on stderr and the verb says +DISCLOSE( ex, Exclusions::DisclosureWhy::ForHeaderSaturated, "atoms: the for-header exclusion stream spent its whole bud +DISCLOSE( ex, Exclusions::DisclosureWhy::StmtCrementSaturated, "atoms: the statement-crement exclusion stream spent its +DISCLOSE( ex, Exclusions::DisclosureWhy::StmtAssignSaturated, "atoms: the statement-assign exclusion stream spent its wh +DISCLOSE( ex, Exclusions::DisclosureWhy::InnerCommaSaturated, "atoms: the inner-comma exclusion stream spent its whole b +DISCLOSE( st, Type3Stats::DisclosureWhy::PairCapHit, "clones: Type-3 pair cap hit — first N compared (both-gate-surviv +DISCLOSE( scan, CommentCoherenceScan::DisclosureWhy::UnreadableFile, "comment-coherence: an indexed file could not be re +DISCLOSE( st, StreamBlobStats::DisclosureWhy::ListUnwritable, "crossref: cannot write the blob-batch list — cross-bran +DISCLOSE( st, StreamBlobStats::DisclosureWhy::BatchNotStarted, "crossref: git cat-file --batch failed to start — cross +DISCLOSE( st, StreamBlobStats::DisclosureWhy::StreamEndedMidBlob, "crossref: git cat-file stream e +DISCLOSE( dropSink, RefEnumeration::DisclosureWhy::TipNotObjectName, "crossref: for-each-ref yielded a ref whose tip is +DISCLOSE( "crossref: refusing a diff whose revision arguments are not resolved object names" ) +DISCLOSE( sweep, ParallelSweep::DisclosureWhy::WorkerThrew, "crossref: a git worker threw — this shard of the sweep is +DISCLOSE( stats, StreamBlobStats::DisclosureWhy::BatchNotStarted, "crossref: a blob-stream slice was never read — comp +DISCLOSE( plumb, RefPlumbing::DisclosureWhy::RevisionNotObjectName, "crossref: ref tip or HEAD is not a resolved object +DISCLOSE( plumb, RefPlumbing::DisclosureWhy::MergeBaseNotObjectName, "crossref: merge-base returned something that is no +DISCLOSE( plumb, RefPlumbing::DisclosureWhy::NoMergeBase, "crossref: no merge-base for ref (shallow clone or unrelated h +DISCLOSE( "crossref: refusing to list a tree whose revision argument is not a resolved object name" ) +DISCLOSE( "whereis: the index's def sites match no HEAD row (working tree drifted from HEAD?) — keeping the lexical la +DISCLOSE( reads[ k ], BranchBlobReadBack::DisclosureWhy::NotReadBackWhole, "whereis: a blob the pa +DISCLOSE( scan, WorktreeScan::DisclosureWhy::ListingFailed, "whereis: git could not list the working tree's changes — +DISCLOSE( scan, WorktreeScan::DisclosureWhy::OverPathCap, "whereis: more changed paths than the overlay reads — the re +DISCLOSE( scan, WorktreeScan::DisclosureWhy::CopyUnread, "whereis: a changed path could not be read from the working tre +DISCLOSE( Diagnostics::answerUnchanged, "lsTree lists the same rows per ref: only slower", "crossref: +DISCLOSE( out, CMakeScan::DisclosureWhy::SymlinkEscapesRoot, "flags: a CMake file's symlink target leaves the root — f +… [73 more display lines; full output is 17832 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --pattern='x'` + +*A pattern that collapses to a bare token is REFUSED — never reported as hits=0.* + +**exit code: 1** + +````` +(empty) +````` + +stderr: + +````` +ripwire: --pattern: this pattern collapses to a single bare token, which is a TEXT search, not a structural one — a clean parse is not enough (a bare word parses fine in most grammars and means nothing structural). Give it a shape (foo($X), $A + $B, if ($C) { ... }) or use the grep flag for litera … [line truncated: 176 more bytes on this line] +````` + +## `./build/ripwire . --grep=DISCLOSE --and=cache` + +*Boolean grep: hits where BOTH literals share the matched line (--grep-scope=line is the default).* + +````` + + + +inline bool topLevelEvidence( std::string_view source, const TSLanguage* language, const Predicate& predicate ) +The crawl's per-file byte ceiling. A text file larger than this is skipped: at this size it is o…constexpr std::size_t kDefaultMaxFileBytes = 4u * 1024u * 1024u +TSTree* parseTree( TSParser* parser, std::string_view src ) + +Exercise every registered grammar and its embedded query, reporting loaded and expected totalsinline DoctorGrammarProbe doctorProbeGrammars() +struct FileHealth +constexpr std::array<LangEntry, 57> kLangTable = +inline RevSide sliceAtRev( const std::string& root, const std::string& sha, const std::string& rel, const Symbol& sym, SliceFam fam, const ::TSLanguage* grammar…struct SliceScan +enum class AstWalk : std::uint8_t +inline bool astroFenceTailIsBlank( std::string_view src, std::size_t from, std::size_t to ) noexcept +constexpr std::uint32_t kMaxKotlinStringNestDepth = 128u +std::vector<LocalNameFact> collectGatedLocalNames( std::string_view defBytes, std::uint32_t defStartLine, Lang lang ) +inline ModuleScan scanModule( std::string_view source, std::string_view path ) +inline bool hasPhantomScopeSeparator( TSNode qualified ) noexcept +bool jsonNestsTooDeep( std::string_view bytes ) noexcept +constexpr std::uint32_t kMaxYamlNestDepth = 64u +inline bool hasMainGuard( std::string_view source ) +SpanTierBatch spanTiersOfFiles( std::span<const std::string> diskPaths, bool useMemo ) +inline SliceScan sliceScanDefinition( const std::string& src, const Symbol& sym, SliceFam fam, const ::TSLanguage* grammar, std::string_view varName ) +inline void runParseWorker( ParsePoolShared& sh, unsigned t ) +enum class DisclosureWhy : std::uint8_t +struct LintRule +inline std::string buildMemoryStopAttr( const IngestResult& ing, bool json ) +inline bool hasPytestTable( std::string_view source ) +… [137 more display lines; full output is 19568 bytes on 44 raw line(s)] +````` + +## `./build/ripwire . --for="tree-sitter parse of a source file" --sections=lego,compose` + +*Restore the / sections the --for bundle collapses to a counted stub by default (this is the stub's own next= spelling).* + +**wall time: 2.93s** + +````` + + + + +inline bool topLevelEvidence( std::string_view source, const TSLanguage* language, const Predicate& predicate ) +The crawl's per-file byte ceiling. A text file larger than this is skipped: at this size it is o…constexpr std::size_t kDefaultMaxFileBytes = 4u * 1024u * 1024u +TSTree* parseTree( TSParser* parser, std::string_view src ) + +Exercise every registered grammar and its embedded query, reporting loaded and expected totalsinline DoctorGrammarProbe doctorProbeGrammars() +struct FileHealth +constexpr std::array<LangEntry, 57> kLangTable = +inline RevSide sliceAtRev( const std::string& root, const std::string& sha, const std::string& rel, const Symbol& sym, SliceFam fam, const ::TSLanguage* grammar…struct SliceScan +enum class AstWalk : std::uint8_t +inline bool astroFenceTailIsBlank( std::string_view src, std::size_t from, std::size_t to ) noexcept +constexpr std::uint32_t kMaxKotlinStringNestDepth = 128u +std::vector<LocalNameFact> collectGatedLocalNames( std::string_view defBytes, std::uint32_t defStartLine, Lang lang ) +inline ModuleScan scanModule( std::string_view source, std::string_view path ) +inline bool hasPhantomScopeSeparator( TSNode qualified ) noexcept +bool jsonNestsTooDeep( std::string_view bytes ) noexcept +constexpr std::uint32_t kMaxYamlNestDepth = 64u +inline bool hasMainGuard( std::string_view source ) +SpanTierBatch spanTiersOfFiles( std::span<const std::string> diskPaths, bool useMemo ) +inline SliceScan sliceScanDefinition( const std::string& src, const Symbol& sym, SliceFam fam, const ::TSLanguage* grammar, std::string_view varName ) +inline void runParseWorker( ParsePoolShared& sh, unsigned t ) +enum class DisclosureWhy : std::uint8_t +struct LintRule +inline std::string buildMemoryStopAttr( const IngestResult& ing, bool json ) +inline bool hasPytestTable( std::string_view source ) +… [148 more display lines; full output is 19901 bytes on 44 raw line(s)] +````` + +## `./build/ripwire . --for="tree-sitter parse of a source file" --auto-bodies` + +*Opt OUT of compact conceptual serving: restore the rank-first auto walk (bundle="auto").* + +**wall time: 2.22s** + +````` + + + + +inline bool topLevelEvidence( std::string_view source, const TSLanguage* language, const Predicate& predicate ) +The crawl's per-file byte ceiling. A text file larger than this is skipped: at this size it is o…constexpr std::size_t kDefaultMaxFileBytes = 4u * 1024u * 1024u +TSTree* parseTree( TSParser* parser, std::string_view src ) + +Exercise every registered grammar and its embedded query, reporting loaded and expected totalsinline DoctorGrammarProbe doctorProbeGrammars() +struct FileHealth +constexpr std::array<LangEntry, 57> kLangTable = +inline RevSide sliceAtRev( const std::string& root, const std::string& sha, const std::string& rel, const Symbol& sym, SliceFam fam, const ::TSLanguage* grammar…struct SliceScan +enum class AstWalk : std::uint8_t +inline bool astroFenceTailIsBlank( std::string_view src, std::size_t from, std::size_t to ) noexcept +constexpr std::uint32_t kMaxKotlinStringNestDepth = 128u +std::vector<LocalNameFact> collectGatedLocalNames( std::string_view defBytes, std::uint32_t defStartLine, Lang lang ) +inline ModuleScan scanModule( std::string_view source, std::string_view path ) +inline bool hasPhantomScopeSeparator( TSNode qualified ) noexcept +bool jsonNestsTooDeep( std::string_view bytes ) noexcept +constexpr std::uint32_t kMaxYamlNestDepth = 64u +inline bool hasMainGuard( std::string_view source ) +SpanTierBatch spanTiersOfFiles( std::span<const std::string> diskPaths, bool useMemo ) +inline SliceScan sliceScanDefinition( const std::string& src, const Symbol& sym, SliceFam fam, const ::TSLanguage* grammar, std::string_view varName ) +inline void runParseWorker( ParsePoolShared& sh, unsigned t ) +enum class DisclosureWhy : std::uint8_t +struct LintRule +inline std::string buildMemoryStopAttr( const IngestResult& ing, bool json ) +inline bool hasPytestTable( std::string_view source ) +… [133 more display lines; full output is 20257 bytes on 55 raw line(s)] +````` + +## `./build/ripwire . --for="quality delta acks ledger rubber stamp"` + +*Doc-mention surfacing (default ON): a markdown doc naming a top-resolved symbol in a backtick rides in below that symbol — the legend's [doc mentions: …] clause says it fired.* + +**wall time: 2.07s** + +````` + + + + + +L2 stale acks in the range form. A changed-files answer holds no whole target tree to check the …std::vector<rw::quality::StaleAck> refPairStaleAcks( const gtl::btree_map<std::string, rw::quality::AckRecord>& acks, const RefPairDelta& refs ) +inline QualityDeltaOutcome computeQualityDelta( const std::string& root ) +inline const char* kAcksFile = ".ripwire_quality_acks" +inline constexpr const char* kAtStampLegend = "<!-- at= is the git commit these numbers were computed at +inline rw::pathguard::NoFollowRead openRegularFileStream( std::string_view what, const std::string& path ) +inline Result computeDmm( const std::string& root, std::string_view spec, const IngestResult& workingIng, const std::vector<std::string>& excludes, std::size_t maxFileBytes … [line truncated: 5 more bytes on this line] +inline const char* kNotesFile = ".ripwire_notes" +inline void writePlanLint( std::FILE* out, const LintResult& res ) + +The REF-PAIR floor's comparison, once loadRefPairDelta has both sides: the base snapshot is the …void refPairBasis( const rw::Config& cfg, const std::string& root, RefPairDelta& refs, DeltaBasis& out ) +std::optional<int> refuseForeignAckSelection( const rw::Config& cfg, const rw::quality::Scope& scope, const std::vector<rw::quality::Regression>& outOf … [line truncated: 16 more bytes on this line] +std::optional<int> runQualityDelta( const MainDispatch& d ) +struct DeltaBasis +int ackNothingToAccept( const std::string& acksFile, const gtl::btree_map<std::string, rw::quality::AckRecord>& acks, const rw::quality::Scope& scope, std::size… … [line truncated: 4 more bytes on this line] +DirtyPinVerdict inspectDirtyBaselinePin( const MainDispatch& d, const std::string& acksFile ) +struct IdentityHealing +inline std::pair<std::string, std::string> qualityDeltaJson( const std::string& root ) +inline bool writeAckRecords( const std::string& path, const gtl::btree_map<std::string, AckRecord>& acks ) +inline std::string staleAcksXml( const std::vector<StaleAck>& staleAcks, EscapeFn esc ) +inline std::string qualityAcksPath( const std::string& root ) +struct QualityDeltaOutcome +std::optional<int> resolveDeltaBasis( const MainDispatch& d, const std::string& baselineFile, RefPairDelta& refs, DeltaBasis& out ) +inline bool atomicWriteFile( const std::string& path, const std::string& blob ) +inline std::string gitHeadSha( const std::string& root ) +inline gtl::btree_map<std::string, AckRecord> readAckRecords( const std::string& path, std::size_t& badLines ) +… [192 more display lines; full output is 22804 bytes on 45 raw line(s)] +````` + +## `./build/ripwire . --for="quality delta acks ledger rubber stamp" --no-doc-mention` + +*The same task with doc-mention surfacing OFF — the contrast the flag exists for (no [doc mentions] clause, one fewer row).* + +**wall time: 2.12s** + +````` + + + + + +L2 stale acks in the range form. A changed-files answer holds no whole target tree to check the …std::vector<rw::quality::StaleAck> refPairStaleAcks( const gtl::btree_map<std::string, rw::quality::AckRecord>& acks, const RefPairDelta& refs ) +inline QualityDeltaOutcome computeQualityDelta( const std::string& root ) +inline const char* kAcksFile = ".ripwire_quality_acks" +inline constexpr const char* kAtStampLegend = "<!-- at= is the git commit these numbers were computed at +inline rw::pathguard::NoFollowRead openRegularFileStream( std::string_view what, const std::string& path ) +inline Result computeDmm( const std::string& root, std::string_view spec, const IngestResult& workingIng, const std::vector<std::string>& excludes, std::size_t maxFileBytes … [line truncated: 5 more bytes on this line] +inline const char* kNotesFile = ".ripwire_notes" +inline void writePlanLint( std::FILE* out, const LintResult& res ) + +The REF-PAIR floor's comparison, once loadRefPairDelta has both sides: the base snapshot is the …void refPairBasis( const rw::Config& cfg, const std::string& root, RefPairDelta& refs, DeltaBasis& out ) +std::optional<int> refuseForeignAckSelection( const rw::Config& cfg, const rw::quality::Scope& scope, const std::vector<rw::quality::Regression>& outOf … [line truncated: 16 more bytes on this line] +std::optional<int> runQualityDelta( const MainDispatch& d ) +struct DeltaBasis +int ackNothingToAccept( const std::string& acksFile, const gtl::btree_map<std::string, rw::quality::AckRecord>& acks, const rw::quality::Scope& scope, std::size… … [line truncated: 4 more bytes on this line] +DirtyPinVerdict inspectDirtyBaselinePin( const MainDispatch& d, const std::string& acksFile ) +struct IdentityHealing +inline std::pair<std::string, std::string> qualityDeltaJson( const std::string& root ) +inline bool writeAckRecords( const std::string& path, const gtl::btree_map<std::string, AckRecord>& acks ) +inline std::string staleAcksXml( const std::vector<StaleAck>& staleAcks, EscapeFn esc ) +inline std::string qualityAcksPath( const std::string& root ) +struct QualityDeltaOutcome +std::optional<int> resolveDeltaBasis( const MainDispatch& d, const std::string& baselineFile, RefPairDelta& refs, DeltaBasis& out ) +inline bool atomicWriteFile( const std::string& path, const std::string& blob ) +inline std::string gitHeadSha( const std::string& root ) +inline gtl::btree_map<std::string, AckRecord> readAckRecords( const std::string& path, std::size_t& badLines ) +… [192 more display lines; full output is 22624 bytes on 45 raw line(s)] +````` + +## `./build/ripwire . --safe-delete=rankGraphTeleport` + +*"Can I delete this?" — callers + transitive impact + every use site + how much of the radius is tested, composed in ONE call; risk= names what was found, never a verdict.* + +````` + + + + + + + + + +````` + +## `./build/ripwire . --safe-delete=DoesNotExist` + +*Unknown-symbol refusal shape for --safe-delete.* + +**exit code: 1** + +````` +(empty) +````` + +stderr: + +````` +ripwire: --safe-delete symbol not found: DoesNotExist +````` + +## `./build/ripwire . --handoff` + +*The continuation packet for the NEXT session: disk truth (branch/sha, changed symbols, blast radius, tests) + labeled suggestions. Recorded against a CLEAN tree.* + +**wall time: 5.14s** + +````` + + + + + + + + + + + + + +````` + +## `./build/ripwire . --handoff --token-budget=1200` + +*The same packet under a hard ceiling: heuristic rows drop tail-first (withheld= disclosed), verified rows never drop.* + +**wall time: 5.22s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [432 more display lines; full output is 46000 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --no-ignore --top-k=3` + +*Crawl paths the repo's own .gitignore covers (default honours it and discloses ignored_files=/ignored_dirs= only when it dropped anything — this repo's crawl drops nothing, so the header is identical to the default map's; --skipped's ignore_mode= says which rule applied).* + +**wall time: 1.02s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [55 more display lines; full output is 7834 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --no-stable --top-k=3` + +*--no-stable outside --mcp: what the flag does (or says) when there is no stable-by-default ordering to opt out of.* + +**wall time: 1.17s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [55 more display lines; full output is 7834 bytes on 1 raw line(s)] +````` + +stderr: + +````` +ripwire: --no-stable is read only by --mcp/--listen (it opts out of the stable ordering the server turns on) — it changed nothing here; the CLI map orders important-first unless you pass --order=stable +````` + +## `./build/ripwire . --run-trace="cat /aux/asan_trace_now.txt; exit 1"` + +*EXEC-MODE --from-trace: run a command, and on a non-zero exit map its captured output onto indexed symbols in the same call — the whole fix-loop entry. ripwire exits 4 here because the wrapped command failed, which is the signal, not an incident.* + +**exit code: 4** — **wall time: 1.16s** + +````` + + + + + + const&, float) src/graph.h:10134 + #1 0x102f3e884 in rw::rankGraph(Graph const&, float) src/graph.h:10175 + #2 0x102e11f30 in runDefaultMap(MainDispatch const&) src/main.cpp:1753 + #3 0x102e01a44 in main src/main.cpp:4174 + #4 0x1a2b3c0dc in start+0x9dc (dyld:arm64e+0x60dc)]]> pw = biasPrior( g, p ); + const std::size_t N = pw.size(); + std::vector teleport( pw.begin(), pw.end() ); + std::vector rankDouble( N, 0.0 ); + PageRankRun run{}; // an N == 0 graph never enters the kernel: { 0, converged } — see PageRankRun + if( N ) + { + double teleportMass = 0.0; + for( const double value : teleport ) + { + teleportMass += value; + } + if( teleportMass > 0.0 ) + { + const double inverseMass = 1.0 / teleportMass; + for( double& value : teleport ) + { + value *= inverseMass; + } + } + run = pageRankDouble( g.inEdges, g.wOutDeg, teleport, rankDouble, PageRankConfig{ .alpha = double( alpha ) } ); + } + std::vector r( N, 0.f ); + std::transform( rankDouble.begin(), rankDouble.end(), r.begin(), []( double value ) { return float( value ); } ); + return { std::move( r ), run.iterationCount, run.hasConverged }; +}]]>inline std::vector<float> biasPrior( const Graph& g, const std::vector<float>& p )#define PROFILE_SCOPE_DESCRIBE( desc )#define PROFILE_SCOPE_DESC … [line truncated: 289 more bytes on this line] +````` + +## `./build/ripwire . --run-trace="true"` + +*A command that exits 0: a minimal success record (exit, measured duration, disclosed output tail) and NO bundle — nothing failed, nothing to map.* + +**wall time: 1.13s** + +````` + + + + +````` + +## `./build/ripwire . --run-trace="sleep 30" --run-timeout=2` + +*A command still running at the cap: its process group is killed and the run reports timed_out=1 — an honest timeout, never an empty success.* + +**exit code: 4** — **wall time: 2.91s** + +````` + + + + +````` + +stderr: + +````` +ripwire: --run-trace: TIMEOUT — the command exceeded the 2 s cap; its process group was killed +````` + +## `./build/ripwire . --run-timeout=5` + +*--run-timeout alone is refused loudly (it only modifies --run-trace).* + +**exit code: 1** + +````` +(empty) +````` + +stderr: + +````` +ripwire: --run-timeout=SECONDS modifies --run-trace — pass it too (e.g. ripwire --run-trace="make -j" --run-timeout=60) +````` + + +--- + +# assess quality — the wider lens family + +## `./build/ripwire . --quality-panel` + +*THE single wide-angle quality read: six families in one pass, an eligible/ranked shortlist rather than a firehose.* + +**wall time: 6.36s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [188 more display lines; full output is 15672 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --biggest-first --limit=8` + +*Per-function size ranking, largest Halstead volume/token entropy/lines first — a size proxy, not a readability order (withdrawn, docs/EVALS.md §8) — a RANKING lens, not a grade.* + +**wall time: 1.02s** + +````` + + + + + + + + + + + +````` + +## `./build/ripwire . --comment-coherence --limit=8` + +*Functions WITH a doc comment, most name-restating first: c_coeff (high = the comment repeats the name) and cic (Jaccard of comment vs identifier vocabulary), both reported, never collapsed.* + +````` + + + + + + + + + + + +````` + +## `./build/ripwire . --context-ratio --limit=8` + +*The local-reasoning lens: to understand this symbol, how much must you know that is NOT in front of you (ent_ratio= edge share, read_ratio= token-weighted).* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [21 more display lines; full output is 11235 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --nonlocal-state --limit=8` + +*Per function, the non-local MUTABLE state it can reach (transitively), most writes first — unsound by construction, and the legend says where.* + +**wall time: 2.40s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [41 more display lines; full output is 7601 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --ensemble --limit=8` + +*The family join: per function, which of four orthogonal evidence families fire, ranked by how many agree.* + +**wall time: 2.83s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [36 more display lines; full output is 7488 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --field-affinity` + +*The cache-locality lens over every aggregate: fields READ TOGETHER but declared FAR APART (split-line / straddle findings, Chilimbi separation weight) — advice only, never a rewrite.* + +**wall time: 1.29s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [509 more display lines; full output is 45573 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --field-affinity=Symbol` + +*The same lens narrowed to ONE struct — the one --layout=Symbol shows the offsets for.* + +**wall time: 1.30s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + +````` + +## `./build/ripwire . --naming-consistency --limit=8` + +*The corpus's OWN case-convention vote per (language, kind) group; off-convention names get a mechanical propose= (a suggestion, never a blind rename).* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [13 more display lines; full output is 4358 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --naming-calibration` + +*Score the naming-* rules against this repo's own rename history: proxy=old/(old+new) per rule, 0.50 = chance; read pairs= (sample size) first.* + +**wall time: 9.95s** + +````` + + + + + + + + + + + +

+

+

+

+

+

+

+

+

+

+

+

+

+

+

+

+… [146 more display lines; full output is 15431 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --lint --naming-locals` + +*The opt-in --lint modifier: naming predicates over LOCAL variable names too, C/C++ only, only inside functions already past a size/complexity gate.* + +**wall time: 2.20s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [640 more display lines; full output is 72652 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --lint-catalog` + +*The built-in rule registry — one row per rule with sev=/category=/rationale/lang=/since=; no corpus needed.* + +````` + + +a C-style cast — cppcoreguidelines-pro-type-cstyle-cast prefers the explicit static_cast/const_cast/reinterpret_cast +a goto statement — cppcoreguidelines-avoid-goto +a do/while loop shape (on Swift, its do/catch block shares the same grammar node and also matches) +a call to an unbounded C string function (strcpy/strcat/sprintf/gets) +a call to a broken hash or cipher (MD5/SHA1/MD4/RC4) +a doubly-parenthesized expression — readability-redundant-parentheses +an if-body that is just `;` — bugprone-suspicious-semicolon +a C-style typedef struct/union where `using` is preferred +a non-trivial numeric literal inside a function body, outside a const/constexpr init +a catch block with an empty body +x = x — almost always a copy-paste bug +a function body over 80 lines +brace nesting depth over 4 inside a function body +a bare `return;` mixed with a value-returning return in the same function +a statement after an unconditional return/break/continue/throw/raise in the same block +a 1-2 letter Function/Method/Var name — visible far beyond any tiny scope [Beniamini/Hofmeister] +more than 5 split tokens in one name [Butler; AlSuhaibani] +foo1/foo2/... digit-suffix siblings sharing a base name in one scope [Butler] +internal consecutive underscores, or a C-family reserved __x/_X form [Butler] +snake_case and camelCase mixed inside one name [Butler] +an is/has/can/should/was-prefixed name whose KNOWN return type is not bool-like [LAPD A2] +a set-prefixed name whose KNOWN return type is not void-like [LAPD A3] +a co-visible pair within edit distance <=2, reordered tokens, or a bare/digit-suffixed twin [Namesake] +every split subtoken is corpus-ubiquitous (BM25 idf) on a body past the size floor — fires only at the low end [Sparck Jones 1972] +the comma operator inside an expression (never a for-header comma) [Gopstein FSE 2017] +++/-- evaluated inside a larger expression (never a whole statement) [Gopstein FSE 2017] +an assignment whose VALUE is consumed (a condition, an argument, ...) [Gopstein FSE 2017] +a conditional expression inside a conditional expression [Gopstein FSE 2017] +… [12 more display lines; full output is 7800 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --legend-dict` + +*The session legend dictionary the MCP server serves as ripwire://legend-dict/full — one definition per line, headed by its dictv= version; no corpus needed. =roster lists the completeness attributes it defines.* + +````` +ripwire legend dictionary ripwire.dict/v1 dictv=30173b0aa2d06473 entries=908 +: the answer's rows come first; its root keeps only task= changed= from= to=, and this LAST child carries every other root attribute unchanged (schema= included); legend="ref": a definition is sent once per session (this dictionary's core, or the first answer that ne … [line truncated: 83 more bytes on this line] +schema=ripwire.KEY/v1: the line ripwire.KEY/v1 below reads the answer's rows +window: shown= total= capped= has_more= next_offset= offset= limit= page a list (capped=1 cut; next_offset= pastes as offset=) +X_capped= (any attribute ending _capped): 1 = cut +under ref, est_tokens= and over_ceiling= price the answer with its inline legend: an upper bound +under ref, a group of n <= 8 runner-less rows prints as its n single rows, each run_unknown=1 +ripwire.map/v1 : ranked symbol map: groups rows (k= rank), resolved callees; the header comment is data +ripwire.map-diff/v1 : the ranked map anchored at at=: what the diff touched, the map's row vocabulary +ripwire.metrics/v1 : the ranked map with per-symbol metrics: in/out, cx/ccx, loc, params, nest, humps/deep, locals, cbo, amp, tested, ev +ripwire.around/v1 : call neighbourhood of of=: depth= hops, fanout= kept per hop; absent rows lie outside that boundary +ripwire.query/v1 : lexical-rank map for the query term, the map's row vocabulary +ripwire.pack-signatures/v1 : the ranked map plus signature rows +ripwire.pack-top-n/v1 : the ranked map plus bodies of the top-N symbols +ripwire.skipped/v1 : why the index lacks a file ; indexed but unvouched ; census +ripwire.notes/v1 : field notes by target: holds ; the kept count comment: notes= rows, targets= rows, dangling= targets matching nothing indexed (listed, surfaced nowhere) +ripwire.lego/v1 : ONE interface/base type: , its method contract, every implementor +ripwire.expand/v1 : full bodies: of ; resolved callees +ripwire.expand-file/v1 : the file's own text: ; per scoped symbol; full id = p::sc::n +ripwire.candidate-page/v1 : budgeted-bundle candidate page: a resumable window over the ranked candidate set the bundle cuts, for task= under budget_tokens=, one tier per page, ranks global across pages +ripwire.pack-task/v1 : one-call task bundle for task= under budget_tokens=: ranking, ranked but over 1 hop out (of_top= ranked rows) > with callees > 1-hop from the bodies (o … [line truncated: 105 more bytes on this line] +ripwire.from-trace/v1 : trace frames mapped to indexed symbols, innermost first; the innermost in-corpus body included +ripwire.exemplar/v1 : the best-in-class instance of kind= for the task, chosen by role: , to imitate +ripwire.pack-task/v1 : N minimally overlapping agent bundles carved along call-graph communities plus one shared core; each wraps a ; bundle role=core|partition i= symbols= modules= bytes= tokens=: one agent's ctx, symbols= ids assigned, bytes= its size; tokens= = est_t … [line truncated: 345 more bytes on this line] +ripwire.callers/v1 : 1-hop CALLERS of of= (defs= matched, count= distinct symbols): ; hop_tested=/hop_untested= +ripwire.callees/v1 : 1-hop CALLEES of of= (defs= matched, count= distinct symbols): +ripwire.uses/v1 : resolvable use-sites of of=: +ripwire.impact/v1 : transitive blast radius of of=: reach set, importers; defs= matched, reaches= their transitive callers, radius_tested= non-tests an indexed test reaches, radius_untested= the rest; importers= files that #include/import a def's file +ripwire.path/v1 : one DIRECTED call path from= to to=, each a hop; reachable=0 hops=0 when none +ripwire.connect/v1 : minimal joining subgraph: groups, terminals, joins, edges, +… [879 more display lines; full output is 93890 bytes on 909 raw line(s)] +````` + +## `./build/ripwire . --lint --lint-select=cache-` + +*Run ONLY one rule family; the root carries selected="K of N" so a filtered zero is never confusable with an unfiltered one.* + +**wall time: 1.72s** + +````` + + + + + + + + + + +vector<std::vector<double>> +nodes[ perm[i] ] +nodes[ perm[ i + 1 ] ] +p = p->next +map +byName[ symN[i] ] +vector<std::vector<std::vector<std::uint32_t>>> +vector<std::vector<std::uint32_t>> +m[ symN[id] ] +vector<std::vector<double>> +m[ in.keys[ i ] ] +map +vector<std::vector<std::vector<std::uint32_t>>> +vector<std::vector<std::uint32_t>> +m[ symN[id] ] +map +m[ g_pool[ g_keyOf[i] ] ] +g_pool[ g_readOf[i] ] +g_pool[ g_readOf[i] ] +m[ g_pool[i] ] +… [608 more display lines; full output is 73393 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --lint --lint-ignore=naming-,cache-` + +*DROP two families, applied after selection; the raw select=/ignore= you passed rides on the root.* + +**wall time: 1.78s** + +````` + + + + + + + + + + + + + + + + + + + + + + + +0.5 +1023u +main (90 lines) +20 +delta < 0.02 && delta > -0.02 +0.02 +… [725 more display lines; full output is 66267 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --lint --lint-select=goto --lint-max-per-rule=4` + +*A rule that spends its own match budget (here 4, set BELOW the default 5000 so the floor shows on this repo): goto carries count_capped="1", its count= is a FLOOR, and the root names the call that counts the rest — findings_next= re-runs only the floored rules under a 10x budget.* + +**wall time: 1.82s** + +````` + + + +goto done; +goto epilogue; +goto epilogue; +goto epilogue; + +````` + +## `./build/ripwire . --lint --lint-select=goto --lint-max-per-rule=40` + +*Pasting that findings_next=: the same rule under a 10x budget counts its true total — no count_capped=, no findings_next=.* + +**wall time: 1.80s** + +````` + + + +goto done; +goto epilogue; +goto epilogue; +goto epilogue; +goto epilogue; +goto epilogue; +goto epilogue; +goto epilogue; +goto epilogue; +goto epilogue; +goto epilogue; +goto epilogue; +goto epilogue; +goto error; +goto error; +goto error; +goto done; +goto done; + +````` + +## `./build/ripwire . --lint --lint-select=cach-` + +*An unresolvable PREFIX refuses (exit 1) with a did-you-mean from a real edit distance (one character off cache-).* + +**exit code: 1** — **wall time: 1.76s** + +````` +(empty) +````` + +stderr: + +````` +ripwire: --lint-select: 'cach-' matches no rule or family (did you mean 'cache-'?) — see --lint-catalog for the full registry +````` + +## `./build/ripwire . --lint --lint-select=nosuchfamily` + +*A PREFIX with no near miss at all: the refusal points at --lint-catalog instead of guessing.* + +**exit code: 1** — **wall time: 1.79s** + +````` +(empty) +````` + +stderr: + +````` +ripwire: --lint-select: 'nosuchfamily' matches no rule or family — see --lint-catalog for the full registry +````` + +## `./build/ripwire . --lint --sarif` + +*The SAME findings as SARIF 2.1.0 (what github/codeql-action/upload-sarif consumes) — pure re-serialization, results count == the native run's.* + +**wall time: 1.78s** + +````` +{"version":"2.1.0","$schema":"https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json","runs":[{"tool":{"driver":{"name":"ripwire","rules":[{"id":"c-style-cast","shortDescription":{"text":"c-style-cast"},"properties":{"builtin":true,"capped":false,"applicable": … [line truncated: 7 more bytes on this line] +{"id":"goto","shortDescription":{"text":"goto"},"properties":{"builtin":true,"capped":false,"applicable":true}}, +{"id":"do-while","shortDescription":{"text":"do-while"},"properties":{"builtin":true,"capped":false,"applicable":true}}, +{"id":"unsafe-c-fn","shortDescription":{"text":"unsafe-c-fn"},"properties":{"builtin":true,"capped":false,"applicable":true}}, +{"id":"weak-crypto","shortDescription":{"text":"weak-crypto"},"properties":{"builtin":true,"capped":false,"applicable":true}}, +{"id":"redundant-parens","shortDescription":{"text":"redundant-parens"},"properties":{"builtin":true,"capped":false,"applicable":true}}, +{"id":"suspicious-semicolon","shortDescription":{"text":"suspicious-semicolon"},"properties":{"builtin":true,"capped":false,"applicable":true}}, +{"id":"typedef-over-using","shortDescription":{"text":"typedef-over-using"},"properties":{"builtin":true,"capped":false,"applicable":true}}, +{"id":"magic-number","shortDescription":{"text":"magic-number"},"properties":{"builtin":true,"capped":true,"applicable":true}}, +{"id":"empty-catch","shortDescription":{"text":"empty-catch"},"properties":{"builtin":true,"capped":false,"applicable":true}}, +{"id":"self-assign","shortDescription":{"text":"self-assign"},"properties":{"builtin":true,"capped":false,"applicable":true}}, +{"id":"large-function","shortDescription":{"text":"large-function"},"properties":{"builtin":true,"capped":false,"applicable":true}}, +{"id":"deep-nesting","shortDescription":{"text":"deep-nesting"},"properties":{"builtin":true,"capped":false,"applicable":true}}, +{"id":"inconsistent-return","shortDescription":{"text":"inconsistent-return"},"properties":{"builtin":true,"capped":false,"applicable":true}}, +{"id":"unreachable-code","shortDescription":{"text":"unreachable-code"},"properties":{"builtin":true,"capped":false,"applicable":true}}, +{"id":"naming-short","shortDescription":{"text":"naming-short"},"properties":{"builtin":true,"capped":false,"applicable":true}}, +{"id":"naming-wordy","shortDescription":{"text":"naming-wordy"},"properties":{"builtin":true,"capped":false,"applicable":true}}, +{"id":"naming-series","shortDescription":{"text":"naming-series"},"properties":{"builtin":true,"capped":false,"applicable":true}}, +{"id":"naming-underscore","shortDescription":{"text":"naming-underscore"},"properties":{"builtin":true,"capped":false,"applicable":true}}, +{"id":"naming-case","shortDescription":{"text":"naming-case"},"properties":{"builtin":true,"capped":false,"applicable":true}}, +{"id":"naming-predicate","shortDescription":{"text":"naming-predicate"},"properties":{"builtin":true,"capped":false,"applicable":true}}, +{"id":"naming-setter","shortDescription":{"text":"naming-setter"},"properties":{"builtin":true,"capped":false,"applicable":true}}, +{"id":"naming-confusable","shortDescription":{"text":"naming-confusable"},"properties":{"builtin":true,"capped":false,"applicable":true}}, +{"id":"naming-uninformative","shortDescription":{"text":"naming-uninformative"},"properties":{"builtin":true,"capped":false,"applicable":true}}, +{"id":"atom-comma-operator","shortDescription":{"text":"atom-comma-operator"},"properties":{"builtin":true,"capped":false,"applicable":true}}, +{"id":"atom-embedded-crement","shortDescription":{"text":"atom-embedded-crement"},"properties":{"builtin":true,"capped":false,"applicable":true}}, +{"id":"atom-assign-as-value","shortDescription":{"text":"atom-assign-as-value"},"properties":{"builtin":true,"capped":false,"applicable":true}}, +{"id":"atom-nested-ternary","shortDescription":{"text":"atom-nested-ternary"},"properties":{"builtin":true,"capped":false,"applicable":true}}, +{"id":"atom-implicit-predicate","shortDescription":{"text":"atom-implicit-predicate"},"properties":{"builtin":true,"capped":false,"applicable":true}}, +{"id":"atom-octal-literal","shortDescription":{"text":"atom-octal-literal"},"properties":{"builtin":true,"capped":false,"applicable":true}}, +… [12761 more display lines; full output is 1804753 bytes on 1 raw line(s)] +````` + +Parsed summary of the same SARIF (past the display cut): + +````` +sarif 2.1.0 rules= 39 results= 6376 +````` + +## `./build/ripwire . --lint --sarif --limit=5` + +*SARIF is always the FULL result set: paging alongside it refuses loudly.* + +**exit code: 1** + +````` +(empty) +````` + +stderr: + +````` +ripwire: --sarif always emits the full result set — drop --limit=N/--offset=M +````` + +## `./build/ripwire . --dmm` + +*The Delta Maintainability Model scalar for the WORKING TREE vs HEAD — recorded against a CLEAN tree (the sandbox section shows a real delta). UNAVAILABLE is a stated reason, never 0 or 1.* + +**wall time: 3.97s** + +````` + + +

+

+ +````` + +## `./build/ripwire . --dmm=HEAD` + +*The per-commit scalar: HEAD vs its first parent, with the three separately actionable sub-scores.* + +**wall time: 8.01s** + +````` + + +

+

+ +````` + +## `./build/ripwire . --dmm=HEAD~3..HEAD` + +*The range form: tree HEAD vs tree HEAD~3.* + +**wall time: 7.97s** + +````` + + +

+

+ +````` + +## `./build/ripwire . --cochange --cochange-groups` + +*Modularity-violation GROUPS instead of pairs: "X co-changes with {A,B,C}, none of which it depends on" — a greedy cover, disclosed as greedy.* + +**wall time: 1.76s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [829 more display lines; full output is 55101 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --cochange --cochange-recur=2` + +*Only pairs whose co-change RECURS in 2+ sub-windows of the mined window (sub_windows= is the denominator) — a one-off sprint stops reading like a structural defect.* + +**wall time: 1.61s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +````` + +## `./build/ripwire . --html=/aux/map2.html --color-by=community` + +*The HTML graph with the initial colour mode set to community (the page embeds all five modes and keeps a live selector).* + +**wall time: 1.57s** + +````` +(empty) +````` + +Artifact written: + +````` + 156583 /aux/map2.html +````` + +## `./build/ripwire . --index-out=/aux/ci_index` + +*CI generate-and-exit: cold-parse and write BOTH committable cache families (lean + rich), no map on stdout.* + +**wall time: 4.11s** + +````` +(empty) +````` + +stderr: + +````` +ripwire: --index-out wrote /aux/ci_index.lean.ripwirecache (26937586 bytes, lean family) +ripwire: --index-out wrote /aux/ci_index.rich.ripwirecache (55620806 bytes, rich family) +````` + +Artifact written: + +````` + 26937586 /aux/ci_index.lean.ripwirecache + 55620806 /aux/ci_index.rich.ripwirecache + 82558392 total +````` + +## `./build/ripwire . --cache=/aux/ci_index.lean.ripwirecache --top-k=3` + +*Consume the lean artifact in a PR job: restore-equivalence, never blob-byte-identity.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [55 more display lines; full output is 7834 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --pin-census=/aux/pin_census.tsv --top-k=3` + +*Eval-only: a per-call-site census of WHICH mechanism resolved each call, and the canonical id of every surviving target.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [55 more display lines; full output is 7834 bytes on 1 raw line(s)] +````` + +Artifact written: + +````` +# ripwire pin-census v3 C=kind\tmech\tpre\tpost\tflags\tcaller_id\tcallee\ttargets(|-sep)\tline +# v3 field escape: every id and callee field is escaped, so no separator occurs inside one. A backslash is +# written \\, TAB \t, LF \n, CR \r, every other control byte 0x00-0x1f and the targets separator | as \xHH +# (lowercase hex). Split lines on LF, fields on TAB and targets on |, THEN decode. Columns are unchanged +# from v2 and an id without those bytes is spelled exactly as in v2; a C++ out-of-line template member +# whose template-argument list spans lines keeps its line break as \n instead of splitting the row. +# line is the 1-based call-site line in the caller's file (v2, appended LAST so v1 readers are unchanged): +# the key a SCIP occurrence joins on, so a coverage loss can be classified per site instead of guessed. + 123608 /aux/pin_census.tsv +````` + +## `./build/ripwire . --plan-lint=test/planlintfix/wave.md` + +*The house PLAN/DESIGN format's STRUCTURE check — never semantics; exit 2 when a card or ledger row gates.* + +**exit code: 2** + +````` + + + + + + + + + + + + + +````` + +## `./build/ripwire . --plan-lint=test/planlintfix/wave_ledger.md` + +*The ledger-shaped fixture through the same check (exit 2: one gating card).* + +**exit code: 2** + +````` + + + + + + + +````` + +## `./build/ripwire . --doctor --agent=claude` + +*--doctor plus a LIVE integration inspection for one agent: PATH binary, installed-skill manifest parity, hook executability, MCP wiring — read-only, fixed repair commands, never config contents; exit 1 when any check fails.* + +**exit code: 1** — **wall time: 6.41s** + +````` + + + + + + + + + + + + + +````` + +## `./build/ripwire . --doctor --agent=nosuch` + +*Other --agent values refuse.* + +**exit code: 1** + +````` +(empty) +````` + +stderr: + +````` +ripwire: unsupported --agent value 'nosuch' (supported: codex, claude) +````` + + +--- + +# the dirty-tree verbs (throwaway clone, NOT the read-only repo) + +Everything below runs with `cwd` = the throwaway clone at `/dirty` (`git clone --local` of this repo, then one deliberate regression in `src/infra/sortutil.h`). The read-only repo is never touched. The binary is the same `build/ripwire`, addressed absolutely. + +## `./build/ripwire . --situ` + +*Situational report for a real diff: blast radius + tests + co-change + forgotten co-change partners.* + +**wall time: 5.95s** + +````` +ripwire situational-awareness — 1 changed file(s), 13 symbols in them +root: . +at: eb541409e+dirty + [1] blast radius: 235 symbols across 55 files transitively depend on these changes (showing 8 of 55 files — shown=8 total=55 capped=1 prcontext_cap=20 (--pr-context's own per-file list is cut at 20 too); next: --situ --limit=55) + counts_floor=1 graph_ambiguous=2911 graph_unresolved=14064 graph_unindexed=266 (files no grammar in this build could read at all) (the map header's own gauges) — every count above is a FLOOR, never a total: call edges are name-based, so dynamic dispatch, callbacks and macros can be missing … [line truncated: 45 more bytes on this line] + src/mcpverbs.h (33 dependent symbols) + src/quality.h (15 dependent symbols) + src/verbs_quality.h (14 dependent symbols) + src/verbs_for.h (12 dependent symbols) + src/editcheck.h (9 dependent symbols) + src/resolve.h (9 dependent symbols) + src/verbs_navigate.h (9 dependent symbols) + src/graph.h (8 dependent symbols) + [2] tests to run (5) order=evidence: [changed] you edited it, [partner] named after a changed file, then hops asc (1 = direct); "(n): a, b" = n runner-less files sharing that evidence; a (run: …) is relative to root: + test/verify_radix.cpp [hops=1] (run: bash test/greptiercheck.sh) + test/adaptivecutshapefix/adaptive_cut_shape_test.cpp [hops=2] (run: bash test/adaptivecutshapecheck.sh) + test/includeprecise_unit.cpp [hops=2] (run: bash test/includeprecisecheck.sh) + test/verify_csr.cpp [hops=2] (run: bash test/a9disclosurecheck.sh) + test/rustimport_unit.cpp [hops=4] (run: bash test/rustimportprecisecheck.sh) + script_gates_unmodelled=767 — test/*.sh gates never appear above: script-to-binary edges are not call edges (a path count) + [3] co-change — usually edited with these but NOT in your diff (0) window="18mo@HEAD" commits="4963": + (none — 4963 commits were mined and none co-edited a file outside your diff) + next: --test-gate +````` + +## `./build/ripwire . --test-gate` + +*The pre-PR gate with real obligations — exit 4 when tests-to-run or untested blast radius is non-empty.* + +**exit code: 4** — **wall time: 4.40s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +````` + +## `./build/ripwire . --quality-delta` + +*Every row carries p="file:line", the gating rows are marked gating="1" and now bar= (the threshold each numeric row is judged against), and the exit-2 refusal prints a naming line on stderr; a gating row carries a pasteable next=.* + +**exit code: 2** — **wall time: 24.97s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [146 more display lines; full output is 17933 bytes on 1 raw line(s)] +````` + +stderr: + +````` +ripwire: no ./.ripwire_quality_baseline — auto-comparing the working tree vs git HEAD (commit the baseline with --quality-baseline to pin it) +ripwire: --quality-delta gating: 5 preexisting-worse major finding(s); first: api-surface src/infra/sortutil.h::rw::sortutil::nonNegativeFloatDescKey at src/infra/sortutil.h:109 (was=1 now=2) +````` + +## `./build/ripwire . --quality-delta --legend=full` + +*The same gating report under --legend=full — same rows, same exit 2; the default compact comment (schema="ripwire.quality-delta/v1", the shape an agent's edit loop should run) becomes ~4 KB of prose legend.* + +**exit code: 2** — **wall time: 12.51s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [146 more display lines; full output is 22925 bytes on 1 raw line(s)] +````` + +stderr: + +````` +ripwire: no ./.ripwire_quality_baseline — auto-comparing the working tree vs git HEAD (commit the baseline with --quality-baseline to pin it) +ripwire: --quality-delta gating: 5 preexisting-worse major finding(s); first: api-surface src/infra/sortutil.h::rw::sortutil::nonNegativeFloatDescKey at src/infra/sortutil.h:109 (was=1 now=2) +````` + +## `./build/ripwire . --quality-delta --json` + +*The same findings as JSON (one of the CI/scripting verbs --json supports) — same exit 2 as the XML form.* + +**exit code: 2** — **wall time: 12.45s** + +````` +{"baseline":"git-HEAD","regressions":9,"minor":1,"acked":0,"stale":164,"preexisting-worse":6,"new-symbol":3,"gating":5,"register-macro-excluded":62,"api-new-surface":2,"at":"eb541409e+dirty","renames":60,"rename_window_commits":400,"acked_by_rename":0,"acked_by_content":0,"renames_window_truncated": … [line truncated: 239 more bytes on this line] +{"kind":"complexity","sym":"src/infra/sortutil.h::rw::sortutil::lessByScoreDescId","was":1,"now":67,"p":"src/infra/sortutil.h:49","gating":true}, +{"kind":"dead-code","sym":"src/infra/sortutil.h::rw::sortutil::nonNegativeFloatAscKeyCopy","p":"src/infra/sortutil.h:119","origin":"new-symbol"}, +{"kind":"dead-code","sym":"src/infra/sortutil.h::rw::sortutil::sortScoredIdsWithOptions","p":"src/infra/sortutil.h:129","origin":"new-symbol"}, +{"kind":"duplication","members":"src/infra/sortutil.h::rw::sortutil::nonNegativeFloatAscKeyCopy | src/infra/sortutil.h::rw::sortutil::nonNegativeFloatDescKey","tokens":59,"p":"src/infra/sortutil.h:119","gating":true}, +{"kind":"nesting","sym":"src/infra/sortutil.h::rw::sortutil::lessByScoreDescId","was":1,"now":6,"p":"src/infra/sortutil.h:49","gating":true}, +{"kind":"new-clone-of-reused-helper","sym":"src/infra/sortutil.h::rw::sortutil::nonNegativeFloatAscKeyCopy | src/infra/sortutil.h::rw::sortutil::nonNegativeFloatDescKey","was":0,"now":4,"p":"src/infra/sortutil.h:119","gating":true}, +{"kind":"params","sym":"src/infra/sortutil.h::rw::sortutil::sortScoredIdsWithOptions","was":0,"now":8,"p":"src/infra/sortutil.h:129","origin":"new-symbol"}, +{"kind":"verbosity","sym":"src/infra/sortutil.h::rw::sortutil::lessByScoreDescId","was":8,"now":47,"p":"src/infra/sortutil.h:49","sev":"minor"}], +"sa":[{"kind":"api-surface","key":"085e3d408c2c4a35","why":"target-gone"}, +{"kind":"api-surface","key":"155d74d341a49f3c","why":"target-gone"}, +{"kind":"api-surface","key":"1a15386c2d1e47af","why":"target-gone"}, +{"kind":"api-surface","key":"298e798c7f075715","why":"target-gone"}, +{"kind":"api-surface","key":"56acdf9b5c314a14","why":"target-gone"}, +{"kind":"api-surface","key":"5a07390012b46e06","why":"target-gone"}, +{"kind":"api-surface","key":"6eef859578c8c376","why":"target-gone"}, +{"kind":"api-surface","key":"6f2394762f82a855","why":"target-gone"}, +{"kind":"api-surface","key":"7f2c3eefdf6e512e","why":"target-gone"}, +{"kind":"api-surface","key":"802e513731103806","why":"target-gone"}, +{"kind":"api-surface","key":"b6a24afef32a68a8","why":"target-gone"}, +{"kind":"api-surface","key":"c923e661c197b265","why":"target-gone"}, +{"kind":"api-surface:new-symbol","key":"1926b0d9e94541a0","why":"target-gone"}, +{"kind":"api-surface:new-symbol","key":"1d3814ba687a4aef","why":"target-gone"}, +{"kind":"api-surface:new-symbol","key":"1db76028879244ef","why":"target-gone"}, +{"kind":"api-surface:new-symbol","key":"25c9f0504fd376a4","why":"target-gone"}, +{"kind":"api-surface:new-symbol","key":"36e39c7ab0fc1686","why":"target-gone"}, +{"kind":"api-surface:new-symbol","key":"4bce64bd920de0c3","why":"target-gone"}, +{"kind":"api-surface:new-symbol","key":"61f0e361ef7dee27","why":"target-gone"}, +{"kind":"api-surface:new-symbol","key":"79625906f9f71ad0","why":"target-gone"}, +{"kind":"api-surface:new-symbol","key":"8ecb190954dce18a","why":"target-gone"}, +… [143 more display lines; full output is 16522 bytes on 1 raw line(s)] +````` + +stderr: + +````` +ripwire: no ./.ripwire_quality_baseline — auto-comparing the working tree vs git HEAD (commit the baseline with --quality-baseline to pin it) +ripwire: --quality-delta gating: 5 preexisting-worse major finding(s); first: api-surface src/infra/sortutil.h::rw::sortutil::nonNegativeFloatDescKey at src/infra/sortutil.h:109 (was=1 now=2) +````` + +## `./build/ripwire . --quality-delta --quality-ack --ack-only=zzznope` + +*NEW FLAG: --ack-only matching nothing REFUSES rather than falling back to acking everything.* + +**exit code: 1** — **wall time: 7.46s** + +````` +(empty) +````` + +stderr: + +````` +ripwire: no ./.ripwire_quality_baseline — auto-comparing the working tree vs git HEAD (commit the baseline with --quality-baseline to pin it) +ripwire: ack provenance backfill — 0 clone row(s) reconstructed from the current tree, 214 re-derived, 20 left UNVERIFIED (their group was not found here — a floor, not proof it is gone), 56 left legacy (member set not found cloning here — a floor, not proof it is gone), 1311 ineligible (no cu … [line truncated: 200 more bytes on this line] +ripwire: --ack-only=zzznope matched none of the 9 finding(s) — nothing accepted, but ./.ripwire_quality_acks was not in canonical form (ack provenance backfill / legacy rows) and has been re-serialised +````` + +## `./build/ripwire . --quality-delta --quality-ack --ack-only=api-surface` + +*NEW FLAG: ack only the api-surface findings — a per-finding ratchet instead of a rubber stamp.* + +**wall time: 7.66s** + +````` +(empty) +````` + +stderr: + +````` +ripwire: no ./.ripwire_quality_baseline — auto-comparing the working tree vs git HEAD (commit the baseline with --quality-baseline to pin it) +ripwire: ack provenance backfill — 0 clone row(s) reconstructed from the current tree, 214 re-derived, 20 left UNVERIFIED (their group was not found here — a floor, not proof it is gone), 56 left legacy (member set not found cloning here — a floor, not proof it is gone), 1311 ineligible (no cu … [line truncated: 200 more bytes on this line] +ripwire: acknowledged 1 of 9 finding(s) (8 left UNACKED by --ack-only, 0 already acked) → ./.ripwire_quality_acks +````` + +## `./build/ripwire . --quality-delta` + +*Re-run after the partial ack: acked=3, the rest still gate (exit 2).* + +**exit code: 2** — **wall time: 11.55s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [145 more display lines; full output is 17636 bytes on 1 raw line(s)] +````` + +stderr: + +````` +ripwire: no ./.ripwire_quality_baseline — auto-comparing the working tree vs git HEAD (commit the baseline with --quality-baseline to pin it) +ripwire: --quality-delta gating: 4 preexisting-worse major finding(s); first: complexity src/infra/sortutil.h::rw::sortutil::lessByScoreDescId at src/infra/sortutil.h:49 (was=1 now=67) +````` + +## `./build/ripwire . --ack-only=gating` + +*--ack-only WITHOUT --quality-ack REFUSES loudly (exit 1, the pairing named) — it used to be silently ignored.* + +**exit code: 1** + +````` +(empty) +````` + +stderr: + +````` +ripwire: --ack-only=SUBSTR narrows --quality-ack — pass both (e.g. ripwire

--quality-delta --ack-only=contract-change --quality-ack="reason") +````` + +## `./build/ripwire . --edit-check=nonNegativeFloatDescKey` + +*A real contract-change: was=1 now=2 params, with the call sites that are now provably incompatible, and a pasteable next= (--uses=SYM) on the root.* + +**wall time: 1.75s** + +````` + + + + + + +````` + +## `./build/ripwire . --edit-check=nonNegativeFloatDescKey --legend=full` + +*The same verdict under --legend=full: the default's one compact comment (the post-edit reflex at its cheapest) becomes ~5.7 KB of prose legend, every row identical.* + +**wall time: 1.72s** + +````` + + + + + + +````` + +## `./build/ripwire . --pr-context` + +*The review-evidence bundle with an actual changed file.* + +**wall time: 4.03s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [80 more display lines; full output is 8067 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --map-diff --top-k=5` + +*The map re-ranked with a teleport toward the changed file (changed=1 here, not 0).* + +**wall time: 2.49s** + +````` + + + + + + + + + + + + + + + + + + + + + + +````` + +## `./build/ripwire . --clones` + +*The duplicated helper the sandbox edit introduced shows up as a clone group.* + +**wall time: 2.28s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [308 more display lines; full output is 16216 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --stray-content=zz-orphan` + +*CHANGED: a ref with NO merge base with HEAD now reports v="unknown" ok="0" in its own bucket — the absence of an answer, never a claim it is merged. (The sandbox carries a deliberately parentless branch built with `git commit-tree`; a shallow CI clone puts every ref here.)* + +**wall time: 2.00s** + +````` + + + + + +````` + +stderr: + +````` +[math degraded] crossref: no merge-base for ref (shallow clone or unrelated history?) — verdict is unknown, not merged (crossref.h:1274, RefPlumbing rw::crossref::probeRefBase(const std::string &, const RefInfo &, const std::string &) — logged once per site) +````` + +## `./build/ripwire . --stray-content=zz-orphan --plan` + +*CHANGED: --plan surfaces those same refs as an row rather than silently dropping them.* + +**wall time: 3.01s** + +````` + + + + +````` + +stderr: + +````` +[math degraded] crossref: no merge-base for ref (shallow clone or unrelated history?) — verdict is unknown, not merged (crossref.h:1274, RefPlumbing rw::crossref::probeRefBase(const std::string &, const RefInfo &, const std::string &) — logged once per site) +````` + +## `./build/ripwire . --dmm` + +*The DMM scalar on a REAL delta: the sandbox edit grew one unit past the nesting/complexity thresholds and added an 8-parameter one, so dmm is low and the three sub-scores say which property moved.* + +**wall time: 4.60s** + +````` + + +

+

+ +````` + +## `./build/ripwire . --quality-delta --scope=src/graph.h` + +*OWNERSHIP partition for a shared tree: every regression here lives in src/infra/, so under a scope naming src/graph.h they ALL print under with a do-not-ack banner and never gate — scoped-out-gating= says how many would have.* + +**wall time: 11.73s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [148 more display lines; full output is 17903 bytes on 1 raw line(s)] +````` + +stderr: + +````` +ripwire: no ./.ripwire_quality_baseline — auto-comparing the working tree vs git HEAD (commit the baseline with --quality-baseline to pin it) +````` + +## `./build/ripwire . --quality-delta --quality-ack --scope=src/graph.h --ack-only=api-surface` + +*The rubber-stamp guard: an --ack-only that names an OUT-OF-SCOPE row refuses (exit 1) and writes nothing.* + +**exit code: 1** — **wall time: 7.19s** + +````` +(empty) +````` + +stderr: + +````` +ripwire: no ./.ripwire_quality_baseline — auto-comparing the working tree vs git HEAD (commit the baseline with --quality-baseline to pin it) +ripwire: --ack-only=api-surface selects 1 finding(s) OUT OF SCOPE for --scope=src/graph.h — refusing, and writing nothing at all: + api-surface src/infra/sortutil.h::rw::sortutil::nonNegativeFloatDescKey at src/infra/sortutil.h:109 + those rows belong to whoever is editing those paths. Acking them here writes their debt into a committed ledger under YOUR + reason string, which is how a per-finding ratchet becomes a rubber stamp. Narrow the pattern, or widen the scope if they really are yours. +````` + +## `./build/ripwire . --handoff` + +*The continuation packet with a REAL diff: verified changed symbols + blast radius + tests-to-run, then the heuristic rows.* + +**wall time: 3.62s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +````` + +## `./build/ripwire . --note-add="lessByScoreDescId: keep this branch-free — it sits inside the PageRank sort comparator"` + +*Pin a field note (write-side memory) to a symbol; committed to .ripwire_notes in the sandbox. The BARE name is resolved through the same resolver the read verbs use and stored as the canonical id — the rewrite is echoed on stderr, because a silent one is not a disclosure.* + +````` +src/infra/sortutil.h::rw::sortutil::lessByScoreDescId 2026-10-19 keep this branch-free — it sits inside the PageRank sort comparator eb541409e8fbff63c10f6e4192661251ec56de3c release/0.6.8-prep +````` + +stderr: + +````` +ripwire: --note-add: tip: notes that say "chose X over Y because Z" surface better — consider adding the why +ripwire: --note-add: target 'lessByScoreDescId' canonicalised to 'src/infra/sortutil.h::rw::sortutil::lessByScoreDescId' — that is the id --for/--expand key notes by +````` + +## `./build/ripwire . --notes` + +*The note is listed under the canonical id, dangling="0" — i.e. it will actually surface. (Before the H1 fix the bare name was stored verbatim and read dangling="1": recorded, and surfaced nowhere.)* + +````` + + + + + + + + + + + + + + + + gate scripts' claim (~line 1305) is NOT enforced — the derived-vs-stated sibling loop here covers docs/EVALS.md only. It drifted 407→451 unnoticed (fixed 2026-08-23). To close: grep both files ('file:line:' parsing) in the gateCountClaims arm.]]> + + + + +````` + +## `./build/ripwire . --note-add="gitOneLine: which one?"` + +*Two definitions carry this name, so the write REFUSES rather than pick one: a note keys ONE canonical id, and an ambiguous selector is refused, never silently narrowed. Every candidate is named, with a runnable retry.* + +**exit code: 1** + +````` +(empty) +````` + +stderr: + +````` +ripwire: --note-add: tip: notes that say "chose X over Y because Z" surface better — consider adding the why +ripwire: --note-add: target 'gitOneLine' is ambiguous — it matches 2 definitions in 2 distinct contracts, and a note keys ONE canonical id (it would surface on one of them and look absent on the rest). Qualify one: ./src/handoff.h:gitOneLine, ./src/quality.h:gitOneLine — e.g. --note-add="./src/h … [line truncated: 33 more bytes on this line] +````` + +## `./build/ripwire . --note-add="lessByScoreDescIdd: typo"` + +*A name that resolves to nothing is refused with the read verbs' own did-you-mean — never written as a dead note.* + +**exit code: 1** + +````` +(empty) +````` + +stderr: + +````` +ripwire: --note-add: tip: notes that say "chose X over Y because Z" surface better — consider adding the why +ripwire: --note-add: target not found: lessByScoreDescIdd (did you mean 'lessByScoreDescId'?) — a note keys the canonical id a read verb resolves; to note a FILE instead, pass a path (one with a '/' or an extension), which may name a file that does not exist yet +````` + +## `./build/ripwire . --note-add="src/infra/sortutil.h::rw::sortutil::lessByScoreDescId: chose the flat two-branch compare over the nested ladder because the comparator sits inside the PageRank sort"` + +*The same symbol addressed by its CANONICAL id: already canonical, so nothing is rewritten and both notes land on ONE target.* + +````` +src/infra/sortutil.h::rw::sortutil::lessByScoreDescId 2026-10-19 chose the flat two-branch compare over the nested ladder because the comparator sits inside the PageRank sort eb541409e8fbff63c10f6e4192661251ec56de3c release/0.6.8-prep +````` + +## `./build/ripwire . --expand=lessByScoreDescId --top-k=0` + +*Both notes riding along with the symbol's body — the elements follow the body, past the display cut, so they are extracted below.* + +````` + + + + + + + + + + + + + +````` + +## `./build/ripwire . --insert-after-symbol=lessByScoreDescId --edit-payload=/aux/payload_note.h` + +*Insert immediately AFTER one uniquely-resolved definition; replaced_bytes=0 because the insert verbs never overwrite. The receipt carries the folded post-edit verification (lines=, edit_check, tests_to_run) so the loop closes in one call.* + +**wall time: 3.90s** + +````` +{"applied":"insert_after_symbol","symbol":"lessByScoreDescId","file":"src/infra/sortutil.h","span":{"start":2375,"end":2463},"lines":{"start":56,"end":58},"replaced_bytes":0,"old_file_bytes":10824,"new_file_bytes":10912,"file_eol":"lf","eol_normalized":false,"trailing_newline_folded":true,"separator … [line truncated: 652 more bytes on this line] +"graph_ambiguous":2911,"graph_unresolved":14064,"graph_unindexed":266,"counts_floor":true},"tests_to_run":[{"p":"test/verify_radix.cpp","hops":1,"run":"bash test/greptiercheck.sh"}, +{"p":"test/adaptivecutshapefix/adaptive_cut_shape_test.cpp","hops":2,"run":"bash test/adaptivecutshapecheck.sh"}, +{"p":"test/includeprecise_unit.cpp","hops":2,"run":"bash test/includeprecisecheck.sh"}, +{"p":"test/verify_csr.cpp","hops":2,"run":"bash test/a9disclosurecheck.sh"}, +{"p":"test/rustimport_unit.cpp","hops":4,"run":"bash test/rustimportprecisecheck.sh"}], +"order":"evidence","partners":0,"run_first":1,"tests":5,"script_gates_unmodelled":767,"graph_ambiguous":2911,"graph_unresolved":14064,"graph_unindexed":266,"counts_floor":true,"next":"bash test/greptiercheck.sh"} +````` + +stderr: + +````` +ripwire edit: applied atomically; receipt carries region, blob_sha, edit_check, tests_to_run; next: bash test/greptiercheck.sh +````` + +## `./build/ripwire . --insert-after-symbol=lessByScoreDescId --edit-payload=/aux/payload_note.h --no-post-check` + +*The opt-out: the same insert with the folded verification skipped — lines= still rides (it is free), edit_check/tests_to_run do not, and the two pasteable commands stay on stderr.* + +**wall time: 1.76s** + +````` +{"applied":"insert_after_symbol","symbol":"lessByScoreDescId","file":"src/infra/sortutil.h","span":{"start":2375,"end":2463},"lines":{"start":56,"end":58},"replaced_bytes":0,"old_file_bytes":10912,"new_file_bytes":11000,"file_eol":"lf","eol_normalized":false,"trailing_newline_folded":true,"separator … [line truncated: 572 more bytes on this line] +````` + +stderr: + +````` +ripwire edit: applied atomically; receipt carries region, blob_sha (post-check skipped); next: --edit-check=src/infra/sortutil.h:lessByScoreDescId +````` + +## `./build/ripwire . --insert-before-symbol=nonNegativeFloatDescKey --edit-payload=/aux/payload_note.h --edit-target-file=src/infra/sortutil.h` + +*Insert BEFORE, with --edit-target-file pinning which same-named definition (here unambiguous — the disambiguator is simply honoured).* + +**wall time: 4.17s** + +````` +{"applied":"insert_before_symbol","symbol":"nonNegativeFloatDescKey","file":"src/infra/sortutil.h","span":{"start":2895,"end":2983},"lines":{"start":74,"end":75},"replaced_bytes":0,"old_file_bytes":11000,"new_file_bytes":11088,"file_eol":"lf","eol_normalized":false,"trailing_newline_folded":false,"s … [line truncated: 755 more bytes on this line] +{"n":"benchAdaptive","p":"bench/bench_radix_ab.cpp:157","l":[162]}, +{"n":"radixSortNonNegativeFloatsDesc","p":"src/infra/sortutil.h:105","l":[114]}, +{"n":"radixSortByScoreDescId","p":"src/infra/sortutil.h:120","l":[180]}], +"graph_ambiguous":2911,"graph_unresolved":14064,"graph_unindexed":266,"counts_floor":true},"tests_to_run":[{"p":"test/verify_radix.cpp","hops":1,"run":"bash test/greptiercheck.sh"}, +{"p":"test/adaptivecutshapefix/adaptive_cut_shape_test.cpp","hops":2,"run":"bash test/adaptivecutshapecheck.sh"}, +{"p":"test/includeprecise_unit.cpp","hops":2,"run":"bash test/includeprecisecheck.sh"}, +{"p":"test/verify_csr.cpp","hops":2,"run":"bash test/a9disclosurecheck.sh"}, +{"p":"test/rustimport_unit.cpp","hops":4,"run":"bash test/rustimportprecisecheck.sh"}], +"order":"evidence","partners":0,"run_first":1,"tests":5,"script_gates_unmodelled":767,"graph_ambiguous":2911,"graph_unresolved":14064,"graph_unindexed":266,"counts_floor":true,"next":"--uses=src/infra/sortutil.h:nonNegativeFloatDescKey"} +````` + +stderr: + +````` +ripwire edit: applied atomically; receipt carries region, blob_sha, edit_check, tests_to_run; next: --uses=src/infra/sortutil.h:nonNegativeFloatDescKey +````` + +## `./build/ripwire . --replace-symbol-body=DoesNotExist --edit-payload=/aux/payload_note.h` + +*An unknown TARGET refuses and leaves every file byte-identical.* + +**exit code: 1** — **wall time: 1.76s** + +````` +(empty) +````` + +stderr: + +````` +ripwire: --replace-symbol-body: symbol 'DoesNotExist' not found +````` + +## `./build/ripwire . --edit-plan=/aux/edit_plan.json --dry-run` + +*A versioned multi-edit TRANSACTION preflighted without writing: the receipt shows what each op would read and touch.* + +**wall time: 1.77s** + +Input file: + +````` +{ + "version": 1, + "edits": [ + { + "op": "insert_before_symbol", + "target": "nonNegativeFloatDescKey", + "payload": "plan_note.h" + } + ] +} +````` + +````` +{"schema":"ripwire.edit-plan/v1","mode":"dry-run","edits":1,"files":1,"callers_union":4,"graph_ambiguous":2911,"graph_unresolved":14064,"graph_unindexed":266,"counts_floor":true,"atomic_scope":"per-file","rollback_on_write_error":true,"recheck_before_each_write":true,"multifile_crash_atomic":false," … [line truncated: 353 more bytes on this line] +````` + +## `./build/ripwire . --edit-plan=/aux/edit_plan.json --apply` + +*The same plan committed: per-file locks, re-verify-before-write, atomic rename, rollback on a later failure.* + +**wall time: 3.91s** + +````` +{"schema":"ripwire.edit-plan/v1","mode":"apply","edits":1,"files":1,"callers_union":4,"graph_ambiguous":2911,"graph_unresolved":14064,"graph_unindexed":266,"counts_floor":true,"applied":1,"atomic_files":1,"atomic_scope":"per-file","rollback_on_write_error":true,"recheck_before_each_write":true,"mult … [line truncated: 537 more bytes on this line] +{"n":"benchAdaptive","p":"bench/bench_radix_ab.cpp:157","l":[162]}, +{"n":"radixSortNonNegativeFloatsDesc","p":"src/infra/sortutil.h:107","l":[116]}, +{"n":"radixSortByScoreDescId","p":"src/infra/sortutil.h:122","l":[182]}], +"graph_ambiguous":2911,"graph_unresolved":14064,"graph_unindexed":266,"counts_floor":true}}]} +````` + +## `./build/ripwire . --edit-plan=/aux/edit_plan.json` + +*Neither --dry-run nor --apply: the mode is explicit, so this refuses.* + +**exit code: 1** + +````` +(empty) +````` + +stderr: + +````` +ripwire: --edit-plan requires exactly one of --dry-run or --apply +````` + +## `./build/ripwire . --quality-delta` + +*After the agent's edits: the complexity/nesting rows on lessByScoreDescId are gone (the replace undid them), the rest still gate (exit 2).* + +**exit code: 2** — **wall time: 12.24s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [142 more display lines; full output is 17000 bytes on 1 raw line(s)] +````` + +stderr: + +````` +ripwire: no ./.ripwire_quality_baseline — auto-comparing the working tree vs git HEAD (commit the baseline with --quality-baseline to pin it) +ripwire: --quality-delta gating: 2 preexisting-worse major finding(s); first: duplication src/infra/sortutil.h::rw::sortutil::nonNegativeFloatAscKeyCopy | src/infra/sortutil.h::rw::sortutil::nonNegativeFloatDescKey at src/infra/sortutil.h:88 (was=0 now=59) +````` + +## `./build/ripwire . --quality-baseline` + +*REFUSES, exit 1: this sandbox tree is already regressed, and pinning here would swallow that debt into the floor so every later delta read clean. It names how many gating findings it would absorb, the first of them, and the way forward.* + +**exit code: 1** — **wall time: 7.25s** + +````` +(empty) +````` + +stderr: + +````` +ripwire: --quality-baseline: this tree already holds 2 gating finding(s) against HEAD — pinning here would absorb + them into the floor, and every later --quality-delta would read clean. First: duplication src/infra/sortutil.h::rw::sortutil::nonNegativeFloatAscKeyCopy | src/infra/sortutil.h::rw::sortutil::nonNegativeFloatDescKey (was=0 now=59) + Commit the tree first, or pass --allow-dirty to pin anyway (the sidecar then records the 2 absorbed, and every + report against it carries baseline_absorbed="2"). +````` + +## `./build/ripwire . --quality-baseline --allow-dirty` + +*The consent form: pin anyway. The sidecar is stamped with the dirty pin and the absorbed count, so the fact outlives the process that knew it.* + +**wall time: 10.66s** + +````` +(empty) +````` + +stderr: + +````` +ripwire: --quality-baseline --allow-dirty: pinned with 2 gating finding(s) ABSORBED into the floor (stamped in the sidecar; every --quality-delta against it carries baseline_absorbed="2") +ripwire: wrote ./.ripwire_quality_baseline (snapshot of 28702 per-symbol rows, 29599 indexed symbols total) +````` + +Artifact written: + +````` + 4867476 .ripwire_quality_baseline +# ripwire quality baseline v6 — regenerate with --quality-baseline; do not hand-edit +producer d48904697b489e550db3aa8caeb4115883b8b80eafebf4c4079a7682d33a8c59 +head eb541409e8fbff63c10f6e4192661251ec56de3c +dirty 1 +absorbed 2 +ccx 67c15011ee8b 0 +ccx d19d7c13cbc7 2 +ccx 1f7edbff929e5 10 +ccx 6a732f6699b +````` + +## `./build/ripwire . --quality-delta` + +*Against that sidecar the same tree reads regressions=0 — but baseline_absorbed= is on the root, so this green means clean SINCE THE PIN, never clean. A baseline is a floor YOU chose, and it belongs BEFORE the change.* + +**wall time: 11.53s** + +````` + + +````` + + +--- + +# the MCP dialect — the same verbs over stdio JSON-RPC (one-shot exchange, not a persistent server) + +## `printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"initialize"}' '{"jsonrpc":"2.0","id":2,"method":"tools/list"}' | ./build/ripwire --mcp` + +*initialize + tools/list: the manifest an agent host loads at session start — every verb's name, description and input schema.* + +````` +{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-11-25","serverInfo":{"name":"ripwire","version":"1.0"},"capabilities":{"tools":{},"resources":{}},"instructions":"Map before reading files. Start a new task with explore; use from_trace for an error; use impact plus uses before changing a sym … [line truncated: 738 more bytes on this line] +{"jsonrpc":"2.0","id":2,"result":{"tools":[{"name":"analyze","description":"Architecture map for a directory: signatures and the call graph for the top symbols. Use when landing cold in a repo or subdir, before reading files; for a task-scoped inventory use 'for', for one symbol's neighborhood find_ … [line truncated: 742 more bytes on this line] +{"name":"rank_by","description":"The SAME architecture map 'analyze' serves, ranked by a different signal instead of plain PageRank. rank_by = pagerank (default, omit it — the CLI's own unbiased --rank-by=pagerank; 'analyze' can rank differently on a tree with uncommitted changes, where it biases … [line truncated: 1077 more bytes on this line] +{"name":"find_symbol","description":"A symbol's 1-hop neighborhood: the symbol (with a fetch_body handle) plus direct callers (calledBy) and callees (calls). Full transitive reach: 'impact'. Read/write/import sites, not just calls: 'uses'. JSON {symbol, calledBy, calls, defs, count, hop_tested, hop_ … [line truncated: 1181 more bytes on this line] +{"name":"find_referencing_symbols","description":"Direct (1-hop) callers of a symbol, each with a fetch_body handle. For the full transitive blast radius use 'impact', for read/write/import sites 'uses'. JSON {symbol, calledBy, defs, count, hop_tested, hop_untested, declined_calls, counts_floor}; ca … [line truncated: 945 more bytes on this line] +{"name":"grep","description":"Trigram literal search; each hit annotated with its enclosing symbol (which function/class it is in). Hits carry text; enclosing rows a fetch_body handle. pattern = the literal; in=any lifts the span tiering (default: tightest non-empty tier; rest rides as suppressed_co … [line truncated: 837 more bytes on this line] +{"name":"cochange","description":"Files that historically change together with this file — the co-edit partners. Fowler's Shotgun Surgery as change coupling. dep_capable=false means neither side could carry one (sh/md/json/binary), so surprising is undefined rather than informative. file = the fil … [line truncated: 700 more bytes on this line] +{"name":"memory_recall","description":"Most relevant memory notes / docs for a task, full text — the few that matter, not the whole corpus. path = docs/memory dir; task = what you're working on; top_k = docs to return, 1..1000 (default 8), refused outside that band, never clamped; budget_tokens = … [line truncated: 790 more bytes on this line] +{"name":"situational_awareness","description":"The 5 things to know about a diff, as JSON: blast_radius, tests_to_run, forgotten (usual co-change partners missing from this diff), hotspot_alert, modules_touched. forgotten = the Shotgun Surgery check. diff/files optional — defaults to 'git diff HEA … [line truncated: 1297 more bytes on this line] +{"name":"mentions","description":"Docs (markdown plans/designs) that name a code symbol in a backtick. symbol = the code symbol name; limit/offset page the files. An @FILE:LINE line-seed rebinds to the innermost definition enclosing that line and answers for it, disclosing the rebound name as 'sym' … [line truncated: 625 more bytes on this line] +{"name":"for","description":"Task-lens ranked, signatures-only inventory of the building blocks most relevant to a task (cx=complexity, in=reuse-count). task = the task in plain words; budget_tokens = an optional soft row target (the CLI --for --token-budget). The header carries route= (which ranker … [line truncated: 1061 more bytes on this line] +{"name":"lego","description":"Interface-to-impls view for ONE named interface/base: its method contract plus EVERY implementor (own-language only), each with file path. Use when implementing against a KNOWN interface (contrast 'for', which sprays top interfaces for a task). type = interface/base nam … [line truncated: 841 more bytes on this line] +{"name":"owners","description":"Bus-factor: recency-weighted (6-month half-life) author ownership per file. symbol = optional, restricts to the file that defines it; limit/offset page the rows. at= is the commit these numbers were computed at (+dirty = the working tree differed). An @FILE:LINE line- … [line truncated: 879 more bytes on this line] +{"name":"replace_symbol_body","description":"Replace a symbol's ENTIRE definition (signature through closing brace) with new_body — splices over the full def span, preserving every byte outside it verbatim; new_body must be a complete, well-formed definition. One trailing newline folds (trailing_n … [line truncated: 1323 more bytes on this line] +{"name":"insert_before_symbol","description":"Insert text immediately BEFORE a symbol's definition (its first byte); padded to the file's own blank-line seam (separator_padded=N). Same refusal contract as replace_symbol_body (not found / ambiguous / stale index / symlink / concurrent write, file unc … [line truncated: 877 more bytes on this line] +{"name":"insert_after_symbol","description":"Insert text immediately AFTER a symbol's definition (past its final byte, which is preserved exactly); padded to the file's own blank-line seam (separator_padded=N); one trailing newline folds (trailing_newline_folded). Same refusal contract as replace_sy … [line truncated: 878 more bytes on this line] +{"name":"fetch_body","description":"Full (or partial-range) source of a symbol's definition, addressed by the stable `handle` a read verb attached to it (bodies on request, not by default). start_line/end_line are optional, 1-based, INCLUSIVE and BODY-RELATIVE (line 1 = the def's first line), clampe … [line truncated: 1049 more bytes on this line] +{"name":"exemplar","description":"BEFORE writing a function / method / class / struct / interface / variable, get the repo's single best-in-class instance of that kind to imitate — signature AND full body. chosen by ROLE, NEVER by text similarity to your task: candidates are first filtered to cogn … [line truncated: 1375 more bytes on this line] +{"name":"quality_delta","description":"Your PR self-check, run every time you think a change is DONE — pairs with the CLI-only --test-gate (names the tests to run + the untested blast radius; not MCP-exposed) to form the two-step pre-PR gate. Reports ONLY what your working tree made WORSE vs basel … [line truncated: 867 more bytes on this line] +{"name":"quality_baseline","description":"PIN the quality floor: writes .ripwire_quality_baseline stamped with the current git HEAD sha, snapshotting complexity / duplication / dead-code / API surface. Call once at the start of non-trivial work, then quality_delta compares against this pinned floor … [line truncated: 445 more bytes on this line] +{"name":"impact","description":"IS IT SAFE TO CHANGE X? — the TRANSITIVE blast radius of a symbol via calls, nearest first (d= hop depth), then PageRank. Use before modifying or deleting a symbol; it beats find_referencing_symbols (direct callers only), and 'uses' catches the read/write/import sit … [line truncated: 1325 more bytes on this line] +{"name":"uses","description":"The STATICALLY RESOLVABLE use-sites of a symbol, not just calls: role (call | read | write | import | extends), file:line, and enclosing symbol. Use to see the footprint before renaming or changing a name — find_referencing_symbols and impact follow only calls. extern … [line truncated: 1146 more bytes on this line] +{"name":"affected","description":"WHICH TESTS TO RUN for a change — test files that transitively reach the changed files/symbols, ranked by evidence (edited > partner-named > hop distance). files = changed files and/or symbols, comma-separated: each item is tried as an indexed PATH pattern first, … [line truncated: 1238 more bytes on this line] +{"name":"path_between","description":"Does A REACH B, and HOW? — the shortest directed CALL path between two symbols, hop-by-hop. reachable=\"0\" hops=\"0\" is a valid 'not reachable' answer — call edges are name-based, so a missing dynamic/callback edge can hide a real path. Named path_between … [line truncated: 709 more bytes on this line] +{"name":"connect","description":"When a task touches 2..16 named symbols, returns the minimal subgraph RELATING them - terminals, the fewest joining intermediaries (with signatures), and call edges in true direction - finding the shared-caller joins a directed path_between cannot; unrelated symbols … [line truncated: 950 more bytes on this line] +{"name":"explore","description":"ONE-call task orientation: the routed+anchored ranking, full bodies of the top hits, their 1-hop callers, field notes, and tests_to_run — ALL under one deterministic byte budget, in a fixed section order (ranking > bodies > callers > notes > tests) that degrades gr … [line truncated: 2135 more bytes on this line] +{"name":"from_trace","description":"Paste a stack trace / sanitizer report / compiler error and get it mapped onto indexed symbols, ranked INNERMOST-first: the parsed frame map, the ranked suspects' signatures, and the innermost in-corpus symbol's FULL body. Out-of-corpus frames are listed a … [line truncated: 1233 more bytes on this line] +{"name":"edit_check","description":"Just edited a symbol? Did its CONTRACT (param count + publicness) change vs git HEAD, and which 1-hop callers are NOW INCOMPATIBLE with the new arity by fixed-arity evidence (not a guess — every folded definition disagrees)? This is call sites worth OPENING, not … [line truncated: 1908 more bytes on this line] +{"name":"whereis","description":"WHERE DOES THIS CONTENT LIVE? Which branch's tree defines or mentions a symbol, HEAD first, with on-head=0 naming the case this verb exists for: content that lives only on a branch (a finished fix stranded on 1 of 30 refs). Each distinct blob is read once (content-ad … [line truncated: 1475 more bytes on this line] +{"name":"stray_content","description":"Per branch: the lines its own divergent work AUTHORED (vs its merge-base with HEAD) that the live line does NOT have. Four verdicts (unmerged+superseded+merged+unknown=refs): v=unmerged is genuinely absent; v=superseded means the live line re-implemented the wo … [line truncated: 1123 more bytes on this line] +{"name":"flags","description":"WHAT IS BUILT BUT DARK here — the answer to 'why don't I see feature X?'. Harvests all three gate patterns (ifndef/define header gates, CMake option(), getenv reads) with each gate's kind, DEFAULT, the size of the code it guards, and its read sites. When a name is bo … [line truncated: 1434 more bytes on this line] +{"name":"doc_drift","description":"WHICH OF THIS REPO'S DOC CLAIMS ARE NOW FALSE. Verifies the CHECKABLE anchors in every markdown file against the live index and returns ONLY the ones that no longer hold: file:line refs (missing-file / past-eof / line-moved), backticked symbol mentions (undefined), … [line truncated: 1167 more bytes on this line] +{"name":"slice","description":"WHERE IS THIS VARIABLE DEFINED AND USED inside one function — NAME-BASED intra-procedural def-use rows of one variable inside ONE uniquely-resolved definition (the ARISE slicer, arXiv:2605.03117). symbol alone lists the sliceable locals to pick from; add var (or spel … [line truncated: 1722 more bytes on this line] +{"name":"batch","description":"ONE-TURN CONTEXT SWEEP: answer up to 16 heterogeneous READ sub-queries in a single call (the deterministic $0 counterpart of a parallel-search agent). queries = array over the SAME path, in EITHER grammar: {verb, ...args} objects, or the CLI --batch file's own \"verb:a … [line truncated: 2040 more bytes on this line] +````` + +The manifest, summarised (name / description bytes / required args) — what the host pays in context every session: + +````` +tools= 33 manifest_bytes= 47450 (~tokens at 4 bytes/token: 11862 ) +explore desc_bytes= 1340 schema_bytes= 958 required=['task'] +batch desc_bytes= 1235 schema_bytes= 962 required=['queries'] +edit_check desc_bytes= 1180 schema_bytes= 889 required=['symbol'] +slice desc_bytes= 953 schema_bytes= 932 required=['symbol'] +whereis desc_bytes= 701 schema_bytes= 941 required=['symbol'] +flags desc_bytes= 881 schema_bytes= 720 required=[] +exemplar desc_bytes= 954 schema_bytes= 591 required=[] +replace_symbol_body desc_bytes= 865 schema_bytes= 610 required=['symbol', 'new_body'] +impact desc_bytes= 821 schema_bytes= 666 required=['symbol'] +situational_awareness desc_bytes= 865 schema_bytes= 581 required=[] +affected desc_bytes= 790 schema_bytes= 610 required=['files'] +from_trace desc_bytes= 836 schema_bytes= 559 required=['trace'] +find_symbol desc_bytes= 752 schema_bytes= 590 required=['symbol'] +doc_drift desc_bytes= 728 schema_bytes= 604 required=[] +uses desc_bytes= 626 schema_bytes= 682 required=['symbol'] +stray_content desc_bytes= 678 schema_bytes= 604 required=[] +rank_by desc_bytes= 758 schema_bytes= 481 required=[] +for desc_bytes= 453 schema_bytes= 777 required=['task'] +fetch_body desc_bytes= 623 schema_bytes= 588 required=['handle'] +connect desc_bytes= 535 schema_bytes= 580 required=['symbols'] +find_referencing_symbols desc_bytes= 505 schema_bytes= 590 required=['symbol'] +insert_after_symbol desc_bytes= 458 schema_bytes= 577 required=['symbol', 'text'] +insert_before_symbol desc_bytes= 455 schema_bytes= 578 required=['symbol', 'text'] +owners desc_bytes= 425 schema_bytes= 625 required=[] +quality_delta desc_bytes= 833 schema_bytes= 193 required=[] +lego desc_bytes= 360 schema_bytes= 651 required=['type'] +grep desc_bytes= 445 schema_bytes= 562 required=['pattern'] +memory_recall desc_bytes= 447 schema_bytes= 500 required=['task'] +path_between desc_bytes= 357 schema_bytes= 506 required=['from', 'to'] +cochange desc_bytes= 388 schema_bytes= 476 required=['file'] +analyze desc_bytes= 546 schema_bytes= 318 required=[] +mentions desc_bytes= 305 schema_bytes= 486 required=['symbol'] +quality_baseline desc_bytes= 407 schema_bytes= 193 required=[] +````` + +## `printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"initialize"}' '{"jsonrpc":"2.0","id":2,"method":"tools/list"}' | ./build/ripwire --mcp --mcp-tools=core` + +*--mcp-tools=core: the server lists only the core profile's tools (explore, batch, from_trace, impact, uses, fetch_body, edit_check, quality_delta), so a host that loads every schema at session start pays only for those.* + +````` +{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-11-25","serverInfo":{"name":"ripwire","version":"1.0"},"capabilities":{"tools":{},"resources":{}},"instructions":"Map before reading files. Start a new task with explore; use from_trace for an error; use impact plus uses before changing a sym … [line truncated: 955 more bytes on this line] +{"jsonrpc":"2.0","id":2,"result":{"tools":[{"name":"fetch_body","description":"Full (or partial-range) source of a symbol's definition, addressed by the stable `handle` a read verb attached to it (bodies on request, not by default). start_line/end_line are optional, 1-based, INCLUSIVE and BODY-RELAT … [line truncated: 1092 more bytes on this line] +{"name":"quality_delta","description":"Your PR self-check, run every time you think a change is DONE — pairs with the CLI-only --test-gate (names the tests to run + the untested blast radius; not MCP-exposed) to form the two-step pre-PR gate. Reports ONLY what your working tree made WORSE vs basel … [line truncated: 867 more bytes on this line] +{"name":"impact","description":"IS IT SAFE TO CHANGE X? — the TRANSITIVE blast radius of a symbol via calls, nearest first (d= hop depth), then PageRank. Use before modifying or deleting a symbol; it beats find_referencing_symbols (direct callers only), and 'uses' catches the read/write/import sit … [line truncated: 1325 more bytes on this line] +{"name":"uses","description":"The STATICALLY RESOLVABLE use-sites of a symbol, not just calls: role (call | read | write | import | extends), file:line, and enclosing symbol. Use to see the footprint before renaming or changing a name — find_referencing_symbols and impact follow only calls. extern … [line truncated: 1146 more bytes on this line] +{"name":"explore","description":"ONE-call task orientation: the routed+anchored ranking, full bodies of the top hits, their 1-hop callers, field notes, and tests_to_run — ALL under one deterministic byte budget, in a fixed section order (ranking > bodies > callers > notes > tests) that degrades gr … [line truncated: 2135 more bytes on this line] +{"name":"from_trace","description":"Paste a stack trace / sanitizer report / compiler error and get it mapped onto indexed symbols, ranked INNERMOST-first: the parsed frame map, the ranked suspects' signatures, and the innermost in-corpus symbol's FULL body. Out-of-corpus frames are listed a … [line truncated: 1233 more bytes on this line] +{"name":"edit_check","description":"Just edited a symbol? Did its CONTRACT (param count + publicness) change vs git HEAD, and which 1-hop callers are NOW INCOMPATIBLE with the new arity by fixed-arity evidence (not a guess — every folded definition disagrees)? This is call sites worth OPENING, not … [line truncated: 1908 more bytes on this line] +{"name":"batch","description":"ONE-TURN CONTEXT SWEEP: answer up to 16 heterogeneous READ sub-queries in a single call (the deterministic $0 counterpart of a parallel-search agent). queries = array over the SAME path, in EITHER grammar: {verb, ...args} objects, or the CLI --batch file's own \"verb:a … [line truncated: 2040 more bytes on this line] +````` + +The core manifest, summarised (name / description bytes / required args): + +````` +tools= 8 manifest_bytes= 14191 (~tokens at 4 bytes/token: 3547 ) +explore desc_bytes= 1340 schema_bytes= 958 required=['task'] +batch desc_bytes= 1235 schema_bytes= 962 required=['queries'] +edit_check desc_bytes= 1180 schema_bytes= 889 required=['symbol'] +impact desc_bytes= 821 schema_bytes= 666 required=['symbol'] +from_trace desc_bytes= 836 schema_bytes= 559 required=['trace'] +uses desc_bytes= 626 schema_bytes= 682 required=['symbol'] +fetch_body desc_bytes= 623 schema_bytes= 588 required=['handle'] +quality_delta desc_bytes= 833 schema_bytes= 193 required=[] +````` + +## `printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"initialize"}' '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"for","arguments":{"path":".","task":"pagerank power iteration"}}}' | ./build/ripwire --mcp` + +*MCP `for`: always bundle=sigs (never the CLI's compact route), the same ranked signatures as --for.* + +**wall time: 1.53s** + +````` +{"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-11-25","serverInfo":{"name":"ripwire","version":"1.0"},"capabilities":{"tools":{},"resources":{}},"instructions":"Map before reading files. Start a new task with explore; use from_trace for an error; use impact plus uses before changing a sym … [line truncated: 738 more bytes on this line] +{"jsonrpc":"2.0","id":2,"result":{"content":[{"type":"text","text":". window: shown= capped= (capped=1 cut). at=: commit+dirty+shallow. next= … [line truncated: 6520 more bytes on this line] +"_index":"[index: files=3102 symbols=29597 hash=8b4438fa]","_fresh":"ok"}} +{"jsonrpc":"2.0","id":3,"result":{"content":[{"type":"text","text":""; } +inline std::string viaLinkLegendComment() +{ + return std::string( ""; +} // …and the MAP's spelling, as its own comment: the same three facts as kViaNameLegend in the fewest bytes (a map prints the // most hedged rows of any answer, and its --max-tokens fit pays for every legend byte in rows), plus x=, the count a merged // row carries (serialize.h writeMapCalleeRows), and where each merged candidate is listed. No `--` inside an XML comment: // the flag is named callees=. inline constexpr std::string_view kMapViaNameLegend = ""; +inline constexpr std::string_view kMapViaLinkLegend = + ""; +inline constexpr std::string_view kMapAnchorMissLegend = + ""; +inline std::string anchorMissLegendComment() +{ + return std::string( ""; +} // …and MCP's spelling: the same sentence as the `via_note` key of an answer one of whose entries carries "via":"name". -inline std::string viaNameNoteJson() +inline std::string viaFactNoteJson( std::string text, std::string_view from, std::string_view jsonSpelling, std::string_view key ) { - std::string text = kViaNameLegend; - for( std::size_t at = text.find( "via=\"name\"" ); at != std::string::npos; at = text.find( "via=\"name\"", at ) ) + for( std::size_t at = text.find( from ); at != std::string::npos; at = text.find( from, at ) ) { - text.replace( at, 10, "\\\"via\\\":\\\"name\\\"" ); // via="name" -> \"via\":\"name\" inside the JSON string - at += 16; + text.replace( at, from.size(), jsonSpelling ); + at += jsonSpelling.size(); } if( const std::size_t at = text.find( "on a row" ); at != std::string::npos ) { @@ -1241,7 +1275,19 @@ inline std::string viaNameNoteJson() { text.pop_back(); } - return ",\"via_note\":\"" + text + "\""; + return ",\"" + std::string( key ) + "\":\"" + text + "\""; +} +inline std::string viaNameNoteJson() +{ + return viaFactNoteJson( kViaNameLegend, "via=\"name\"", "\\\"via\\\":\\\"name\\\"", "via_note" ); +} +inline std::string viaLinkNoteJson() +{ + return viaFactNoteJson( kViaLinkLegend, "via=\"link\"", "\\\"via\\\":\\\"link\\\"", "via_link_note" ); +} +inline std::string anchorMissNoteJson() +{ + return viaFactNoteJson( kAnchorMissLegend, "anchor=\"missing\"", "\\\"anchor\\\":\\\"missing\\\"", "anchor_missing_note" ); } // The same fact in --for's two dialects, which are prose strips joined by ';' and ':' rather than XML comment diff --git a/src/infra/mdslug.h b/src/infra/mdslug.h new file mode 100644 index 000000000..24153b6ed --- /dev/null +++ b/src/infra/mdslug.h @@ -0,0 +1,62 @@ +#pragma once + +// mdslug.h — the ONE GitHub-style anchor slug of a markdown heading name. +// +// Two stages need it and neither can include the other: ingest_docs.h resolves an in-file [text](#slug) at +// extraction, and graph.h resolves a cross-file [text](other.md#slug) once every heading in the repo is a +// symbol. A leaf header keeps them on one definition (a second copy is the clone --quality-delta flags). + +#include +#include +#include + +namespace rw +{ + +// alnum lowered, spaces→'-', '-'/'_' kept, everything else dropped. First match wins on collision +// (GitHub's -1/-2 suffixes, percent-encoded slugs and headings with inline markup are not modelled; such a link falls +// back to the target file's own edge — disclosed here rather than guessed, and never marked anchor="missing": see +// mdSlugOutsideModel, mdHeadingOutsideModel and graph.h mdSlugProvablyAbsent). +inline std::string mdSlugOf( std::string_view name ) +{ + std::string slug; + slug.reserve( name.size() ); + for( const char c : name ) + { + if( ( c >= 'a' && c <= 'z' ) || ( c >= '0' && c <= '9' ) || c == '_' || c == '-' ) { slug += c; } + else if( c >= 'A' && c <= 'Z' ) { slug += char( c - 'A' + 'a' ); } + else if( c == ' ' || c == '\t' ) { slug += '-'; } + } + return slug; +} + +// A link slug this function cannot decide about: a byte >= 0x80 (it drops non-ASCII letters, GitHub keeps them) or a '%' +// (an encoded slug; it does not decode). A miss on such a slug proves nothing about the heading. +inline bool mdSlugOutsideModel( std::string_view slug ) noexcept +{ + return std::any_of( slug.begin(), slug.end(), []( char c ) { return c == '%' || static_cast( c ) >= 0x80; } ); +} + +// A heading name whose slug mdSlugOf cannot give as GitHub does: GitHub slugs the RENDERED text, mdSlugOf the raw source. +// `[`/`<`/`&` open a link, a tag or an entity (`## See [docs](x.md)` renders "See docs", `see-docs`; the raw slug reads +// see-docsxmd), `*` and a `_` that is not inside a word open emphasis (`## _Setup_` renders "Setup", `setup`; the raw slug +// keeps the underscores). A snake_case word is no emphasis. A file with such a heading proves nothing by a slug miss. +inline bool mdHeadingOutsideModel( std::string_view name ) noexcept +{ + const auto word = []( char c ) { return ( c >= 'a' && c <= 'z' ) || ( c >= 'A' && c <= 'Z' ) || ( c >= '0' && c <= '9' ); }; + for( std::size_t i = 0; i < name.size(); ++i ) + { + const char c = name[ i ]; + if( c == '[' || c == '<' || c == '&' || c == '*' ) + { + return true; + } + if( c == '_' && !( i > 0 && i + 1 < name.size() && word( name[ i - 1 ] ) && word( name[ i + 1 ] ) ) ) + { + return true; + } + } + return false; +} + +} // namespace rw diff --git a/src/ingest.cpp b/src/ingest.cpp index bb300fe5f..cddb26107 100644 --- a/src/ingest.cpp +++ b/src/ingest.cpp @@ -192,6 +192,7 @@ extern "C" #include "handlershape.h" // --quality-delta's handler/placeholder shapes — AstWalk::HandlerShapes rides the shared file walk (reads nodeTextOf above) #include "ingest_relations.h" #include "ingest_jsimports.h" +#include "infra/mdslug.h" // mdSlugOf — shared with graph.h (the cross-file link bind) #include "ingest_docs.h" #include "ingest_names.h" #include "ingest_binds.h" diff --git a/src/ingest_cache.h b/src/ingest_cache.h index 703b9dd9e..8b44965c6 100644 --- a/src/ingest_cache.h +++ b/src/ingest_cache.h @@ -345,11 +345,17 @@ constexpr std::uint32_t kCacheVersion = 36; // 36: train 30 — strict // (Py `pkg.mod`, TS `./x`, Rust `crate::a::b`/`mod:x`) — // a target FORMAT change → old caches must be rejected. // 4: Include gained a `bool isAngle` (quote/angle) field -constexpr std::uint32_t kParserVer = 198; // bump on any grammar/.scm/extraction change +constexpr std::uint32_t kParserVer = 206; // bump on any grammar/.scm/extraction change + // 206 = 2026-10-18 (#409, PR #414, renumbered again at merge with main 939dbb69): strictly above every + // live number — main 196 (rich 197), train 31 198 (199), lane rust-receivers 202 (203), lane ix1-slot + // 204 (205). Rich file tag 207. kCacheVersion stays 36 (no record format change). // 198 = 2026-10-18 (train 31): strictly above every number a live build used — main 196 (rich 197), // lane h1-annot 194 (195); body-fold, hrr, callers-band2, dangling-warnings and ix2-slice kept // their base value 184. Rich file tag 199. kCacheVersion stays 36: no lane changed a record format // (h1-annot appends two LocalBindKind values; no lane touched a cache serializer). + // 185 = 2026-10-18 (#409): a cross-file markdown [text](other.md#slug) keeps the link as + // written (RawRef::memberPath, unbound) so graph.h pins the exact file + heading. + // 185, not 184: 184 is train 29. Rich file tag 186. kCacheVersion stays 34. // 196 = 2026-10-18 (train 30): strictly above every number a live build used — main 184 (rich 185), // lane call-result-typing 180 (rich 181), lane fcall-forward 188 (189), lane go-local-var 190 (191), // lane name-template-dispatch 192 (193), lane h1-annot (train 31) 194 (195); tail-sym and diff --git a/src/ingest_docs.h b/src/ingest_docs.h index 6af4b4bc9..f0e88c4e9 100644 --- a/src/ingest_docs.h +++ b/src/ingest_docs.h @@ -40,7 +40,9 @@ namespace // (5) links → edges: [[slug]] (the agent-memory/Obsidian convention) and [text](other.md) / // [label]: other.md → a ref named by the target's stem (the resolve ladder's same-dir preference // lands it on the file node); [text](#anchor) → a ref named by the matching heading in THIS file -// (GitHub slugify; same-file preference wins) — a doc-section→doc-section edge. Dangling links drop. +// (GitHub slugify; same-file preference wins) — a doc-section→doc-section edge. A cross-file +// [text](other.md#anchor) keeps the stem on the ref and the link as written in memberPath; graph.h +// pins it to that exact file's heading (the file node when the heading is absent). Dangling links drop. // Determinism: the walk is a preorder over the AST (byte order); pending anchors resolve in byte order. // Names are XML-escaped downstream; CR bytes never enter a name, embedded newlines flatten to spaces. namespace mdtier @@ -79,21 +81,8 @@ inline std::string mdCleanHeadingText( std::string_view raw ) return name; } -// GitHub anchor slug of a heading name: alnum lowered, spaces→'-', '-'/'_' kept, everything else -// dropped. First match wins on collision (GitHub's -1/-2 suffixes are not modelled; a collided anchor -// resolves to the first heading — disclosed here rather than guessed). -inline std::string mdSlugOf( std::string_view name ) -{ - std::string slug; - slug.reserve( name.size() ); - for( const char c : name ) - { - if( ( c >= 'a' && c <= 'z' ) || ( c >= '0' && c <= '9' ) || c == '_' || c == '-' ) { slug += c; } - else if( c >= 'A' && c <= 'Z' ) { slug += char( c - 'A' + 'a' ); } - else if( c == ' ' || c == '\t' ) { slug += '-'; } - } - return slug; -} +// GitHub anchor slug of a heading name: infra/mdslug.h (shared with graph.h's cross-file link bind). +using rw::mdSlugOf; inline void mdWalk( TSNode node, std::string_view src, bool inQuote, std::uint32_t depth, MdWalkOut& out ) { @@ -202,6 +191,23 @@ inline void mdWalk( TSNode node, std::string_view src, bool inQuote, std::uint32 } // namespace mdtier +inline void lowercaseMarkdownSlug( std::string& written ) +{ + const std::size_t frag = written.find( '#' ); + if( frag == std::string::npos ) + { + return; + } + for( std::size_t k = frag + 1; k < written.size(); ++k ) + { + char& c = written[ k ]; + if( c >= 'A' && c <= 'Z' ) + { + c = char( c - 'A' + 'a' ); + } + } +} + void extractMarkdown( std::uint32_t fileId, std::string_view src, std::string_view stem, TSNode root, std::vector& defs, std::vector& refs ) { @@ -284,8 +290,9 @@ void extractMarkdown( std::uint32_t fileId, std::string_view src, std::string_vi }; // link-target handling shared by inline links, reference definitions and (via slug) anchors. - // Cross-file: [x](other.md) / [x](../a/other.md#frag) → ref named by the target's STEM (the resolve - // ladder's same-dir preference lands it on other.md's file node, exactly like [[other]]). Anchors: + // Cross-file: [x](other.md) / [x](../a/other.md) → ref named by the target's STEM (the resolve + // ladder's same-dir preference lands it on other.md's file node, exactly like [[other]]). + // The ref also carries the link as written (memberPath) so buildGraph can pin the EXACT file and heading. Anchors: // [x](#slug) → pending, resolved against THIS file's headings once they are all known. std::vector> pendingAnchors; // (refByte, slug) const auto emitLinkTarget = [ & ]( std::string_view target, std::uint32_t refByte ) @@ -309,9 +316,10 @@ void extractMarkdown( std::uint32_t fileId, std::string_view src, std::string_vi pendingAnchors.emplace_back( refByte, std::move( slug ) ); return; } + const std::string_view writtenTarget = target; // path#slug as written, for the exact bind in buildGraph if( const std::size_t frag = target.find( '#' ); frag != std::string_view::npos ) { - target = target.substr( 0, frag ); // cross-file anchor part deferred (stem edge only, disclosed) + target = target.substr( 0, frag ); } const bool isMd = target.size() > 3 && ( target.ends_with( ".md" ) || target.ends_with( ".markdown" ) ); if( !isMd ) @@ -326,6 +334,11 @@ void extractMarkdown( std::uint32_t fileId, std::string_view src, std::string_vi RawRef r; r.fileId = fileId; r.startByte = refByte; r.lang = Lang::Markdown; r.isInherit = false; r.name.assign( base ); + // The link as written (path + optional #slug; the slug is lowered, the path is not). It is a fact of THIS file alone, so it is + // safe in the cache; the (file, slug) heading it names is resolved in buildGraph, where every file's + // headings exist (graph.h bindMarkdownLinks). buildGraph reads this path. A member call still reads it beside memberCall. + r.memberPath.assign( writtenTarget ); + lowercaseMarkdownSlug( r.memberPath ); refs.push_back( std::move( r ) ); } }; @@ -503,6 +516,7 @@ void extractMarkdown( std::uint32_t fileId, std::string_view src, std::string_vi } } } + } // namespace — ingest_docs.h section of ingest.cpp } // namespace rw diff --git a/src/langfacts.h b/src/langfacts.h index 160734514..d3961171c 100644 --- a/src/langfacts.h +++ b/src/langfacts.h @@ -77,6 +77,9 @@ struct LangFacts /// (verbs_quality.h; pinned by test/qddegradecheck.sh (E3)). bool defectShapeNeedsMacroSet = false; + /// Markdown: a link targets a section, so shared graph code treats this language's sections as link targets (graph.h). + bool linkSections = false; + /// C, C++, Objective-C: a function span the parser reports DIRECTLY inside a function's span is read as a misread (htop's /// `else if (…) {` inside a macro-heavy body read as a function named `if`), never a closure (src/closures.h; this fact /// decides only the closure relation, no symbol or edge). A lambda or a block is anonymous, and a C++ local class's @@ -93,7 +96,7 @@ inline constexpr std::array kLangFacts = { { { .lang = Lang::Rust }, { .lang = Lang::Swift }, { .lang = Lang::ObjC, .noNestedNamedFunctions = true }, - { .lang = Lang::Markdown }, + { .lang = Lang::Markdown, .linkSections = true }, { .lang = Lang::JavaScript }, { .lang = Lang::Bash }, { .lang = Lang::Java }, diff --git a/src/main.cpp b/src/main.cpp index b2c399d90..f80d1ee76 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -2444,7 +2444,7 @@ int runDefaultMap( const MainDispatch& d ) { bodiesSection = rw::chargeSection( [ & ]( std::FILE* f ) { expandRecord = EmittedBodies{}; // a re-render starts from empty: the record is what THIS render emitted - packBodies( f, ing, expandNodes, cfg.packBudgetBytes, g.outOff, g.outTargets, g.outNameOnly, cfg.compress, redactPtr, + packBodies( f, ing, expandNodes, cfg.packBudgetBytes, g.outOff, g.outTargets, rw::withAnchorEdge( rw::CalleeEdgeBytes( g.outNameOnly, nullptr, nullptr ), &g.outAnchorMiss ), cfg.compress, redactPtr, expandRanges.empty() ? nullptr : &expandRanges, d.notesPtr, /*outEmitted=*/&expandRecord, /*truncateOversizedFirst=*/true, /*withFileContext=*/true, mapRootArg, &calleeOrder ); }, // V1: octocode F2 sibs=/inc= rw::kBytesPerTokenBody ); @@ -2826,7 +2826,7 @@ int runDefaultMap( const MainDispatch& d ) // not as the literal source token, for that same reason (the count is a naive grep over this file). if( !expandNodes.empty() && noteAppliesToBundle ) // !serveWholeFile is this whole branch's precondition (see the if above) { - emitSection( bodiesSection, [ & ]{ packBodies( out, ing, expandNodes, cfg.packBudgetBytes, g.outOff, g.outTargets, g.outNameOnly, cfg.compress, redactPtr, + emitSection( bodiesSection, [ & ]{ packBodies( out, ing, expandNodes, cfg.packBudgetBytes, g.outOff, g.outTargets, rw::withAnchorEdge( rw::CalleeEdgeBytes( g.outNameOnly, nullptr, nullptr ), &g.outAnchorMiss ), cfg.compress, redactPtr, expandRanges.empty() ? nullptr : &expandRanges, d.notesPtr, /*outEmitted=*/nullptr, /*truncateOversizedFirst=*/true, /*withFileContext=*/true, mapRootArg, &calleeOrder ); } ); bodiesEmittedEarly = true; @@ -2900,7 +2900,7 @@ int runDefaultMap( const MainDispatch& d ) } if( !expandNodes.empty() && !serveWholeFile && !bodiesEmittedEarly ) // #289: already served ahead of the map below { - emitSection( bodiesSection, [ & ]{ packBodies( out, ing, expandNodes, cfg.packBudgetBytes, g.outOff, g.outTargets, g.outNameOnly, cfg.compress, redactPtr, + emitSection( bodiesSection, [ & ]{ packBodies( out, ing, expandNodes, cfg.packBudgetBytes, g.outOff, g.outTargets, rw::withAnchorEdge( rw::CalleeEdgeBytes( g.outNameOnly, nullptr, nullptr ), &g.outAnchorMiss ), cfg.compress, redactPtr, expandRanges.empty() ? nullptr : &expandRanges, d.notesPtr, /*outEmitted=*/nullptr, /*truncateOversizedFirst=*/true, /*withFileContext=*/true, mapRootArg, &calleeOrder ); } ); // L3: --expand bodies surface notes; V1: sibs=/inc= } diff --git a/src/mcpverbs.h b/src/mcpverbs.h index 87e8b6822..d4aa4bdea 100644 --- a/src/mcpverbs.h +++ b/src/mcpverbs.h @@ -723,6 +723,38 @@ inline std::string symbolQueryDefTableJson( const IngestResult& ing, const DefGr return out; } +inline std::string symbolQueryNextField( bool referencingOnly, const PageWindow& pwPrimary, std::size_t rowTotal, const McpPageArgs& page, + std::string_view name, std::string_view nextSelector ) +{ + // L8 + C2 rider: a capped page names the next page, the direction's own follow-up beside it (the CLI root's pair); any + // other answer names the follow-up alone — P3 (L7): the CLI root's next= (mcpattrparitycheck). + const std::string hintCall = nextFlag( referencingOnly ? "--uses=" : "--expand=", nextSelector ); + if( pwPrimary.end < rowTotal ) + { + return nextFieldJson( callHierarchyPageNext( referencingOnly, name, page.limit, pwPrimary.end ) ) + + nextFieldJson( hintCall, callHierarchyHintAttr( referencingOnly ) ); + } + return nextFieldJson( hintCall ); +} + +inline std::string symbolQueryNotes( bool anyVia, bool anyLink, bool anyMiss ) +{ + std::string out; + if( anyVia ) + { + out += viaNameNoteJson(); + } + if( anyLink ) + { + out += viaLinkNoteJson(); + } + if( anyMiss ) + { + out += anchorMissNoteJson(); + } + return out; +} + inline std::string symbolQueryJson( const std::string& root, const std::string& name, bool referencingOnly, McpPageArgs page = {} ) { @@ -754,10 +786,18 @@ inline std::string symbolQueryJson( const std::string& root, const std::string& const HopTestedPartition chTested = computeHopTestedPartition( ing, g, referencingOnly ? calledBy : calls ); bool anyVia = false; // FE-B: an entry carried "via":"name" — the answer then carries via_note, its reading + bool anyLink = false; // an entry carried "via":"link" — the answer then carries via_link_note + bool anyMiss = false; // an entry carried "anchor":"missing" — the answer then carries anchor_missing_note + const std::span callerMatches( referencingOnly ? chRows.matches : chCallers.matches ); + const std::span calleeMatches( chRows.matches ); + NodeId linkRow = kNoNode; + bool linkToward = false; RowCallSites calledBySites{}; // CALLSITE-AT: the calledBy window's sites (set once the window is known, below) const auto symObj = [ & ]( NodeId id, bool via, const std::string& attrib = std::string() ) -> std::string { anyVia = anyVia || via; + const char* linkFrag = linkJsonFrag( g, callerMatches, calleeMatches, id, linkRow, linkToward, anyVia, anyLink ); + const char* missFrag = anchorMissJsonFrag( g, callerMatches, calleeMatches, id, linkRow, linkToward, anyMiss ); const Symbol& s = ing.symbols[id]; // T4: attach the stable content-handle so the agent can `fetch_body{handle}` instead of us re-sending // the body. Names/signatures by default (this verb); bodies by handle on request. @@ -771,6 +811,8 @@ inline std::string symbolQueryJson( const std::string& root, const std::string& + ",\"handle\":\"" + mcpdetail::jsonEscape( handleFor( ix, id ) ) + "\"" + ( rw::isTestedByReach( ing, chTested.testReach, id ) ? ",\"tested\":true" : "" ) + ( via ? ",\"via\":\"name\"" : "" ) // FE-B: the CLI row's via="name", the same edge bit (defattrib.h, one pass) + + linkFrag + + missFrag + attrib // attrib-multidef: the CLI row's of_def= / prov="split" + "}"; }; @@ -786,6 +828,8 @@ inline std::string symbolQueryJson( const std::string& root, const std::string& { if( i != w.begin ) { a += ","; } anySplit = anySplit || at.splitArm( i ); + linkRow = ids[ i ]; + linkToward = callersSide; // CALLSITE-AT: a calledBy row carries its call sites (the CLI --callers row's sites_at=); a calls row never does. a += withRowSites( symObj( ids[i], at.nameOnly( i ), ofDefKeyJson( at.groups, at.tokens, i ) + splitArmKeyJson( at.splitArm( i ) ) ), calledBySites, i - w.begin, callersSide ); @@ -839,20 +883,6 @@ inline std::string symbolQueryJson( const std::string& root, const std::string& // the UNION of all of their neighbours), count= = the un-windowed row total, hop_tested/hop_untested = // the partition over that full set. const auto [ chNextSelector, chNextIsBare ] = callHierarchyNextSelector( ing, chRows, name, referencingOnly ); - // L8 + C2 rider: a capped page names the next page, the direction's own follow-up beside it (the CLI root's pair); any - // other answer names the follow-up alone — P3 (L7): the CLI root's next= (mcpattrparitycheck). fix round 1 (B1): built - // here as its own statement, not as a ?: of string temporaries inside the root's one long concatenation below. - const std::string sqHintCall = nextFlag( referencingOnly ? "--uses=" : "--expand=", chNextSelector ); - std::string sqNextKeys; - if( pwPrimary.end < rowTotal ) - { - sqNextKeys = nextFieldJson( callHierarchyPageNext( referencingOnly, name, page.limit, pwPrimary.end ) ); - sqNextKeys += nextFieldJson( sqHintCall, callHierarchyHintAttr( referencingOnly ) ); - } - else - { - sqNextKeys = nextFieldJson( sqHintCall ); - } // 0.6.6 command sweep: find_symbol carries TWO arrays, and count/hop_tested/hop_untested describe `calls` // only — "count":0 sat beside "calledBy_total":67 on a leaf and read as a contradiction. The CLI key names // stay (mcpattrparitycheck), and count_of names the array they total, right beside them @@ -866,7 +896,7 @@ inline std::string symbolQueryJson( const std::string& root, const std::string& + declinedSiteCutKeyJson( sqDcCut ) // the CLI root's dc_shown= / dc_next=, absent when nothing was cut + declinedIfaceKeyJson( chRows.declinedIface ) // the CLI root's declined_iface= (callers direction only) + stdMemberCallsSpelling( chRows.stdCalls, /*json=*/true ) // the CLI callees answer's line (callees direction only; absent when none) - + sqNextKeys; // next= (and on a capped page its uses_next / expand_next), built above + + symbolQueryNextField( referencingOnly, pwPrimary, rowTotal, page, name, chNextSelector ); // P3 (L7): the CLI root's next= (mcpattrparitycheck) if( !referencingOnly && chRows.bodylessDefs > 0 ) { out += ",\"bodyless_defs\":" + std::to_string( chRows.bodylessDefs ); @@ -899,10 +929,7 @@ inline std::string symbolQueryJson( const std::string& root, const std::string& { out += ",\"calls\":" + rowArray( calls, pwPrimary, false ); } - if( anyVia ) - { - out += rw::viaNameNoteJson(); // FE-B: the reading of "via":"name", only in an answer that carries it - } + out += symbolQueryNotes( anyVia, anyLink, anyMiss ); out += nextFieldJson( splitArmLegend( anySplit ), "split_note" ); // attrib-multidef Q1: likewise for "prov":"split" out += declinedSitesJson( sqDc, referencingOnly, /*mcp=*/true ); // lane declined-sites: the CLI rows, never in calls/calledBy or count out += nextFieldJson( declinedSitesLegend( declinedSitesLegendFlags( sqDc, referencingOnly, sqDcCut ), /*mcp=*/true ), "declined_note" ); // their reading, only when they ride @@ -2237,7 +2264,7 @@ inline McpForAssembly assembleMcpFor( const McpForAssemblyInputs& in, std::strin share = forSoftSectionsShare( budgetBytesForTokens( budgetTokens ), mcpHdrXml.size() + atCut + legoStr.size() + composeStr.size() + routeStr.size() ); } mcpSlot = renderForBodySlot( BodySlotInputs{ ing, in.g.outOff, in.g.outTargets, in.g.outNameOnly, lensRank, flRootArg, redact, notesPtr, - /*viaLegendInHead=*/false, /*compress=*/false, &in.g.outProv, /*splitLegendInHead=*/false }, + /*viaLegendInHead=*/false, /*compress=*/false, &in.g.outProv, /*splitLegendInHead=*/false, &in.g.outAnchorMiss }, in.slotOwner, share ); } mcpForSoftRefill( in, renderSigs, sigsStr, mcpSigsCut, &mcpSigsNext, @@ -3193,11 +3220,13 @@ inline std::optional impactText( const std::string& root, const std } const DefTableListing imListing = defTableListing( imAttr.groups, imNamed ); const bool imHasVia = std::any_of( show.begin() + ipw.begin, show.begin() + ipw.end, [ & ]( NodeId n ) { return impactRowNameOnly( proven, n ); } ); + const bool imHasLink = std::any_of( show.begin() + ipw.begin, show.begin() + ipw.end, [ & ]( NodeId n ) { return impactDirectLink( g, seeds, depth, n ); } ); rw::emitTo( mem, "{}{}. {}{}{}{}{}{}{}{}{}{}{}{}-->", kImpactLegendOpen, kPageRaiseCapClause, reach.empty() ? "" : kImpactDepthLegend, // 0.6.5: exactly when d=/by_depth= ride, as on the CLI ( std::string( kImpactImportTierLegend ) + importersNextLegend( !imports.next.empty() ) ).c_str(), // cut-fix E, as on the CLI ( impactTsImportLegend( imports.importsUnresolved, imports.tsconfigUnread ) // #220: exactly when the root carries them, as on the CLI - + viaNameLegend( imHasVia, true ) ).c_str(), // FE-B: exactly when a shown row is via="name", as on the CLI + + viaNameLegend( imHasVia, true ) + + viaLinkLegend( imHasLink ) + impactAnchorLegendFor( g, seeds, depth, show, ipw.begin, ipw.end ) ).c_str(), kTestedRowLegend, kImpactTestedPartitionLegend, // A6 kTestedLensBlindSpotLegend, // F-02: rides with the partition, byte-identical to the CLI twin unprovenDefsVerbLegend( UnprovenDefsVerb::Impact, unprovenDefs > 0 ).c_str(), // H1: exactly when the root carries unproven_defs=, as on the CLI @@ -3237,7 +3266,7 @@ inline std::optional impactText( const std::string& root, const std // A6: tested="1" only (never a literal 0) — see kTestedRowLegend. 0.6.5: d= as on the CLI row. rw::emitTo( mem, "", symTag( s.kind ), ex( s.name ).c_str(), ex( rp ).c_str(), s.line, depthRunAttrXml( show, depth, i, ipw.begin ), isTestedByReach( ing, impTestReach, show[i] ) ? " tested=\"1\"" : "", - viaNameAttr( impactRowNameOnly( proven, show[i] ) ), imAttr.ofDefXml( show[i] ) ); } // FE-B (D3); attrib-multidef + impactLinkAttrs( g, proven, seeds, depth, show[ i ] ), imAttr.ofDefXml( show[i] ) ); } // the import tier's rows, after the symbol rows and under their own tag — a different unit, so a // different element (see the CLI arm and kImpactImportTierLegend for why they are never one number). emitImportRowsXml( mem, ing, std::span( imports.files ).first( imports.shown ), imRootPrefix, @@ -3615,6 +3644,13 @@ inline McpAffectedResult affectedText( const std::string& root, std::string_view return { mcpAnswerText( stream ), false, {}, false }; } +inline std::string pathKindLegend( const Graph& g, std::span path ) +{ + return std::string( viaNameLegend( pathHasNameOnlyHop( g, path ) ) ) + + viaLinkLegend( pathHasNameOnlyHop( g, path, kEdgeLink ) ) + + anchorMissLegend( pathHasNameOnlyHop( g, path, 0 ) ); +} + // `path` verb: the shortest directed CALL path from `from` to `to` (does A reach B, and how?). Reuses // resolveFocus + shortestPath, exactly as the CLI --path=A,B. Returns the … XML fragment (with // reachable="0" hops="0" and no children when B is NOT reachable from A — a valid answer, not an error), @@ -3670,14 +3706,14 @@ inline std::optional pathText( const std::string& root, const std:: const ToValueRefs ptToValueRefs = toValueRefs( ing, pth.empty(), dstDefs, &valueRefIndexOf( ix ) ); // PATH-GAP: the CLI --path's gap clause, by the same analysis and emitter (src/pathgaps.h). const PathSearchGaps ptGaps = pth.empty() ? pathSearchGaps( ing, g, srcDefs, valueRefIndexOf( ix ) ) : PathSearchGaps{}; - const PathNearSink ptNear = pth.empty() ? pathNearSink( ing, g, dstDefs, ptGaps.cone, valueRefIndexOf( ix ) ) : PathNearSink{}; // ix2-slice, as on the CLI - const PathBridge ptBridge = pth.empty() ? pathBridge( ing, g, srcDefs, dstDefs, valueRefIndexOf( ix ) ) : PathBridge{}; // ix2-slice, as on the CLI - const bool ptHasVia = pathHasNameOnlyHop( g, pth ); // FE-B: exactly when a hop of this path is via="name", as on the CLI - const PathEndpointBinding ptBound = pathEndpointBinding( g, endpointDefinitionCount( ing, srcDefs ), endpointDefinitionCount( ing, dstDefs ), pth ); // attrib-multidef, as on the CLI + const PathNearSink ptNear = pth.empty() ? pathNearSink( ing, g, dstDefs, ptGaps.cone, valueRefIndexOf( ix ) ) : PathNearSink{}; // ix2-slice, as on the CLI + const PathBridge ptBridge = pth.empty() ? pathBridge( ing, g, srcDefs, dstDefs, valueRefIndexOf( ix ) ) : PathBridge{}; // ix2-slice, as on the CLI + const std::string ptKindLegend = pathKindLegend( g, pth ); + const PathEndpointBinding ptBound = pathEndpointBinding( g, endpointDefinitionCount( ing, srcDefs ), endpointDefinitionCount( ing, dstDefs ), pth ); // attrib-multidef, as on the CLI const bool ptHasSplit = ptBound.splitHop; const std::vector ptOwners = pathHopOwners( ing, pth ); // PATH-OWNERS, as on the CLI rw::emitTo( mem, "{}", ( unprovenDefsVerbLegend( UnprovenDefsVerb::Path, unprovenDefs > 0 ) + viaNameLegend( ptHasVia ) + "graph holds none. {}{}{}{}-->{}", ( unprovenDefsVerbLegend( UnprovenDefsVerb::Path, unprovenDefs > 0 ) + ptKindLegend + splitArmLegend( ptHasSplit ) + pathUnboundLegend( !ptBound.from || !ptBound.to ) + pathOwnerLegend( anyPathHopOwner( ptOwners ) ) ).c_str(), ( std::string( toValueRefsLegend( ptToValueRefs.count > 0 ) ) + valueRefsDepthLegend( ptToValueRefs.depthCut.files > 0 ) ).c_str(), @@ -3963,7 +3999,17 @@ inline void packConnect( std::FILE* out, const IngestResult& ing, const Graph& g { return grp.terminals.size() > 1 && std::any_of( grp.edges.begin(), grp.edges.end(), [ & ]( const ConnectEdge& e ) { return edgeNameOnly( g, e.from, e.to ); } ); } ); - const std::string connectViaLegend = connectHasVia ? "" : std::string(); + const bool connectHasLink = std::any_of( res.groups.begin(), res.groups.end(), [ & ]( const ConnectGroup& grp ) + { + return grp.terminals.size() > 1 && std::any_of( grp.edges.begin(), grp.edges.end(), [ & ]( const ConnectEdge& e ) { return edgeIsLink( g, e.from, e.to ); } ); + } ); + const bool connectHasMiss = std::any_of( res.groups.begin(), res.groups.end(), [ & ]( const ConnectGroup& grp ) + { + return grp.terminals.size() > 1 && std::any_of( grp.edges.begin(), grp.edges.end(), [ & ]( const ConnectEdge& e ) { return edgeAnchorMiss( g, e.from, e.to ); } ); + } ); + const std::string connectViaLegend = ( connectHasVia ? "" : std::string() ) + + ( connectHasLink ? "" : std::string() ) + + ( connectHasMiss ? "" : std::string() ); const std::size_t connectExtraBytes = connectRootAttr.size() + std::strlen( rootRelPathsLegend( !rootArg.empty() ) ) + connectUnindexedLegend.size() + connectUnprovenAttr.size() + connectUnprovenLegend.size() + connectAmbiguousAttr.size() + connectViaLegend.size(); @@ -4115,7 +4161,8 @@ inline void packConnect( std::FILE* out, const IngestResult& ing, const Graph& g { payload.append( "" ); // FE-B: a name-only edge says so + .append( viaEdgeAttr( edgeNameOnly( g, e.from, e.to ), edgeIsLink( g, e.from, e.to ) ) ) + .append( anchorMissAttr( edgeAnchorMiss( g, e.from, e.to ) ) ).append( "/>" ); // FE-B, or via="link" } payload.append( "" ); } diff --git a/src/model.h b/src/model.h index f52b96dae..23f3ffb94 100644 --- a/src/model.h +++ b/src/model.h @@ -875,6 +875,8 @@ struct Reference // FE-B (test/receiverevidencecheck.sh): the rest of a member call's receiver chain, for the receiver-evidence rule // (graph.h ReceiverEvidence). memberPath is the member names BETWEEN memberRoot and the callee, '.'-joined — // "bucket" for `this.bucket.listSchemas()`, "req.raw.headers" for `ctx.req.raw.headers.get()`, "" for `root.m()`. + // A markdown link stores the path and slug as written here (the slug lowercased). memberCall stays false. + // buildGraph reads it (bindMarkdownLinks). // memberCtor is the class a CONSTRUCTED receiver names, final segment — "Reply" for `new Reply( r ).send()`, Go // "Merger" for `(&Merger{}).Len()` — with memberRoot "" (a construction has no root identifier). Both "" for every // other shape and every language that records no member-call shape. diff --git a/src/pathgaps.h b/src/pathgaps.h index 0153642cd..d978df44f 100644 --- a/src/pathgaps.h +++ b/src/pathgaps.h @@ -111,14 +111,14 @@ inline void countUnlistedNamesakes( const IngestResult& ing, const Graph& g, std }; const auto keyEq = []( const Key& a, const Key& b ) { return a.lang == b.lang && a.kind == b.kind && a.name == b.name; }; const auto keyOf = [ & ]( NodeId t ) { const Symbol& s = ing.symbols[t]; return Key{ s.lang, s.kind, s.name }; }; - const auto hasHedge = [ & ]( NodeId u ) { return std::any_of( g.outNameOnly.begin() + g.outOff[u], g.outNameOnly.begin() + g.outOff[u + 1], []( std::uint8_t b ) { return b != 0; } ); }; + const auto hasHedge = [ & ]( NodeId u ) { return std::any_of( g.outNameOnly.begin() + g.outOff[u], g.outNameOnly.begin() + g.outOff[u + 1], []( std::uint8_t b ) { return edgeKindByte( b ) == kEdgeName; } ); }; const auto isSym = [ & ]( std::uint32_t k ) { return g.outTargets[k] < ing.symbols.size(); }; std::vector names; for( const NodeId u : cone ) { for( std::uint32_t k = g.outOff[u]; k < g.outOff[u + 1]; ++k ) { - if( g.outNameOnly[k] != 0 && isSym( k ) ) + if( edgeKindByte( g.outNameOnly[k] ) == kEdgeName && isSym( k ) ) { names.push_back( ing.symbols[ g.outTargets[k] ].name ); } @@ -161,7 +161,7 @@ inline void countUnlistedNamesakes( const IngestResult& ing, const Graph& g, std std::vector hedged; // the targets u reached by name alone for( std::uint32_t k = g.outOff[u]; k < g.outOff[u + 1]; ++k ) { - if( isSym( k ) && g.outNameOnly[k] != 0 ) + if( isSym( k ) && edgeKindByte( g.outNameOnly[k] ) == kEdgeName ) { hedged.push_back( keyOf( g.outTargets[k] ) ); } @@ -711,7 +711,7 @@ inline PathNearSink pathNearSink( const IngestResult& ing, const Graph& g, std:: for( std::uint32_t k = g.outOff[c]; k < g.outOff[c + 1]; ++k ) { const NodeId w = g.outTargets[k]; - if( g.outNameOnly[k] == 0 || w >= S ) + if( edgeKindByte( g.outNameOnly[k] ) != kEdgeName || w >= S ) // a markdown link (kEdgeLink) is a proven edge, no by-name hit { continue; } @@ -1097,7 +1097,7 @@ inline PathBridge pathBridge( const IngestResult& ing, const Graph& g, std::span for( std::uint32_t e = g.outOff[u]; e < g.outOff[u + 1] && e < g.outNameOnly.size(); ++e ) { const NodeId w = g.outTargets[e]; - if( g.outNameOnly[e] == 0 || w >= S || byNameSites.siteOf( u, ing.symbols[w].name ) == ByNameCallSites::kNoSite ) + if( edgeKindByte( g.outNameOnly[e] ) != kEdgeName || w >= S || byNameSites.siteOf( u, ing.symbols[w].name ) == ByNameCallSites::kNoSite ) { continue; // not a by-name edge, or every call of the name is builtin-spelled (K5 rule 1) } diff --git a/src/quality.h b/src/quality.h index 6a1cfadb5..d2aa5f833 100644 --- a/src/quality.h +++ b/src/quality.h @@ -2337,11 +2337,16 @@ inline std::string cacheRootKeyHex( const std::string& root ) // not include this header; it relies on ingest.cpp including quality.h (line 13) before ingest_cache.h, and a reorder // that broke that fails the build on the undeclared name rather than passing. constexpr std::uint32_t kIngestCacheVersionMirror = 36; // MUST equal ingest.cpp's kCacheVersion (gated); 36 = train 30 (above call-result-typing's and fcall-forward's 33, name-template-dispatch's 35, main's 34), 34 = train 29 (above lane arity-min's and call-result-typing's 33), 33 = train 29 lane arity-min: def record paramsMin (the train renumbers), 33 = lane call-result-typing: RawBind resultIndex + unwrapDepth, 33 = lane fcall-forward: ref `forwarded` byte, 35 = lane name-template-dispatch: RefRole Pattern 11, 32 = train 28: RefRole Subscript 9 + Inert 10, 31 = train 27: lego-transitive's inherit-ref baseBind (the lane's 30), 29 = FE-B ref memberPath/memberCtor + 4 bind kinds, 28 = FE-A ref memberCall/memberRoot, 27 = corrected fnScope values, 26 = function-local def scope span (25 = #157 + #150) -constexpr std::uint32_t kIngestParserVerMirror = 198; // MUST equal ingest.cpp's kParserVer (gated) +constexpr std::uint32_t kIngestParserVerMirror = 206; // MUST equal ingest.cpp's kParserVer (gated) + // 206 = 2026-10-18 (#409, PR #414, renumbered again at merge: above main 196/197, + // train 31 198/199, rust-receivers 202/203, ix1-slot 204/205; see the kParserVer note) // 198 = 2026-10-18 (train 31: above main 196/197 and h1-annot // 194/195; see the kParserVer note) // 194 = lane h1-annot (reservation 194/195; the train renumbers): // qualified-annotation importedName, AugAssign / OperatorResult binds + // 185 = 2026-10-18 (#409): cross-file markdown links carry their written path#slug + // (memberPath); graph.h pins the exact heading. 185, not 184: 184 is train 29. + // Rich file tag 186. kCacheVersion stays 34. // 196 = 2026-10-18 (train 30: above call-result-typing 180/181, fcall-forward // 188/189, go-local-var 190/191, name-template-dispatch 192/193, h1-annot // 194/195; see the kParserVer note) diff --git a/src/serialize.h b/src/serialize.h index 4d7c58421..dcddd05ab 100644 --- a/src/serialize.h +++ b/src/serialize.h @@ -2642,6 +2642,52 @@ inline std::uint32_t counterAt( const std::vector* v, NodeId id ) return ( v && id < v->size() ) ? ( *v )[ id ] : 0u; } +// The row's prov=, via=, x= and anchor="missing" tail. One writer so the row loop stays a scan. +inline void writeMapCalleeAttrs( XmlWriter& w, std::uint8_t prov, bool link, bool nameOnly, std::size_t repeats, bool miss ) +{ + if( prov != 0 ) + { + w.write( "\" prov=\"" ); + w.write( provLabel( prov ) ); + } + if( link ) + { + w.write( "\" via=\"link" ); + } + else if( nameOnly ) + { + w.write( "\" via=\"name" ); + } + if( repeats > 1 ) + { + w.write( "\" x=\"" ); + w.write( std::to_string( repeats ) ); + } + if( miss ) + { + w.write( "\" anchor=\"missing" ); + } +} + +// The JSON twin of the row's via= and anchor="missing" pair, from the stored kind byte (Graph::outNameOnly): the map rows +// and the inventory entries' callees read it here, so --json spells a markdown link "link", never "name", and keeps its mark. +inline void writeMapCalleeJsonKind( XmlWriter& w, std::uint8_t stored ) +{ + const std::uint8_t kind = edgeKindByte( stored ); + if( kind == kEdgeLink ) + { + w.write( ",\"via\":\"link\"" ); + } + else if( kind == kEdgeName ) + { + w.write( ",\"via\":\"name\"" ); // FE-B: the XML via="name", 1:1 + } + if( ( stored & 0x80 ) != 0 ) // the writer's anchor="missing" mark on a link (graph.h kEdgeKindMask) + { + w.write( ",\"anchor\":\"missing\"" ); + } +} + // FE-B: one map 's rows over out-edges [from, to). A via="name" row (matched by name alone) that the symbol would // print several times byte for byte — N same-named by-name candidates, one row each, carrying only n=/prov=/via= — is // written ONCE, at its first edge, with x="N": the count, and --callees on the lists each candidate with its file @@ -2652,7 +2698,9 @@ inline void writeMapCalleeRows( XmlWriter& w, const IngestResult& ing, std::uint std::vector& scratch ) { const auto provOf = [ & ]( std::uint32_t e ) -> std::uint8_t { return ( outProv && e < outProv->size() ) ? ( *outProv )[ e ] : 0; }; - const auto nameOnly = [ & ]( std::uint32_t e ) { return outNameOnly && e < outNameOnly->size() && ( *outNameOnly )[ e ] != 0; }; + const auto stored = [ & ]( std::uint32_t e ) -> std::uint8_t { return outNameOnly && e < outNameOnly->size() ? ( *outNameOnly )[ e ] : 0; }; + const auto nameOnly = [ & ]( std::uint32_t e ) { return edgeKindByte( stored( e ) ) == kEdgeName; }; + const auto link = [ & ]( std::uint32_t e ) { return edgeKindByte( stored( e ) ) == kEdgeLink; }; // the row's bytes among via="name" rows: name, prov= const auto rowLess = [ & ]( std::uint32_t x, std::uint32_t y ) { @@ -2690,30 +2738,16 @@ inline void writeMapCalleeRows( XmlWriter& w, const IngestResult& ing, std::uint // uniquely resolved (the common case → zero token cost). outProv parallels outTargets exactly. C1, and this is the // whole point of the marker: `amb="K"` on the enclosing says K of this symbol's CALLS were guesses and cannot // say WHICH edges; prov="split" names the arms, and the suspect set becomes the guessed edges and nothing else. - if( provOf( e ) != 0 ) - { - w.write( "\" prov=\"" ); - w.write( provLabel( provOf( e ) ) ); - } - if( nameOnly( e ) ) - { - w.write( "\" via=\"name" ); // FE-B: the same edge bit every other surface reads (viaNameLegendComment) - } - if( repeats > 1 ) - { - w.write( "\" x=\"" ); - w.write( std::to_string( repeats ) ); - } + writeMapCalleeAttrs( w, provOf( e ), link( e ), nameOnly( e ), repeats, ( stored( e ) & 0x80 ) != 0 ); w.write( "\"/>" ); } } -// FE-B: may a row under one of `nodes` carry via="name"? True when any of their out-edges is name-only — an -// over-approximation of what a section renders (a block's 16-per-symbol cap may cut that row; a merged overload -// row prints one representative's edges), asked BEFORE the rows are written by a surface whose first-screen legend must -// define every attribute its payload can carry (the map, --for, --exemplar). -inline bool namesOnlyOutAny( const std::vector& outOff, const std::vector& outNameOnly, - std::span nodes ) noexcept +// A row under one of `nodes` may carry the bit when any of their out-edges has it. Asked before the rows are written +// so the map's first-screen legend can define the attribute. outLink and outNameOnly share this scan. +// want 0: any nonzero byte. want 0x80: the anchor-missing high bit. Otherwise the kind byte equals want. +inline bool markedOutAny( const std::vector& outOff, const std::vector& bits, + std::span nodes, std::uint8_t want = 0 ) noexcept { for( const NodeId n : nodes ) { @@ -2721,9 +2755,10 @@ inline bool namesOnlyOutAny( const std::vector& outOff, const std { continue; } - for( std::uint32_t e = outOff[ n ]; e < outOff[ n + 1 ] && e < outNameOnly.size(); ++e ) + for( std::uint32_t e = outOff[ n ]; e < outOff[ n + 1 ] && e < bits.size(); ++e ) { - if( outNameOnly[ e ] != 0 ) + const bool hit = want == 0 ? bits[ e ] != 0 : ( want == 0x80 ? ( bits[ e ] & 0x80 ) != 0 : edgeKindByte( bits[ e ] ) == want ); + if( hit ) { return true; } @@ -2731,6 +2766,15 @@ inline bool namesOnlyOutAny( const std::vector& outOff, const std } return false; } +// FE-B: may a row under one of `nodes` carry via="name"? True when any of their out-edges is name-only — an +// over-approximation of what a section renders (a block's 16-per-symbol cap may cut that row; a merged overload +// row prints one representative's edges), asked BEFORE the rows are written by a surface whose first-screen legend must +// define every attribute its payload can carry (the map, --for, --exemplar). +inline bool namesOnlyOutAny( const std::vector& outOff, const std::vector& outNameOnly, + std::span nodes ) noexcept +{ + return markedOutAny( outOff, outNameOnly, nodes, kEdgeName ); +} // attrib-multidef Q1: may a row under one of `nodes` carry prov="split"? True when any of their out-edges is one arm of a // resolver split and not by name alone (a by-name row carries via="name" instead) — namesOnlyOutAny's twin question, over @@ -2743,7 +2787,7 @@ inline bool splitArmsOutAny( const std::vector& outOff, const std for( std::uint32_t e = ( std::size_t( n ) + 1 < outOff.size() ) ? outOff[ n ] : 0u; std::size_t( n ) + 1 < outOff.size() && e < outOff[ n + 1 ] && e < outProv.size(); ++e ) { - if( outProv[ e ] == 3u && !( e < outNameOnly.size() && outNameOnly[ e ] != 0 ) ) + if( outProv[ e ] == 3u && !( e < outNameOnly.size() && edgeKindByte( outNameOnly[ e ] ) == kEdgeName ) ) { return true; } @@ -2770,7 +2814,7 @@ inline bool hopSlotHasProvenEdge( const std::vector& outOff, cons } ASSUME( outOff[ id + 1 ] <= outNameOnly.size(), "the hedge bits are one per flattened edge (graph.h buildGraph)" ); return !std::all_of( outNameOnly.begin() + outOff[ id ], outNameOnly.begin() + outOff[ id + 1 ], - []( std::uint8_t bit ) { return bit != 0; } ); + []( std::uint8_t bit ) { return edgeKindByte( bit ) == kEdgeName; } ); } // D7 MAP-INVENTORY: whether THIS map carries the inventory tier (mapinventory.h), decided once for both dialects. The @@ -3155,6 +3199,16 @@ inline void serialize( std::FILE* out, const IngestResult& ing, const std::vecto // and whose element-qualified via/x rows restate it: the --max-tokens fit then prices what is delivered. legend += kMapViaNameLegend; } + if( outNameOnly && !stubbed && !ann.viaLegendStripped + && markedOutAny( outOff, *outNameOnly, std::span( order.data(), std::min( keep, order.size() ) ), kEdgeLink ) ) + { + legend += kMapViaLinkLegend; + } + if( outNameOnly && !stubbed && !ann.viaLegendStripped + && markedOutAny( outOff, *outNameOnly, std::span( order.data(), std::min( keep, order.size() ) ), 0x80 ) ) + { + legend += kMapAnchorMissLegend; + } // C1 DRIFT FIX (Round C lane B, found by re-reading this header's own output). `precise=` means "how many // out-edges a SCIP index PINNED", and the emitter's own comment below says it is "emitted ONLY under // --scip". Both were true when outProv held only {0, 1}. A4-R5 then added value 2 (an FFI binding edge) @@ -6673,6 +6727,8 @@ struct CalleeCallsSink // nullptr / empty ⇒ no row is marked. `splitLegendCharged`: as viaLegendCharged, for its comment (kForSplitClause). const std::vector* prov = nullptr; bool* splitLegendCharged = nullptr; + const std::vector* link = nullptr; // Graph::outLink: via="link" on a callee row. nullptr ⇒ none + const std::vector* anchor = nullptr; // Graph::outAnchorMiss: anchor="missing" on that row. nullptr ⇒ none }; // The wrapper, written in front of the rows it describes (they have to be walked before `shown` @@ -6818,17 +6874,30 @@ struct MergedCalleeNameRow std::vector lines; // every definition line of that name; joined ascending at append time bool via = false; // FE-B: these edges are name-only — merged only with rows of the same bit bool split = false; // attrib-multidef Q1: these edges are split arms — likewise + bool link = false; // a markdown link — merged only with other link rows of the same name + bool miss = false; // that link named no heading — merged only with other miss rows of the same name }; +struct CalleeRowMarks +{ + bool via = false; + bool split = false; + bool link = false; + bool miss = false; +}; inline void collectCalleeNameRow( std::vector& rows, const Symbol& cs, - std::size_t& used, const CalleeCallsSink& sink, bool via = false, bool split = false ) + std::size_t& used, const CalleeCallsSink& sink, CalleeRowMarks mark = {} ) { + const bool via = mark.via; + const bool split = mark.split; + const bool link = mark.link; + const bool miss = mark.miss; char lb[ 16 ]; rw::formatTo( lb, sizeof( lb ), "{}", cs.line ); bool merged = false; for( MergedCalleeNameRow& r : rows ) { - if( r.name == cs.name && r.via == via && r.split == split ) + if( r.name == cs.name && r.via == via && r.split == split && r.link == link && r.miss == miss ) { r.lines.push_back( cs.line ); merged = true; break; @@ -6836,11 +6905,11 @@ inline void collectCalleeNameRow( std::vector& rows, const } if( !merged ) { - rows.push_back( MergedCalleeNameRow { cs.name, { cs.line }, via, split } ); + rows.push_back( MergedCalleeNameRow { cs.name, { cs.line }, via, split, link, miss } ); } // the comma and the digits a merge appends, or the whole row it opens (FE-B: plus its 11-byte via="name"; Q1: its 13-byte - // prov="split") - used += merged ? std::strlen( lb ) + 1 : cs.name.size() + 16 + ( via ? 11u : 0u ) + ( split ? 13u : 0u ); + // prov="split"; a link row's via="link" is the same 11 bytes) + used += merged ? std::strlen( lb ) + 1 : cs.name.size() + 16 + ( ( via || link ) ? 11u : 0u ) + ( split ? 13u : 0u ) + ( miss ? 17u : 0u ); if( sink.recorded ) { sink.recorded->push_back( EmittedBodyCall { cs.name, cs.line, std::string() } ); // §H5: no sig to record @@ -6867,7 +6936,8 @@ inline void appendMergedCalleeNameRows( std::string& callsBody, std::vector& nameOnly; const std::vector* prov = nullptr; bool* splitLegendCharged = nullptr; + const std::vector* link = nullptr; + const std::vector* anchor = nullptr; CalleeEdgeBytes( const std::vector& nameOnlyBytes, const std::vector* provBytes = nullptr, // NOLINT: implicit bool* charged = nullptr ) noexcept : nameOnly( nameOnlyBytes ), prov( provBytes ), splitLegendCharged( charged ) {} }; +inline CalleeEdgeBytes withAnchorEdge( CalleeEdgeBytes edges, const std::vector* anchor ) noexcept +{ + edges.anchor = anchor; + return edges; +} // …and which of their readings the caller's own first-screen legend already carries (FE-B's via="name", Q1's prov="split"), so // the section neither repeats nor charges them. Built implicitly from the via bit alone, as every pre-Q1 caller passes it. struct CalleeLegendsInHead @@ -6915,6 +6992,13 @@ inline CalleeCallsSink withSplitArms( CalleeCallsSink sink, const std::vector* link, + const std::vector* anchor = nullptr ) noexcept +{ + sink.link = link; + sink.anchor = anchor; + return sink; +} // attrib-multidef Q1: is the row id → cid a split arm (and not by name alone: that row says via="name")? The first // such row of an answer charges the comment that defines it to `used`, before the row, when the head did not define it. @@ -6937,6 +7021,95 @@ inline std::string_view calleeRowSplitAttr( const std::vector& ou return split ? std::string_view( " prov=\"split\"" ) : std::string_view(); } +// Is the row id → cid a markdown link whose slug names no heading? Read by the names-only path BEFORE it collects the row, so +// the miss bit is part of the merge key (a good link and a missing one that share a name stay two rows). +inline bool calleeRowAnchorMiss( const CalleeCallsSink& sink, const std::vector& outOff, + const std::vector& outTargets, NodeId id, NodeId cid ) noexcept +{ + return sink.anchor != nullptr && edgeNameOnlyInCsr( outOff, outTargets, *sink.anchor, id, cid ); +} + +// A markdown link whose slug names no heading. Charges the legend once and, when `callsBody` is given, inserts the attribute +// into a signature row that already ends at `>`. +inline void applyCalleeAnchorMiss( const CalleeCallsSink& sink, const std::vector& outOff, + const std::vector& outTargets, NodeId id, NodeId cid, bool& legendCharged, + std::size_t& used, std::string* callsBody ) +{ + if( !calleeRowAnchorMiss( sink, outOff, outTargets, id, cid ) ) + { + return; + } + if( !legendCharged ) + { + used += anchorMissLegendComment().size(); + legendCharged = true; + } + if( callsBody == nullptr ) + { + return; + } + const std::size_t cut = callsBody->rfind( '>' ); + if( cut != std::string::npos ) + { + callsBody->insert( cut, " anchor=\"missing\"" ); + used += 17; + } +} + +// One of a body's callee block. True when the row counts toward shown=. +template +inline bool appendOneCalleeCall( std::string& callsBody, std::vector& nameRows, NodeId cid, NodeId id, + const std::vector& outOff, const std::vector& outTargets, + const IngestResult& ing, ContentOfFn&& contentOf, std::vector& esc, std::size_t& used, + const CalleeCallsSink& sink, bool& linkLegendCharged, bool& anchorLegendCharged ) +{ + if( cid >= ing.symbols.size() ) + { + return false; + } + const Symbol& cs = ing.symbols[cid]; + const bool linkRow = sink.nameOnly != nullptr && edgeNameOnlyInCsr( outOff, outTargets, *sink.nameOnly, id, cid, kEdgeLink ); + if( linkRow && !linkLegendCharged ) + { + used += viaLinkLegendComment().size(); + linkLegendCharged = true; + } + const bool missRow = calleeRowAnchorMiss( sink, outOff, outTargets, id, cid ); + applyCalleeAnchorMiss( sink, outOff, outTargets, id, cid, anchorLegendCharged, used, nullptr ); + const bool nameRow = !linkRow && calleeRowVia( outOff, outTargets, id, cid, used, sink ); + const std::string_view viaAttr = viaEdgeAttr( nameRow, linkRow ); + const std::string_view splitAttr = calleeRowSplitAttr( outOff, outTargets, id, cid, !viaAttr.empty(), used, sink ); + if( sink.namesOnly ) + { + collectCalleeNameRow( nameRows, cs, used, sink, CalleeRowMarks{ nameRow, !splitAttr.empty(), linkRow, missRow } ); + return true; + } + const std::string& csrc = contentOf( cs.fileId ); + if( cs.sigStartByte >= cs.sigEndByte || cs.sigEndByte > csrc.size() ) + { + return false; + } + const std::string sig = cleanSig( csrc.data(), cs.sigStartByte, cs.sigEndByte, sink.redact ); + if( sig.empty() ) + { + return false; + } + char hb[ 64 ]; + rw::formatTo( hb, sizeof( hb ), "\" l=\"{}\"{}{}>", cs.line, viaAttr, splitAttr ); + callsBody += "push_back( EmittedBodyCall { cs.name, cs.line, sig } ); + } + return true; +} + // §P10.1: the disclosed block // for one body's 1-hop callee signatures — extracted out of packBodies so the disclosure logic doesn't // inflate packBodies' own complexity/LOC. `total` is outOff[id+1]-outOff[id] — outTargets is deduped-per- @@ -6950,7 +7123,6 @@ inline void emitCalleeCallsBlock( std::string& out, NodeId id, const std::vector ContentOfFn&& contentOf, std::vector& esc, std::size_t& used, std::size_t budgetBytes, const CalleeCallsSink& sink ) { - RedactCounts* const redact = sink.redact; if( id + 1 >= outOff.size() ) { return; @@ -6966,43 +7138,15 @@ inline void emitCalleeCallsBlock( std::string& out, NodeId id, const std::vector std::string callsBody; std::vector nameRows; // names-only rendering: collected, merged by name, written after the walk int shown = 0; + bool linkLegendCharged = false; + bool anchorLegendCharged = false; for( std::uint32_t k = outOff[id]; k < outOff[id + 1] && shown < int( sink.rowCap ) && used < budgetBytes; ++k ) { const NodeId cid = walk[ k - outOff[id] ]; - if( cid >= ing.symbols.size() ) + if( appendOneCalleeCall( callsBody, nameRows, cid, id, outOff, outTargets, ing, contentOf, esc, used, sink, + linkLegendCharged, anchorLegendCharged ) ) { - continue; - } - const Symbol& cs = ing.symbols[cid]; - const std::string_view viaAttr = viaNameAttr( calleeRowVia( outOff, outTargets, id, cid, used, sink ) ); // FE-B: via="name" or "" - const std::string_view splitAttr = calleeRowSplitAttr( outOff, outTargets, id, cid, !viaAttr.empty(), used, sink ); // Q1: prov="split" or "" - - // COMPACT: the names-only rendering — see collectCalleeNameRow above for what it does and does not do. - if( sink.namesOnly ) - { - collectCalleeNameRow( nameRows, cs, used, sink, !viaAttr.empty(), !splitAttr.empty() ); ++shown; - continue; - } - - const std::string& csrc = contentOf( cs.fileId ); - if( cs.sigStartByte >= cs.sigEndByte || cs.sigEndByte > csrc.size() ) - { - continue; - } - const std::string sig = cleanSig( csrc.data(), cs.sigStartByte, cs.sigEndByte, redact ); - if( sig.empty() ) - { - continue; - } - char hb[ 64 ]; rw::formatTo( hb, sizeof( hb ), "\" l=\"{}\"{}{}>", cs.line, viaAttr, splitAttr ); - callsBody += "push_back( EmittedBodyCall { cs.name, cs.line, sig } ); // §H5 } } appendMergedCalleeNameRows( callsBody, nameRows, esc ); // no-op on the signature rendering (nameRows stays empty) @@ -7680,8 +7824,8 @@ inline void packBodies( std::FILE* out, const IngestResult& ing, const std::vect // total=/shown=/capped= block — see emitCalleeCallsBlock above; `calleeRank` decides which // rows survive when it CUTS one, which is far from rare here (CalleeCallsSink::rank). emitCalleeCallsBlock( piece, id, outOff, outTargets, ing, contentOf, esc, used, budgetBytes, - withSplitArms( CalleeCallsSink{ redact, record ? &record->calls : nullptr, /*namesOnly=*/false, calleeRank, &outNameOnly, - &viaLegendCharged }, outEdges.prov, &splitLegendCharged ) ); + withLinkBits( withSplitArms( CalleeCallsSink{ redact, record ? &record->calls : nullptr, /*namesOnly=*/false, calleeRank, &outNameOnly, + &viaLegendCharged }, outEdges.prov, &splitLegendCharged ), outEdges.link, outEdges.anchor ) ); const std::string bodyNotes = renderNoteChildren( noteIndex, symbolNoteTarget( noteIndex, ing, s ), esc ); // L3/D5 piece += bodyNotes; used += bodyNotes.size(); // W3-N2: same charge-never-trim rule @@ -7757,6 +7901,14 @@ inline void packBodies( std::FILE* out, const IngestResult& ing, const std::vect { w.write( viaNameLegendComment() ); // FE-B: the same rule, for a row's via="name" } + if( children.find( " via=\"link\"" ) != std::string::npos ) + { + w.write( viaLinkLegendComment() ); + } + if( children.find( " anchor=\"missing\"" ) != std::string::npos ) + { + w.write( anchorMissLegendComment() ); + } if( !legendsInHead.split && children.find( " prov=\"split\"" ) != std::string::npos ) { w.write( splitArmCallsLegendComment() ); // attrib-multidef Q1: the same rule, for a row's prov="split" @@ -7855,8 +8007,8 @@ inline OwnerHopRows packOwnerHopRows( const IngestResult& ing, const std::vector } used += rowFixed; emitCalleeCallsBlock( row, id, outOff, outTargets, ing, contentOf, esc, used, budget, - withSplitArms( CalleeCallsSink{ redact, /*recorded=*/nullptr, /*namesOnly=*/true, rank, &outNameOnly, &via, - kForOwnerHopNameGuard, next }, outEdges.prov, outEdges.splitLegendCharged ) ); + withLinkBits( withSplitArms( CalleeCallsSink{ redact, /*recorded=*/nullptr, /*namesOnly=*/true, rank, &outNameOnly, &via, + kForOwnerHopNameGuard, next }, outEdges.prov, outEdges.splitLegendCharged ), outEdges.link, outEdges.anchor ) ); out.xml += row; out.xml += ""; ++out.shown; @@ -7980,7 +8132,7 @@ inline void packHops( std::FILE* out, const IngestResult& ing, const std::vector // owner-hop: the leading owner rows first (THE QUESTION'S OWNER ROWS above; nothing when `owners` is null), then the // ranked rows exactly as before, from the node the owners end at, on the budget and via charge the owners leave them - const OwnerHopRows own = packOwnerHopRows( ing, nodes, owners, outOff, outTargets, CalleeEdgeBytes( outNameOnly, outEdges.prov, &splitLegendCharged ), + const OwnerHopRows own = packOwnerHopRows( ing, nodes, owners, outOff, outTargets, withAnchorEdge( CalleeEdgeBytes( outNameOnly, outEdges.prov, &splitLegendCharged ), outEdges.anchor ), redact, rank, rootArg, budgetBytes, viaLegendCharged, pathRel, contentOf, esc ); std::string children = own.xml; std::size_t requestedCount = own.shown; @@ -8035,9 +8187,9 @@ inline void packHops( std::FILE* out, const IngestResult& ing, const std::vector // the 1-hop callee signatures — the identical block a body carries, charged against the same // running `used` so the row identity bytes and the edge bytes share one budget. emitCalleeCallsBlock( row, id, outOff, outTargets, ing, contentOf, esc, used, budgetBytes, - withSplitArms( CalleeCallsSink{ redact, /*recorded=*/nullptr, /*namesOnly=*/true, rank, &outNameOnly, &viaLegendCharged, - CalleeCallsSink{}.rowCap, next }, - outEdges.prov, &splitLegendCharged ) ); + withLinkBits( withSplitArms( CalleeCallsSink{ redact, /*recorded=*/nullptr, /*namesOnly=*/true, rank, &outNameOnly, &viaLegendCharged, + CalleeCallsSink{}.rowCap, next }, + outEdges.prov, &splitLegendCharged ), outEdges.link, outEdges.anchor ) ); row += ""; children += row; ++shownCount; @@ -8053,6 +8205,14 @@ inline void packHops( std::FILE* out, const IngestResult& ing, const std::vector { w.write( viaNameLegendComment() ); // FE-B: exactly when a hop's row carries via="name" } + if( children.find( " via=\"link\"" ) != std::string::npos ) + { + w.write( viaLinkLegendComment() ); + } + if( children.find( " anchor=\"missing\"" ) != std::string::npos ) + { + w.write( anchorMissLegendComment() ); + } if( !legendsInHead.split && children.find( " prov=\"split\"" ) != std::string::npos ) { w.write( splitArmCallsLegendComment() ); // attrib-multidef Q1: exactly when a hop's row carries prov="split" @@ -10467,9 +10627,9 @@ inline void writeMapInventoryJson( JsonWriter& w, const IngestResult& ing, const { w.write( ",\"prov\":" ); writeJsonStr( w, provLabel( ( *outProv )[ e ] ), esc ); } - if( outNameOnly && e < outNameOnly->size() && ( *outNameOnly )[ e ] != 0 ) + if( outNameOnly && e < outNameOnly->size() ) { - w.write( ",\"via\":\"name\"" ); + writeMapCalleeJsonKind( w, ( *outNameOnly )[ e ] ); } w.write( "}" ); } @@ -10722,9 +10882,9 @@ inline void serializeJson( std::FILE* out, const IngestResult& ing, const std::v { w.write( ",\"prov\":" ); writeJsonStr( w, provLabel( (*outProv)[e] ), esc ); } - if( outNameOnly && e < outNameOnly->size() && ( *outNameOnly )[e] != 0 ) + if( outNameOnly && e < outNameOnly->size() ) { - w.write( ",\"via\":\"name\"" ); // FE-B: the XML via="name", 1:1 + writeMapCalleeJsonKind( w, ( *outNameOnly )[e] ); } w.write( "}" ); } diff --git a/src/verbs_for.h b/src/verbs_for.h index 10506b36e..652c00b11 100644 --- a/src/verbs_for.h +++ b/src/verbs_for.h @@ -2033,7 +2033,7 @@ ForAutoBodiesResult buildForAutoBodies( const rw::Config& cfg, const rw::IngestR // shell the "budget" branch below already gets for free, rather than a second hand-rolled tag. out.attr = " bundle=\"auto\" bodies=\"0\" reason=\"no_candidates\""; out.section = rw::chargeSection( [ & ]( std::FILE* f ) - { rw::packBodies( f, ing, autoBodyIds, /*budgetBytes=*/1, g.outOff, g.outTargets, rw::CalleeEdgeBytes( g.outNameOnly, &g.outProv ), cfg.compress, redactPtr, + { rw::packBodies( f, ing, autoBodyIds, /*budgetBytes=*/1, g.outOff, g.outTargets, rw::withAnchorEdge( rw::CalleeEdgeBytes( g.outNameOnly, &g.outProv, nullptr ), &g.outAnchorMiss ), cfg.compress, redactPtr, /*ranges=*/nullptr, /*noteIndex=*/nullptr, nullptr, /*truncateOversizedFirst=*/false, /*withFileContext=*/false, fabRootArg, &lensRank, rw::CalleeLegendsInHead( viaLegendInHead, splitLegendInHead ) ); }, rw::kBytesPerTokenBody ); @@ -2055,7 +2055,7 @@ ForAutoBodiesResult buildForAutoBodies( const rw::Config& cfg, const rw::IngestR const std::size_t autoBodyBudget = std::max( 1, std::min( leftBytes, cfg.packBudgetBytes ) ); rw::EmittedBodies autoEmitted; out.section = rw::chargeSection( [ & ]( std::FILE* f ) - { rw::packBodies( f, ing, autoBodyIds, autoBodyBudget, g.outOff, g.outTargets, rw::CalleeEdgeBytes( g.outNameOnly, &g.outProv ), cfg.compress, redactPtr, + { rw::packBodies( f, ing, autoBodyIds, autoBodyBudget, g.outOff, g.outTargets, rw::withAnchorEdge( rw::CalleeEdgeBytes( g.outNameOnly, &g.outProv, nullptr ), &g.outAnchorMiss ), cfg.compress, redactPtr, /*ranges=*/nullptr, /*noteIndex=*/nullptr, &autoEmitted, /*truncateOversizedFirst=*/false, /*withFileContext=*/false, fabRootArg, &lensRank, rw::CalleeLegendsInHead( viaLegendInHead, splitLegendInHead ) ); }, rw::kBytesPerTokenBody ); @@ -2461,7 +2461,7 @@ ForAutoBodiesResult buildForCompactHops( const rw::Config& cfg, const rw::Ingest const rw::HopOwners owners = forHopOwners( cfg, ing, ownerPlan, leftBytes, fcRootArg ); out.section = rw::chargeSection( [ & ]( std::FILE* f ) - { rw::packHops( f, ing, hopNodes, hopBudget, g.outOff, g.outTargets, rw::CalleeEdgeBytes( g.outNameOnly, &g.outProv ), redactPtr, &lensRank, fcRootArg, + { rw::packHops( f, ing, hopNodes, hopBudget, g.outOff, g.outTargets, rw::withAnchorEdge( rw::CalleeEdgeBytes( g.outNameOnly, &g.outProv, nullptr ), &g.outAnchorMiss ), redactPtr, &lensRank, fcRootArg, rw::CalleeLegendsInHead( viaLegendInHead, promised.splitLegendInHead ), &owners ); }, // g.outNameOnly also feeds the hop-slot rule (serialize.h noProvenEdge) // MARKUP rate, not the body rate — and this is an honesty choice, not a copy-paste slip. The body // rate (3.80 B/tok) prices SOURCE TEXT; the compact section contains none, only tags, identifiers @@ -3797,7 +3797,7 @@ std::optional runForLensPass( const MainDispatch& d, rw::LensRanking lr, co detailBodyBudget = std::min( detailBodyBudget, std::max( leftBytes, detailFloorBudget ) ); } detailSection = rw::chargeSection( [ & ]( std::FILE* f ) - { packBodies( f, ing, detailIds, detailBodyBudget, g.outOff, g.outTargets, rw::CalleeEdgeBytes( g.outNameOnly, &g.outProv ), cfg.compress, redactPtr, + { packBodies( f, ing, detailIds, detailBodyBudget, g.outOff, g.outTargets, rw::withAnchorEdge( rw::CalleeEdgeBytes( g.outNameOnly, &g.outProv, nullptr ), &g.outAnchorMiss ), cfg.compress, redactPtr, /*ranges=*/nullptr, notesPtr, /*outEmitted=*/nullptr, /*truncateOversizedFirst=*/true, /*withFileContext=*/false, flRootArg, &lensRank, rw::CalleeLegendsInHead( headerParts.viaPresent, headerParts.splitPresent ) ); }, // L3: --detail bodies surface notes too (part of the --for bundle) rw::kBytesPerTokenBody ); @@ -3856,7 +3856,7 @@ std::optional runForLensPass( const MainDispatch& d, rw::LensRanking lr, co { const std::size_t slotSpent = rowsSpentBytes + plan.attrReserve + enrich.section.xml.size(); // the rows at the cut + the hops slot = rw::renderForBodySlot( rw::BodySlotInputs{ ing, g.outOff, g.outTargets, g.outNameOnly, lensRank, flRootArg, redactPtr, notesPtr, - headerParts.viaPresent, cfg.compress, &g.outProv, headerParts.splitPresent }, + headerParts.viaPresent, cfg.compress, &g.outProv, headerParts.splitPresent, &g.outAnchorMiss }, slotOwner, forSoftTarget ? std::optional( rw::forSoftSectionsShare( bundleBudget, slotSpent ) ) : std::nullopt ); if( slot.served ) @@ -4098,7 +4098,7 @@ std::optional runForLensPass( const MainDispatch& d, rw::LensRanking lr, co // directly at the SAME budget — uncharged for that one run, with an alert, never a fabricated number. if( cfg.detail > 0 ) { - rw::emitChargedSection( stdout, detailSection, [ & ]{ packBodies( stdout, ing, detailIds, detailBodyBudget, g.outOff, g.outTargets, rw::CalleeEdgeBytes( g.outNameOnly, &g.outProv ), + rw::emitChargedSection( stdout, detailSection, [ & ]{ packBodies( stdout, ing, detailIds, detailBodyBudget, g.outOff, g.outTargets, rw::withAnchorEdge( rw::CalleeEdgeBytes( g.outNameOnly, &g.outProv, nullptr ), &g.outAnchorMiss ), cfg.compress, redactPtr, /*ranges=*/nullptr, notesPtr, /*outEmitted=*/nullptr, /*truncateOversizedFirst=*/true, /*withFileContext=*/false, flRootArg, &lensRank, rw::CalleeLegendsInHead( headerParts.viaPresent, headerParts.splitPresent ) ); } ); diff --git a/src/verbs_navigate.h b/src/verbs_navigate.h index 9dc37319b..59b86c6bf 100644 --- a/src/verbs_navigate.h +++ b/src/verbs_navigate.h @@ -77,6 +77,27 @@ inline std::string_view jsonRowKeysAt( const std::vector* rowKeys, { return rowKeys != nullptr && i < rowKeys->size() ? std::string_view( ( *rowKeys )[ i ] ) : std::string_view(); } +inline char hopViaCode( bool link, bool name ) noexcept +{ + if( link ) + { + return 2; + } + if( name ) + { + return 1; + } + return 0; +} + +inline const char* jsonViaField( const std::vector* via, std::size_t i ) noexcept +{ + if( via == nullptr || i >= via->size() || ( *via )[ i ] == 0 ) + { + return ""; + } + return ( *via )[ i ] == 2 ? ",\"via\":\"link\"" : ",\"via\":\"name\""; +} inline void printJsonSymbolRows( const rw::IngestResult& ing, const std::vector& ids, std::size_t begin, std::size_t end, std::string_view rootPrefix = {}, const std::vector* testReach = nullptr, @@ -92,7 +113,7 @@ inline void printJsonSymbolRows( const rw::IngestResult& ing, const std::vector< rw::symTag( s.kind ), rw::jsonStr( s.name ).c_str(), rw::jsonStr( p ).c_str(), s.line, depth ? ",\"d\":" + std::to_string( ( *depth )[ ids[i] ] ) : std::string(), ( testReach && rw::isTestedByReach( ing, *testReach, ids[i] ) ) ? ",\"tested\":true" : "", - ( viaName && i < viaName->size() && ( *viaName )[ i ] != 0 ) ? ",\"via\":\"name\"" : "", // FE-B: the row's hedge + jsonViaField( viaName, i ), jsonRowKeysAt( rowKeys, i ) ); } } @@ -137,6 +158,106 @@ inline int refuseNotFoundWithAnswer( const rw::IngestResult& ing, const rw::Conf return 1; } +inline int emitCallHierarchyAlt( const rw::Config& cfg, const rw::IngestResult& ing, const rw::Graph& g, const std::vector& result, + const std::vector& matches, const rw::PageWindow& pw, std::string_view sym, bool wantCallers, + std::size_t bodylessDefsCount, const std::string& chUnprovenAttr, const rw::CallHierarchyRows& chRows, + const rw::OneHopAttribution& chAttr, const rw::DefTableListing& chListing, const HopTestedPartition& chTested, + const std::string& chDeclinedAttr, const std::string& chValueRefsAttr, const std::string& chRootAttr, char* pab, + bool chDiscloseCap, const std::string& chNextAttr, const rw::RowCallSites& chSites, const std::string& chRootPrefix, + const char* tag, const std::string& chDcRows, bool chSingleRoot, const rw::VrRender& chVr, const std::string& chHintCall, + bool chNextIsPage, const rw::DeclinedSiteCut& chDcCut, const std::vector& chDc, + const std::vector& chClosures ) +{ + using namespace rw; + std::vector esc; + const auto ex = [ & ]( std::string_view s ) -> std::string { return std::string( escapeXml( s, esc ) ); }; + const std::string chVrNext = "--uses=" + std::string( sym ); + // lane ix2-slice (IX-2 P2): the callees form's closure blocks — root closures= and the blocks, absent with none + const rw::ClosureBlocksOut chIn = rw::closureBlocks( ing, g, chClosures, chSingleRoot, chRootPrefix, /*json=*/cfg.json && !cfg.columnar ); + if( cfg.columnar ) + { + std::vector page( result.begin() + pw.begin, result.begin() + pw.end ); + const std::string attr = "of=\"" + ex( sym ) + "\" defs=\"" + std::to_string( matches.size() ) + + "\" count=\"" + std::to_string( result.size() ) + "\"" + + ( !wantCallers && bodylessDefsCount > 0 ? " bodyless_defs=\"" + std::to_string( bodylessDefsCount ) + "\"" : "" ) + + chUnprovenAttr + + rw::crossKindAttrXml( chRows.crossKind ) + + rw::defsListedAttrXml( chAttr.groups, chListing ) + + chTested.xmlAttr + + chDeclinedAttr + + chValueRefsAttr + + chIn.head // ix2-slice: closures written in of='s body (rows: the blocks below) + + chRootAttr + + pageDisclosure( pab, kPageDisclosureCap, pw.end - pw.begin, result.size(), pw.end, + cfg.pageLimit, cfg.pageOffset, chDiscloseCap ) + + rw::graphCountFloorAttrXml( g ) + + chNextAttr; + std::vector pageVia( page.size(), 0 ); + std::vector pageSplit( page.size(), 0 ); + std::vector pageOfDef; + for( std::size_t i = 0; i < page.size(); ++i ) + { + pageVia[ i ] = hopViaCode( oneHopRowIsLink( g, matches, result[ pw.begin + i ], wantCallers ), chAttr.nameOnly( pw.begin + i ) ); + pageSplit[ i ] = chAttr.splitArm( pw.begin + i ) ? 1 : 0; + if( chAttr.groups.multi() ) + { + pageOfDef.push_back( rw::ofDefColumnValue( chAttr.groups, chAttr.tokens, pw.begin + i ) ); + } + } + const std::string chDefRows = rw::defTableXml( ing, chAttr.groups, chListing, chRootPrefix ); + const std::string chDcInRows = chDcRows + chIn.rows; + emitColumnarSymbolRows( stdout, ing, tag, attr, page, chRootPrefix, &chTested.testReach, nullptr, + rw::ColumnarRowExtras{ &pageVia, chSites.any ? &chSites.tokens : nullptr, &chSites.totals, + chAttr.groups.multi() ? &pageOfDef : nullptr, &chDefRows, &pageSplit, &chDcInRows } ); // + the and rows verbatim + return 0; + } + rw::emitTo( stdout, "{{\"of\":\"{}\",\"defs\":{},\"count\":{}", jsonStr( sym ).c_str(), matches.size(), result.size() ); + if( !wantCallers && bodylessDefsCount > 0 ) + { + rw::emitTo( stdout, ",\"bodyless_defs\":{}", bodylessDefsCount ); + } + rw::emitTo( stdout, "{}{}{}", rw::unprovenDefsKeyJson( chRows.unprovenDefs ).c_str(), + rw::crossKindKeyJson( chRows.crossKind ).c_str(), + rw::defTableJson( ing, chAttr.groups, chListing, chRootPrefix, /*mcp=*/false ) ); + if( chSingleRoot ) { rw::emitTo( stdout, ",\"root\":\"{}\"", jsonStr( cfg.roots[0] ).c_str() ); } + rw::emitTo( stdout, ",\"hop_tested\":{},\"hop_untested\":{}{}{}", chTested.tested, chTested.untested, + rw::declinedCallsKeyJson( chRows.declinedCalls ) + rw::declinedSiteCutKeyJson( chDcCut ) + + rw::declinedIfaceKeyJson( chRows.declinedIface ) + + rw::stdMemberCallsSpelling( chRows.stdCalls, /*json=*/true ), + ( rw::valueRefsCountKeyJson( chRows.valueRefs.rows.size() ) + rw::valueRefsDepthKeyJson( ing, chRows.valueRefs.depthCut, chVr ) + + rw::patternDispatchKeyJson( chRows.valueRefs.patternSites.size(), chRows.valueRefs.patternWideSites.size() ) ) ); // IX-3: the XML root's pattern counts + rw::emitTo( stdout, "{}{}{}", pageDisclosure( pab, kPageDisclosureCap, pw.end - pw.begin, result.size(), pw.end, + cfg.pageLimit, cfg.pageOffset, chDiscloseCap, kJsonPageSyntax ), + rw::graphCountFloorAttrJson( g ).c_str(), + chNextIsPage ? rw::nextFieldJson( rw::callHierarchyPageNext( wantCallers, sym, cfg.pageLimit, pw.end, false, true ) ) + + rw::nextFieldJson( chHintCall, rw::callHierarchyHintAttr( wantCallers ) ) + : std::string() ); + rw::emitTo( stdout, ",\"{}\":[", tag ); + std::vector chVia( result.size(), 0 ); + bool chHasLink = false; + std::vector chAtKeys( result.size() ); + for( std::size_t i = pw.begin; i < pw.end; ++i ) + { + chVia[ i ] = hopViaCode( oneHopRowIsLink( g, matches, result[ i ], wantCallers ), chAttr.nameOnly( i ) ); + chHasLink = chHasLink || ( chVia[ i ] == 2 ); + if( chSites.any ) + { + chAtKeys[ i ] = rw::callSitesKeyJson( chSites.tokens[ i - pw.begin ], chSites.totals[ i - pw.begin ] ); + } + chAtKeys[ i ] += rw::ofDefKeyJson( chAttr.groups, chAttr.tokens, i ) + rw::splitArmKeyJson( chAttr.splitArm( i ) ); + if( oneHopRowIsLink( g, matches, result[ i ], wantCallers, &g.outAnchorMiss ) ) + { + chAtKeys[ i ] += ",\"anchor\":\"missing\""; + } + } + printJsonSymbolRows( ing, result, pw.begin, pw.end, chRootPrefix, &chTested.testReach, nullptr, &chVia, &chAtKeys ); + rw::emitTo( stdout, "]{}{}{}{}}}", rw::declinedSitesJson( chDc, wantCallers, /*mcp=*/false ), + chIn.head + chIn.rows, // ix2-slice: the closure blocks, JSON spelling + rw::valueRefsJson( ing, chRows.valueRefs, wantCallers, chVr, "vrs", chVrNext ), + chHasLink ? rw::viaLinkNoteJson() : std::string() ); + return 0; +} + std::optional runCallHierarchy( const MainDispatch& d ) { using namespace rw; @@ -269,10 +390,13 @@ std::optional runCallHierarchy( const MainDispatch& d ) const bool chHasModScope = anyModuleScopeRow( ing, chPage ) || anyModuleScopeRow( ing, matches ); // FE-B: exactly when a row on THIS page is via="name" bool chHasVia = false; - for( std::size_t i = pw.begin; i < pw.end && !chHasVia; ++i ) + bool chHasLink = false; + for( std::size_t i = pw.begin; i < pw.end && ( !chHasVia || !chHasLink ); ++i ) { - chHasVia = chAttr.nameOnly( i ); // FE-B's via="name": every edge to the matches by name (defattrib.h, one pass) + chHasVia = chHasVia || chAttr.nameOnly( i ); // FE-B's via="name": every edge to the matches by name (defattrib.h, one pass) + chHasLink = chHasLink || oneHopRowIsLink( g, matches, result[ i ], wantCallers ); } + const bool chHasMiss = rw::pageHasAnchorMiss( g, matches, result, pw.begin, pw.end, wantCallers ); chHasVia = chHasVia || rw::closureBlocksAnyVia( ing, g, chClosures ); // ix2-slice: a closure block's row may carry it too rw::emitTo( stdout, "{}{}{}{}{}{}{}-->{}{}", rw::callHierarchyLegendOpen( wantCallers, chNextIsBare, cfg.columnar, chNextIsPage ).c_str(), rw::capLegendClause( rw::computePageDisclosure( pw.end - pw.begin, result.size(), pw.end, @@ -285,6 +409,7 @@ std::optional runCallHierarchy( const MainDispatch& d ) ( std::string( rw::unprovenDefsLegend( chRows.unprovenDefs > 0 ) ) // H1: likewise, exactly when unproven_defs= is there + rw::crossKindLegend( !chRows.crossKind.empty() ) // hono-07: likewise for cross_kind= + rw::viaNameLegend( chHasVia, false, cfg.columnar ) // FE-B: likewise for via="name" + + rw::viaLinkLegend( chHasLink ) + rw::defTableLegend( chAttr.groups.multi(), chListing.cut ) + rw::splitArmLegend( chHasSplit ) ).c_str(), // attrib-multidef: likewise ( std::string( rw::modScopeLegend( chHasModScope ) ) // #60: likewise, exactly when a t="modscope" row is + rw::callSitesAtLegend( chSites.any, chSites.anyCut, rw::kCallSitesPerRowCap ) ).c_str(), // CALLSITE-AT: likewise for sites_at=/sites_total= @@ -294,6 +419,14 @@ std::optional runCallHierarchy( const MainDispatch& d ) + std::string( rw::closureBlocksLegend( !chClosures.empty() ) ) ).c_str(), // IX-3: exactly when pattern_dispatch= rides; ix2-slice: exactly when an block rides rw::rootRelPathsLegend( chSingleRoot ), rw::multiRootTableLegend( ing.rootLabels.size() >= 2 ) ); + if( cfg.columnar && chHasLink ) + { + rw::emitTo( stdout, "", rw::kViaLinkColumnarClause ); + } + if( chHasMiss ) + { + rw::emitTo( stdout, "", rw::kAnchorMissLegend ); + } } // P3 (L7, nextverb.h): ONE follow-up, shared with MCP. Callers preserve SELECTOR unless a narrowed @@ -311,108 +444,18 @@ std::optional runCallHierarchy( const MainDispatch& d ) const rw::VrRender chVr { chSingleRoot, chRootPrefix }; const std::string chValueRefsAttr = ( rw::valueRefsCountAttrXml( chRows.valueRefs.rows.size() ) + rw::valueRefsDepthAttrXml( ing, chRows.valueRefs.depthCut, chVr ) // + the depth cut that makes it a floor + rw::patternDispatchAttrXml( chRows.valueRefs.patternSites.size(), chRows.valueRefs.patternWideSites.size() ) ); // IX-3: template lookups, never in count= - const std::string chVrNext = "--uses=" + std::string( sym ); + const std::string chVrNext = "--uses=" + std::string( sym ); + if( cfg.columnar || cfg.json ) + { + return emitCallHierarchyAlt( cfg, ing, g, result, matches, pw, sym, wantCallers, bodylessDefsCount, chUnprovenAttr, chRows, + chAttr, chListing, chTested, chDeclinedAttr, chValueRefsAttr, chRootAttr, pab, chDiscloseCap, chNextAttr, + chSites, chRootPrefix, tag, chDcRows, chSingleRoot, chVr, chHintCall, chNextIsPage, chDcCut, chDc, chClosures ); + } // lane ix2-slice (IX-2 P2): the callees form's closure blocks — root closures= and the blocks, absent with none const rw::ClosureBlocksOut chIn = rw::closureBlocks( ing, g, chClosures, chSingleRoot, chRootPrefix, /*json=*/false ); const std::string& chClosuresAttr = chIn.head; const std::string& chInRows = chIn.rows; - // --format=columnar (RESEARCH lever 1): the same page window, re-encoded as a path-table + parallel - // arrays (dedups the repeated per-row markup + paths). Default --format=xml is byte-identical below. - if( cfg.columnar ) - { - std::vector page( result.begin() + pw.begin, result.begin() + pw.end ); - // §B1.1: composed into a std::string, NEVER a fixed `char attrbuf[]`. `of=` echoes a - // caller-supplied symbol NAME, which is unbounded (a markdown SECTION heading routinely runs - // 200-600 chars), so the old 288-byte buffer truncated it mid-attribute at exit 0 — invalid - // XML past ~185 chars once paging flags widened the string, and at a boundary length a - // well-formed tag whose next_offset=/offset=/limit= had been silently amputated. Same - // composition runImpact already used; the columnar-capable family is exactly - // {callers, callees, uses, impact} and this is the last shared site of the first two. - const std::string attr = "of=\"" + ex( sym ) + "\" defs=\"" + std::to_string( matches.size() ) - + "\" count=\"" + std::to_string( result.size() ) + "\"" - + ( !wantCallers && bodylessDefsCount > 0 ? " bodyless_defs=\"" + std::to_string( bodylessDefsCount ) + "\"" : "" ) - + chUnprovenAttr // H1: the decl→def residue, on BOTH directions - + rw::crossKindAttrXml( chRows.crossKind ) // hono-07: defs of 2+ kinds, beside defs= - + rw::defsListedAttrXml( chAttr.groups, chListing ) // attrib-multidef: a cut definition table says so - + chTested.xmlAttr // A6: hop_tested=/hop_untested=, the same partition on every dialect - + chDeclinedAttr // the tier-3 declines, beside the count they are not in - + chValueRefsAttr // value uses, beside the count they are not in (rows: the XML form) - + chClosuresAttr // ix2-slice: closures written in of='s body (rows: the blocks below) - + chRootAttr // R-E: same root= the XML/JSON branches carry - + pageDisclosure( pab, sizeof( pab ), pw.end - pw.begin, result.size(), pw.end, - cfg.pageLimit, cfg.pageOffset, chDiscloseCap ) - + rw::graphCountFloorAttrXml( g ) // §H4 §3.4 — every dialect carries the marker - + chNextAttr; // P3 (L7): the same next= on the columnar root - std::vector pageVia( page.size(), 0 ); // FE-B: the rows' hedge, a dense column when any row carries it - std::vector pageSplit( page.size(), 0 ); // attrib-multidef Q1: likewise for a one-group answer's split arms - std::vector pageOfDef; // attrib-multidef: of_def per row, only on a multi-group answer - for( std::size_t i = 0; i < page.size(); ++i ) - { - pageVia[ i ] = chAttr.nameOnly( pw.begin + i ) ? 1 : 0; - pageSplit[ i ] = chAttr.splitArm( pw.begin + i ) ? 1 : 0; - if( chAttr.groups.multi() ) - { - pageOfDef.push_back( rw::ofDefColumnValue( chAttr.groups, chAttr.tokens, pw.begin + i ) ); - } - } - const std::string chDefRows = rw::defTableXml( ing, chAttr.groups, chListing, chRootPrefix ); - const std::string chDcInRows = chDcRows + chInRows; - emitColumnarSymbolRows( stdout, ing, tag, attr, page, chRootPrefix, &chTested.testReach, nullptr, - rw::ColumnarRowExtras{ &pageVia, chSites.any ? &chSites.tokens : nullptr, &chSites.totals, // CALLSITE-AT: sites_at only when a row has sites - chAttr.groups.multi() ? &pageOfDef : nullptr, &chDefRows, &pageSplit, &chDcInRows } ); // + the and rows verbatim - return 0; - } - - // L2: --json — same rows, keys mirror the XML attr names (of/count/offset/limit/t/n/p). - // §A4c: the page disclosure is pageDisclosure()'s JSON row of the syntax table now, not a hand-rolled - // `offset`+`limit` pair — the SAME seven fields the XML tag two lines below carries (discloseCap=false - // for the same reason: these two verbs have no display cap of their own, so un-paged discloses nothing). - if( cfg.json ) - { - rw::emitTo( stdout, "{{\"of\":\"{}\",\"defs\":{},\"count\":{}", jsonStr( sym ).c_str(), matches.size(), result.size() ); - if( !wantCallers && bodylessDefsCount > 0 ) - { - rw::emitTo( stdout, ",\"bodyless_defs\":{}", bodylessDefsCount ); - } - rw::emitTo( stdout, "{}{}{}", rw::unprovenDefsKeyJson( chRows.unprovenDefs ).c_str(), // H1: absent at zero, like its XML twin - rw::crossKindKeyJson( chRows.crossKind ).c_str(), // hono-07: absent unless defs span 2+ kinds - rw::defTableJson( ing, chAttr.groups, chListing, chRootPrefix, /*mcp=*/false ) ); // attrib-multidef: the table - // R-E: the JSON twin of the XML root= below — right after the leading identifying fields. - if( chSingleRoot ) { rw::emitTo( stdout, ",\"root\":\"{}\"", jsonStr( cfg.roots[0] ).c_str() ); } - rw::emitTo( stdout, ",\"hop_tested\":{},\"hop_untested\":{}{}{}", chTested.tested, chTested.untested, - rw::declinedCallsKeyJson( chRows.declinedCalls ) + rw::declinedSiteCutKeyJson( chDcCut ) - + rw::declinedIfaceKeyJson( chRows.declinedIface ) // A6; then the XML root's declined_calls=/dc_shown=/dc_next=/declined_iface= - + rw::stdMemberCallsSpelling( chRows.stdCalls, /*json=*/true ), // and the XML line - ( rw::valueRefsCountKeyJson( chRows.valueRefs.rows.size() ) + rw::valueRefsDepthKeyJson( ing, chRows.valueRefs.depthCut, chVr ) - + rw::patternDispatchKeyJson( chRows.valueRefs.patternSites.size(), chRows.valueRefs.patternWideSites.size() ) ) ); // IX-3: the XML root's pattern counts - rw::emitTo( stdout, "{}{}{}", pageDisclosure( pab, sizeof( pab ), pw.end - pw.begin, result.size(), pw.end, - cfg.pageLimit, cfg.pageOffset, chDiscloseCap, kJsonPageSyntax ), - rw::graphCountFloorAttrJson( g ).c_str(), // §H4 §3.4 — the JSON dialect's spelling of the same marker - // L8: a capped callers page names the next page and the uses verb, as the XML/columnar root and MCP - // find_referencing_symbols do; the page replays --json. Any other answer keeps its JSON bytes. - chNextIsPage ? rw::nextFieldJson( rw::callHierarchyPageNext( wantCallers, sym, cfg.pageLimit, pw.end, false, true ) ) - + rw::nextFieldJson( chHintCall, rw::callHierarchyHintAttr( wantCallers ) ) - : std::string() ); - rw::emitTo( stdout, ",\"{}\":[", tag ); - std::vector chVia( result.size(), 0 ); // FE-B: the same hedge bit the XML rows carry - std::vector chAtKeys( result.size() ); // CALLSITE-AT: the rows' sites_at/sites_total keys, absent when none - for( std::size_t i = pw.begin; i < pw.end; ++i ) - { - chVia[ i ] = chAttr.nameOnly( i ) ? 1 : 0; - if( chSites.any ) - { - chAtKeys[ i ] = rw::callSitesKeyJson( chSites.tokens[ i - pw.begin ], chSites.totals[ i - pw.begin ] ); - } - chAtKeys[ i ] += rw::ofDefKeyJson( chAttr.groups, chAttr.tokens, i ) + rw::splitArmKeyJson( chAttr.splitArm( i ) ); // attrib-multidef - } - printJsonSymbolRows( ing, result, pw.begin, pw.end, chRootPrefix, &chTested.testReach, nullptr, &chVia, &chAtKeys ); - rw::emitTo( stdout, "]{}{}{}}}", rw::declinedSitesJson( chDc, wantCallers, /*mcp=*/false ), // the XML rows, keys = its attribute names - [ & ] { const rw::ClosureBlocksOut j = rw::closureBlocks( ing, g, chClosures, chSingleRoot, chRootPrefix, /*json=*/true ); return j.head + j.rows; }(), - rw::valueRefsJson( ing, chRows.valueRefs, wantCallers, chVr, "vrs", chVrNext ) ); - return 0; - } - // §P10.6: defs= = resolved definitions this name matched (matches.size()) — the rows below UNION the // neighbors of every def, which --uses/--impact already disclose and these two verbs silently hid. rw::emitTo( stdout, "<{} of=\"{}\" defs=\"{}\" count=\"{}\"{}", tag, ex( sym ).c_str(), matches.size(), result.size(), chRootAttr.c_str() ); @@ -437,7 +480,8 @@ std::optional runCallHierarchy( const MainDispatch& d ) // already follows everywhere else (serialize.h/verbs_for.h), so an untested row costs 0 bytes. rw::emitTo( stdout, "", symTag( s.kind ), ex( s.name ).c_str(), ex( rp ).c_str(), s.line, macroRoleAttr( s.kind ), rw::isTestedByReach( ing, chTested.testReach, result[i] ) ? " tested=\"1\"" : "", - viaNameAttr( chAttr.nameOnly( i ) ), // FE-B: a name-only edge says so on its row + std::string( viaEdgeAttr( chAttr.nameOnly( i ), oneHopRowIsLink( g, matches, result[ i ], wantCallers ) ) ) + + anchorMissAttr( oneHopRowIsLink( g, matches, result[ i ], wantCallers, &g.outAnchorMiss ) ), rw::splitArmAttrXml( chAttr.splitArm( i ) ), rw::ofDefAttrXml( chAttr.groups, chAttr.tokens, i ), // attrib-multidef chSites.any ? rw::callSitesAttrXml( chSites.tokens[ i - pw.begin ], chSites.totals[ i - pw.begin ] ) : std::string() ); // CALLSITE-AT } @@ -991,6 +1035,8 @@ struct SafeDeleteLegendFlags bool defTable = false; // attrib-multidef: the table + of_def= rows (a multi-group selector) bool defTableCut = false; // ... and the table lists only the groups this page's rows name (defs_listed=) bool splitArms = false; // a row is on this page (a one-group selector's split arm, Q1) + bool linkRows = false; // a row is on this page + bool anchorRows = false; // a row is on this page rw::DeclinedSitesLegendFlags declinedSites{}; // lane declined-sites: what the rows on this answer carry (declinedsites.h) }; @@ -1028,6 +1074,8 @@ inline std::string safeDeleteFloorLegend( const SafeDeleteLegendFlags& flags, st clause += "next= is that uses call. "; // runSafeDelete names it exactly then (sdNextAttr) } clause += rw::defTableLegend( flags.defTable, flags.defTableCut ) + rw::splitArmLegend( flags.splitArms ); // attrib-multidef + clause += rw::viaLinkLegend( flags.linkRows ); + clause += rw::anchorMissLegend( flags.anchorRows ); return clause; } @@ -1354,9 +1402,13 @@ std::optional runSafeDelete( const MainDispatch& d ) rw::defTableMarkNamed( sdNamed, sdAttr.groups, sdAttr.tokens, sdLw.begin, sdLw.end ); const rw::DefTableListing sdListing = rw::defTableListing( sdAttr.groups, sdNamed ); bool sdHasSplit = false; - for( std::size_t i = sdLw.begin; i < sdLw.end && !sdHasSplit; ++i ) + bool sdHasLink = false; + bool sdHasMiss = false; + for( std::size_t i = sdLw.begin; i < sdLw.end && ( !sdHasSplit || !sdHasLink || !sdHasMiss ); ++i ) { - sdHasSplit = sdAttr.splitArm( i ); + sdHasSplit = sdHasSplit || sdAttr.splitArm( i ); + sdHasLink = sdHasLink || rw::oneHopRowIsLink( g, defs, callerIds[ i ], true ); + sdHasMiss = sdHasMiss || rw::oneHopRowIsLink( g, defs, callerIds[ i ], true, &g.outAnchorMiss ); } const std::vector sdDc = rw::declinedSiteViews( ing, g, std::span( sdDeclinedSites ).first( sdDcCut.shown ), /*callersForm=*/true, defs, sdAttr.groups, @@ -1368,6 +1420,7 @@ std::optional runSafeDelete( const MainDispatch& d ) .valueRefs = sdValueRefs.rows.size(), .patternDispatch = !sdPatterns.empty() || !sdWide.empty(), .patternWideRows = sdWideRows, .callersFloor = sdFloor.isFloor, .usesFloor = usesFloor, .hasSitesAt = sdSites.any, .sitesCut = sdSites.anyCut, .valueRefDepthCut = sdValueRefs.depthCut.files > 0, .defTable = sdAttr.groups.multi(), .defTableCut = sdListing.cut, .splitArms = sdHasSplit, + .linkRows = sdHasLink, .anchorRows = sdHasMiss, .declinedSites = rw::declinedSitesLegendFlags( sdDc, /*callersForm=*/true, sdDcCut ) } ); const Symbol& lead = ing.symbols[ defs[0] ]; // resolveAllByNameQualified walks ascending id — defs[0] is the @@ -1410,7 +1463,9 @@ std::optional runSafeDelete( const MainDispatch& d ) { rw::emitTo( stdout, " amb=\"{}\"", g.ambOut[ callerId ] ); // M15: the same COUNT a map row's amb= prints — one meaning, one unit } - rw::emitTo( stdout, "{}{}/>", rw::splitArmAttrXml( sdAttr.splitArm( i ) ), rw::ofDefAttrXml( sdAttr.groups, sdAttr.tokens, i ) ); // attrib-multidef + rw::emitTo( stdout, "{}{}{}/>", rw::splitArmAttrXml( sdAttr.splitArm( i ) ), rw::ofDefAttrXml( sdAttr.groups, sdAttr.tokens, i ), + std::string( rw::viaEdgeAttr( false, rw::oneHopRowIsLink( g, defs, callerId, true ) ) ) + + rw::anchorMissAttr( rw::oneHopRowIsLink( g, defs, callerId, true, &g.outAnchorMiss ) ) ); // a direct markdown link } rw::emitTo( stdout, "{}{}", rw::declinedSitesXml( sdDc, /*callersForm=*/true ), // lane declined-sites: each declined possible caller, after the rows rw::valueRefsXml( ing, sdValueRefs, true, sdVr, "--uses=" + std::string( cfg.safeDeleteSym ) ) + rw::patternDispatchXml( ing, sdPatterns, sdVr ) @@ -2473,12 +2528,15 @@ std::optional runPath( const MainDispatch& d ) // ix2-slice (IX-2 P1): the hedged bridge — the fewest-crossings chain over declined / value / closure / namesake hops const rw::PathBridge pthBridge = path.empty() ? rw::pathBridge( ing, g, srcDefs, dstDefs, *pthValueIdx ) : rw::PathBridge{}; const bool pthHasVia = rw::pathHasNameOnlyHop( g, path ); // FE-B: exactly when a hop of THIS path is via="name" + const bool pthHasLink = rw::pathHasNameOnlyHop( g, path, rw::kEdgeLink ); + const bool pthHasMiss = rw::pathHasNameOnlyHop( g, path, 0 ); // attrib-multidef (defattrib.h): from_p=/to_p= only for an endpoint the path bound; prov="split" on a split-arm hop const rw::PathEndpointBinding pthBound = rw::pathEndpointBinding( g, rw::endpointDefinitionCount( ing, srcDefs ), rw::endpointDefinitionCount( ing, dstDefs ), path ); const bool pthHasSplit = pthBound.splitHop; const std::vector pthOwners = rw::pathHopOwners( ing, path ); // PATH-OWNERS: n="Owner.name" per hop rw::emitTo( stdout, "{}", ( rw::unprovenDefsVerbLegend( rw::UnprovenDefsVerb::Path, pthUnprovenDefs > 0 ) + rw::viaNameLegend( pthHasVia ) + + rw::viaLinkLegend( pthHasLink ) + rw::anchorMissLegend( pthHasMiss ) + rw::splitArmLegend( pthHasSplit ) + rw::pathUnboundLegend( !pthBound.from || !pthBound.to ) + rw::pathOwnerLegend( rw::anyPathHopOwner( pthOwners ) ) ).c_str(), ( std::string( rw::toValueRefsLegend( pthToValueRefs.count > 0 ) ) + rw::valueRefsDepthLegend( pthToValueRefs.depthCut.files > 0 ) ).c_str(), @@ -2503,7 +2561,7 @@ std::optional runPath( const MainDispatch& d ) : rw::PathGapsXml{}; rw::emitTo( stdout, "{}>{}{}{}", pthUnreached.rootAttrs, pthUnreached.rows, rw::pathBridgeXml( ing, g, pthBridge, pthSingleRoot, pthRootPrefix ), rw::pathNearSinkXml( ing, pthNear, pthSingleRoot, pthRootPrefix ) ); - // FE-B via="name", attrib-multidef prov="split", PATH-OWNERS n="Owner.name": one emitter, shared with MCP path_between + // FE-B via="name", #409 via="link"/anchor="missing", attrib-multidef prov="split", PATH-OWNERS n="Owner.name": one emitter, shared with MCP path_between rw::emitPathHopRows( stdout, ing, g, path, pthOwners, pthSingleRoot, pthRootPrefix ); rw::emitTo( stdout, "" ); return 0; @@ -2636,6 +2694,7 @@ struct ImpactView const rw::ReachAttribution& attrib; // attrib-multidef (defattrib.h): the definition groups + each reached row's ties const rw::DefTableListing& defListing; // ... and the groups this page's table lists const std::vector& typeOnly; // L7 (typeuse.h ImpactReach::typeOnly): per node, reached only through a type mention; empty when unarmed + std::span seeds; // the matched defs; a depth-1 row's link mark reads the edge caller → seed const rw::ImpactDeclinedRows& dc; // lane ix2-slice (IX-4, declinedsites.h): the rows declined_calls= counts, cut std::span patternSites {}; // IX-3: template lookups that may name SYM — never in reaches= (XML rows) }; @@ -2649,16 +2708,31 @@ inline std::vector impactOfDefKeys( const ImpactView& v ) { keys[ i ] = v.attrib.ofDefJson( v.show[ i ] ); } + for( std::size_t i = v.page.begin; i < v.page.end; ++i ) + { + if( rw::impactDirectLink( v.g, v.seeds, v.depth, v.show[ i ] ) + && rw::oneHopRowIsLink( v.g, v.seeds, v.show[ i ], true, &v.g.outAnchorMiss ) ) + { + keys[ i ] += ",\"anchor\":\"missing\""; + } + } return keys; } -// FE-B: the window's hedge bits in row order (impactRowNameOnly), for the dialects that carry them per entry or per column -inline std::vector impactViaColumn( const ImpactView& v, std::size_t begin, std::size_t end ) +// JSON rows: 2 = a direct markdown link, 1 = via="name". The columnar column uses the same codes. +inline std::vector impactJsonVia( const ImpactView& v ) { std::vector via( v.show.size(), 0 ); - for( std::size_t i = begin; i < end; ++i ) + for( std::size_t i = v.page.begin; i < v.page.end; ++i ) { - via[ i ] = rw::impactRowNameOnly( v.provenReach, v.show[ i ] ) ? 1 : 0; + if( rw::impactDirectLink( v.g, v.seeds, v.depth, v.show[ i ] ) ) + { + via[ i ] = 2; + } + else if( rw::impactRowNameOnly( v.provenReach, v.show[ i ] ) ) + { + via[ i ] = 1; + } } return via; } @@ -2710,7 +2784,7 @@ int emitImpactColumnar( const ImpactView& v ) : " lens=\"shown_importers,importers_capped,importers_next\"" ) + rw::renderDisclosure( v.prD, rw::DiscloseAs::XmlAttrs ) // W2-F + rw::nextAttrXml( rw::nextFlag( "--safe-delete=", v.sym ) ); // P3 (L7): the XML root's next=, same set - const std::vector via = impactViaColumn( v, v.page.begin, v.page.end ); + const std::vector via = impactJsonVia( v ); const std::vector rowVia( via.begin() + v.page.begin, via.begin() + v.page.end ); // FE-B: in `rows` order std::vector rowOfDef; // attrib-multidef: of_def per row, only on a multi-group selector for( std::size_t i = v.page.begin; i < v.page.end && v.attrib.groups.multi(); ++i ) @@ -2767,16 +2841,17 @@ int emitImpactJson( const ImpactView& v ) v.pageLimit, v.pageOffset, true, kJsonPageSyntax ), rw::graphCountFloorAttrJson( v.g ).c_str(), // §H4 §3.4 rw::renderDisclosure( v.prD, rw::DiscloseAs::JsonKeys ).c_str() ); // W2-F: ONE keyset - const std::vector via = impactViaColumn( v, v.page.begin, v.page.end ); // FE-B: the rows' hedge, per entry + const std::vector via = impactJsonVia( v ); // 2 = direct markdown link, 1 = via="name" const std::vector keys = impactOfDefKeys( v ); // attrib-multidef: each row's of_def printJsonSymbolRows( v.ing, v.show, v.page.begin, v.page.end, v.rootPrefix, v.testReach, &v.depth, &via, &keys ); rw::emitTo( stdout, "],\"import_reach\":[" ); rw::emitImportRowsJson( stdout, v.ing, v.importPage, v.rootPrefix, v.importLazyPage ); rw::emitRaw( stdout, "]" ); rw::emitImpactFileRollupJson( stdout, v.ing, v.fileRollup, v.rootPrefix ); // idea #6: present exactly when the XML element is - rw::emitTo( stdout, "{}{}}}", rw::declinedSitesJson( v.dc.views, /*callersForm=*/true, /*mcp=*/false ), // ix2-slice: the XML rows + const std::string imLinkNote = rw::impactViaLinkNote( via, v.page.begin, v.page.end ); + rw::emitTo( stdout, "{}{}{}}}", rw::declinedSitesJson( v.dc.views, /*callersForm=*/true, /*mcp=*/false ), // ix2-slice: the XML rows rw::valueRefsJson( v.ing, v.valueRefs, true, rw::VrRender{ v.singleRoot, v.rootPrefix }, "vrs", - "--uses=" + std::string( v.sym ) ) ); + "--uses=" + std::string( v.sym ) ), imLinkNote.c_str() ); return 0; } @@ -2811,10 +2886,12 @@ int emitImpactXml( const ImpactView& v ) const std::string_view rp = v.singleRoot ? rw::sarif::rootRelativeUri( v.ing.files[ s.fileId ], v.rootPrefix ) : std::string_view( v.ing.files[ s.fileId ] ); // A6: tested="1" only (never a literal 0) — see kTestedRowLegend. 0.6.5: d= the row's hop depth, run-length (graph.h depthRunAttrXml). + const bool imLink = rw::impactDirectLink( v.g, v.seeds, v.depth, v.show[ i ] ); rw::emitTo( stdout, "", symTag( s.kind ), ex( s.name ).c_str(), ex( rp ).c_str(), s.line, rw::depthRunAttrXml( v.show, v.depth, i, v.page.begin ), rw::isTestedByReach( v.ing, *v.testReach, v.show[i] ) ? " tested=\"1\"" : "", - viaNameAttr( rw::impactRowNameOnly( v.provenReach, v.show[i] ) ), // FE-B (D3): reached only through a name-only edge + std::string( viaEdgeAttr( rw::impactRowNameOnly( v.provenReach, v.show[i] ), imLink ) ) + + anchorMissAttr( imLink && rw::oneHopRowIsLink( v.g, v.seeds, v.show[ i ], true, &v.g.outAnchorMiss ) ), v.attrib.ofDefXml( v.show[i] ) ); // attrib-multidef } rw::emitImportRowsXml( stdout, v.ing, v.importPage, v.rootPrefix, v.importLazyPage ); @@ -2825,6 +2902,46 @@ int emitImpactXml( const ImpactView& v ) return 0; } +inline void emitImpactLegend( const rw::Config& cfg, const rw::IngestResult& ing, const rw::Graph& g, const std::vector& reach, + const std::vector& show, const rw::PageWindow& imPage, const rw::ImpactReach& imWalk, + const rw::ImportTier& imports, const std::vector& seeds, const std::vector& imDepth, + const std::vector& imProven, std::size_t imUnprovenDefs, std::size_t imDeclinedCalls, std::size_t imDeclinedIface, + const rw::ImpactFileRollup& imFiles, const rw::ReachAttribution& imAttr, const rw::DefTableListing& imListing, + const rw::ValueRefRows& imValueRefs, const rw::RankDisclosure& prD, const rw::ImpactDeclinedRows& imDc ) +{ + if( cfg.json ) + { + return; + } + const bool imHasModScope = anyModuleScopeRow( ing, std::span( show ).subspan( imPage.begin, imPage.end - imPage.begin ) ); + rw::emitTo( stdout, "{}{}. {}{}{}{}{}{}{}{}{}{}{}{}{}-->", rw::kImpactLegendOpen, rw::kPageRaiseCapClause, + reach.empty() ? std::string() + : std::string( rw::impactDepthLegend( cfg.columnar ) ) + + rw::impactByTypeColumnarLegend( cfg.columnar && std::any_of( show.begin() + imPage.begin, show.begin() + imPage.end, + [ & ]( rw::NodeId n ) { return n < imWalk.typeOnly.size() && imWalk.typeOnly[ n ] != 0; } ) ), + ( std::string( cfg.columnar ? rw::kImpactImportTierColumnarLegend : rw::kImpactImportTierLegend ) + + rw::importersNextLegend( !imports.next.empty() && !cfg.columnar ) ).c_str(), + ( rw::impactTsImportLegend( imports.importsUnresolved, imports.tsconfigUnread ) + + rw::viaNameLegend( std::any_of( show.begin() + imPage.begin, show.begin() + imPage.end, + [ & ]( rw::NodeId n ) { return rw::impactRowNameOnly( imProven, n ); } ), true, cfg.columnar ) + + rw::impactLinkLegendFor( g, seeds, imDepth, show, imPage.begin, imPage.end ) + + rw::impactAnchorLegendFor( g, seeds, imDepth, show, imPage.begin, imPage.end ) ).c_str(), + rw::testedLensLegend( cfg.columnar ), rw::kImpactTestedPartitionLegend, + rw::kTestedLensBlindSpotLegend, + rw::unprovenDefsVerbLegend( rw::UnprovenDefsVerb::Impact, imUnprovenDefs > 0 ).c_str(), + rw::declinedCallsLegendForRows( imDeclinedCalls > 0, g.gateDeclinedCalls > 0, g.stdMemberDeclinedCalls > 0, !imDc.views.empty() ) + + rw::declinedSitesLegend( rw::declinedSitesLegendFlags( imDc.views, /*callersForm=*/true, imDc.cut ) ), // ix2-slice: + the rows, one per counted call + rw::declinedIfaceLegend( imDeclinedIface > 0 ), + ( std::string( rw::modScopeLegend( imHasModScope ) ) + + rw::impactRollupLegend( !imFiles.rows.empty() && !cfg.columnar, imports.umbrella, rw::impactRollupHedged( imFiles ), + !imports.next.empty() && !cfg.columnar, rw::ImpactRollupTypeClauses{ imFiles.typed, imFiles.typeDefs, imFiles.typeCapped } ) + + rw::defTableLegend( imAttr.groups.multi(), imListing.cut ) ).c_str(), + ( std::string( rw::valueRefsReachLegend( !imValueRefs.rows.empty() ) ) + + rw::valueRefsDepthLegendFor( imValueRefs ) + + rw::patternDispatchLegend( !imValueRefs.patternSites.empty() || !imValueRefs.patternWideSites.empty(), true ) ).c_str(), // IX-3: exactly when pattern_dispatch=/pattern_too_wide= rides + rw::graphCountDisclosure( rw::graphGaugeClauses( g ) ).c_str(), rw::renderDisclosure( prD, rw::DiscloseAs::LegendClause ).c_str() ); +} + std::optional runImpact( const MainDispatch& d ) { using namespace rw; @@ -2916,42 +3033,8 @@ std::optional runImpact( const MainDispatch& d ) std::size_t( effectiveRowCap( cfg.pageLimit, rw::kCallHierarchyRowCap ) ), rw::VrRender{ imSingleRoot, imRootPrefix }, /*candidates=*/false ); - if( !cfg.json ) - { // L2: JSON has no comment-node analogue; the XML-only leading doc comment - // §B12.4 in-band (W3FIX): the paging clause comes from pageview.h::kPageRaiseCapClause, which - // carries the limit="0" definition too — rule 7 existed only in --help and that header before. - // §H4 §3.4: opener + the shared floor/counting-unit tail, both from src/graphlegend.h so the MCP - // twin cannot drift from this wording (the §B4 echo-site class). - // LB-H: the import-tier clause is the columnar variant under --format=columnar, because that - // form carries the count without the rows and a reader must be told which shape they hold. - // #60: exactly when a module-scope owner is one of the rows this answer prints — the PAGE, which - // is what `anyModuleScopeRow`'s own contract asks for ("a page of rows, never the corpus"). - const bool imHasModScope = anyModuleScopeRow( ing, std::span( show ).subspan( imPage.begin, imPage.end - imPage.begin ) ); - rw::emitTo( stdout, "{}{}. {}{}{}{}{}{}{}{}{}{}{}{}{}-->", rw::kImpactLegendOpen, rw::kPageRaiseCapClause, - reach.empty() ? std::string() - : std::string( rw::impactDepthLegend( cfg.columnar ) ) // 0.6.5: exactly when rows (d=) and by_depth= exist - + rw::impactByTypeColumnarLegend( cfg.columnar && std::any_of( show.begin() + imPage.begin, show.begin() + imPage.end, // L7: exactly when the column rides - [ & ]( NodeId n ) { return n < imWalk.typeOnly.size() && imWalk.typeOnly[ n ] != 0; } ) ), - ( std::string( cfg.columnar ? rw::kImpactImportTierColumnarLegend : rw::kImpactImportTierLegend ) - + rw::importersNextLegend( !imports.next.empty() && !cfg.columnar ) ).c_str(), // cut-fix E: exactly when the root carries it - ( rw::impactTsImportLegend( imports.importsUnresolved, imports.tsconfigUnread ) // #220: exactly when the root carries them - + rw::viaNameLegend( std::any_of( show.begin() + imPage.begin, show.begin() + imPage.end, // FE-B: exactly when a shown row is via="name" - [ & ]( NodeId n ) { return rw::impactRowNameOnly( imProven, n ); } ), true, cfg.columnar ) ).c_str(), - rw::testedLensLegend( cfg.columnar ), rw::kImpactTestedPartitionLegend, // A6: the columnar form reads its dense column - rw::kTestedLensBlindSpotLegend, // F-02: rides with the partition - rw::unprovenDefsVerbLegend( rw::UnprovenDefsVerb::Impact, imUnprovenDefs > 0 ).c_str(), // H1: exactly when the root carries unproven_defs= - rw::declinedCallsLegendForRows( imDeclinedCalls > 0, g.gateDeclinedCalls > 0, g.stdMemberDeclinedCalls > 0, !imDc.views.empty() ) - + rw::declinedSitesLegend( rw::declinedSitesLegendFlags( imDc.views, /*callersForm=*/true, imDc.cut ) ), // exactly when the root carries declined_calls=; ix2-slice: + the rows, one per counted call - rw::declinedIfaceLegend( imDeclinedIface > 0 ), // likewise, exactly when declined_iface= is there - ( std::string( rw::modScopeLegend( imHasModScope ) ) // #60: likewise, exactly when a t="modscope" row is - + rw::impactRollupLegend( !imFiles.rows.empty() && !cfg.columnar, imports.umbrella, rw::impactRollupHedged( imFiles ), - !imports.next.empty() && !cfg.columnar, rw::ImpactRollupTypeClauses{ imFiles.typed, imFiles.typeDefs, imFiles.typeCapped } ) // idea #6; fix27; FIX1 R1: the XML root's importers_next= (columnar names it in lens=); L7 - + rw::defTableLegend( imAttr.groups.multi(), imListing.cut ) ).c_str(), // attrib-multidef: exactly when the table rides - ( std::string( rw::valueRefsReachLegend( !imValueRefs.rows.empty() ) ) // exactly when the root carries value_refs= - + rw::valueRefsDepthLegendFor( imValueRefs ) // ... and the depth disclosure - + rw::patternDispatchLegend( !imPatterns.empty() || !imValueRefs.patternWideSites.empty(), true ) ).c_str(), // IX-3: exactly when pattern_dispatch=/pattern_too_wide= rides - rw::graphCountDisclosure( rw::graphGaugeClauses( g ) ).c_str(), rw::renderDisclosure( prD, rw::DiscloseAs::LegendClause ).c_str() ); - } + emitImpactLegend( cfg, ing, g, reach, show, imPage, imWalk, imports, seeds, imDepth, imProven, imUnprovenDefs, + imDeclinedCalls, imDeclinedIface, imFiles, imAttr, imListing, imValueRefs, prD, imDc ); // P2.1 + §P8 G1: the rank-ordered listing's 40 is a DEFAULT now, not a ceiling — see the §P10.3 note // above runImpact; pageDisclosure emits the ` shown= capped=` bytes this verb used to hand-roll // (src/pageview.h, THE TRUNCATION VOCABULARY, rules 1-3). LB-G: the same NAMED constant @@ -2961,7 +3044,7 @@ std::optional runImpact( const MainDispatch& d ) imports, importPage, importLazyPage, prD, imSingleRoot, imRootPrefix, imRootAttr, imSingleRoot ? cfg.roots[0] : std::string_view(), cfg.pageLimit, cfg.pageOffset, &imTestReach, imRadiusTested, imRadiusUntested, imDeclinedCalls, imDeclinedIface, g, imDepth, imByDepth, imValueRefs, - imProven, imFiles, imAttr, imListing, imWalk.typeOnly, imDc, imPatterns }; + imProven, imFiles, imAttr, imListing, imWalk.typeOnly, seeds, imDc, imPatterns }; if( cfg.columnar ) { return emitImpactColumnar( view ); } if( cfg.json ) { return emitImpactJson( view ); } diff --git a/test/compactlegendcheck.sh b/test/compactlegendcheck.sh index 616d08bfd..a4239af5d 100755 --- a/test/compactlegendcheck.sh +++ b/test/compactlegendcheck.sh @@ -124,8 +124,12 @@ elif op == "schema": elif op == "rootattrs": # sorted attribute NAMES of the root, `schema` excluded print( " ".join( sorted( set( re.findall( r'([\w:.-]+)="', m.group( 2 ) ) ) - { "schema" } ) ) if m else "" ) elif op == "payload": # the payload with the root's schema attribute removed — the byte-identity operand - if m: # est_tokens= on the root is the price of the EMITTED bytes — a smaller legend is a smaller price, not a row change - tag = m.group( 0 ); pay = pay.replace( tag, re.sub( r'\sest_tokens="[0-9]*"', "", re.sub( r'\sschema="[^"]*"', "", tag ) ), 1 ) + if m: # schema= is the compact dialect's own id + tag = m.group( 0 ); pay = pay.replace( tag, re.sub( r'\sschema="[^"]*"', "", tag ), 1 ) + # est_tokens= prices the emitted bytes. A legend the compact dialect strips moves that price on the + # document root and on an embedded map root (pack-signatures, pack-top-n). A smaller legend is a + # smaller price, not a row change. + pay = re.sub( r'\sest_tokens="[0-9]*"', "", pay ) sys.stdout.write( pay ) elif op == "legend": sys.stdout.write( "".join( leg ) ) elif op == "prose": # PROSE legend bytes: comments this document carries that the FULL document (argv[3]) does not carry @@ -350,6 +354,8 @@ echo "=== (A-PIN) --legend=full is BYTE-IDENTICAL to the pre-L1 default (pinned # RE-ANCHORED 2026-10-18 (train 31, merge of lane hrr over the merged body-fold pin), for.xml only: re-recorded from the # merged binary; a token-level 3-way merge (base = lane body-fold's pin, ours = the merged pin above, theirs = lane hrr's pin; # at= and est_tokens= masked) equals it exactly. 5527 -> 5688 B. The six other pins are unchanged. +# RE-ANCHORED BY HAND 2026-10-19 (markdown section links), map.xml only: the two fixture callees gain via="link", +# the full legend gains the via="link" comment, and est_tokens= prices them, 901 -> 929. PIN_DIR="$ROOT/test/compactlegendfix/pre_l1_full" # the one normalisation, in python on BOTH sides so no sed dialect decides it (BSD sed appends a final newline, GNU # sed does not): at="…" masked, trailing newlines dropped. @@ -630,12 +636,13 @@ probeFor() # reading (+~75 B) rides only an answer whose row names a site. Nothing else moved. Commit 2 (closure containment) adds # "|closure" to the ns d= reading: measured 852 B, inside the same 860 B pin. The OLD path keeps its pin: the # (U-twin-path) arm after the universe loop cuts exactly those readings out of the same probe and holds the rest at 550 B. +# RE-PINNED 2026-10-19 (markdown section links): the via=link reading and the deleted same-pair sentence. PIN_TABLE=' ripwire.edit-check/v1 650 636 ripwire.map/v1 1140 1129 -ripwire.map-diff/v1 900 885 -ripwire.pack-signatures/v1 770 759 -ripwire.metrics/v1 1040 1021 +ripwire.map-diff/v1 970 953 +ripwire.pack-signatures/v1 840 827 +ripwire.metrics/v1 1090 1080 ripwire.deps/v1 780 764 ripwire.hotspots/v1 530 518 ripwire.clones/v1 810 799 @@ -676,7 +683,7 @@ ripwire.mentions/v1 260 243 ripwire.affected/v1 840 826 ripwire.verify/v1 440 421 ripwire.help-task/v1 440 429 -ripwire.query/v1 720 707 +ripwire.query/v1 790 775 ripwire.grep/v1 1380 1368 ripwire.match/v1 370 355 ripwire.lego/v1 290 275 @@ -695,7 +702,7 @@ ripwire.whereis/v1 1050 1037 ripwire.community/v1 730 719 ripwire.layout/v1 1220 1203 ripwire.pack-task/v1 990 974 -ripwire.pack-top-n/v1 760 745 +ripwire.pack-top-n/v1 830 813 ripwire.expand/v1 280 265 ripwire.expand-file/v1 510 508 ripwire.for/v1 1100 1099 diff --git a/test/compactlegendfix/pre_l1_full/map.xml b/test/compactlegendfix/pre_l1_full/map.xml index 01d2de816..6e28ab00c 100644 --- a/test/compactlegendfix/pre_l1_full/map.xml +++ b/test/compactlegendfix/pre_l1_full/map.xml @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/test/fillordercheck.sh b/test/fillordercheck.sh index b22f252cb..6077b8610 100755 --- a/test/fillordercheck.sh +++ b/test/fillordercheck.sh @@ -132,7 +132,9 @@ OFIX="$( order_of test/fixture )" # RE-PIN 2026-10-04 (train 25): 863 -> 905, the document GREW by its legend and nothing else. The v1 header legend's # unresolved=/external= clauses now state the false-edge rule (+106 B); test/golden.xml was re-recorded beside it and differs # from its previous self by exactly those two clauses and est_tokens=. Order is unchanged (important-first). -{ [ "$EFIX" = "905" ] && [ "$OFIX" = "important-first" ]; } \ +# RE-PIN 2026-10-19 (markdown section links): 905 -> 933. The full map gained the via="link" comment and the two +# fixture edges ([[notes]] and [[related]]); order is unchanged. +{ [ "$EFIX" = "933" ] && [ "$OFIX" = "important-first" ]; } \ && ok "test/fixture (est_tokens=$EFIX) does NOT auto-flip — order=$OFIX (golden neutral)" \ || no "test/fixture unexpectedly changed order or est_tokens (est=$EFIX order=$OFIX)" diff --git a/test/golden.xml b/test/golden.xml index ea21e4313..93862cf78 100644 --- a/test/golden.xml +++ b/test/golden.xml @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/test/mdanchorfix/README.md b/test/mdanchorfix/README.md new file mode 100644 index 000000000..80c52b68a --- /dev/null +++ b/test/mdanchorfix/README.md @@ -0,0 +1,5 @@ +# Root Readme + +## Languages + +The root languages section. diff --git a/test/mdanchorfix/docs/README.md b/test/mdanchorfix/docs/README.md new file mode 100644 index 000000000..cecfaeee8 --- /dev/null +++ b/test/mdanchorfix/docs/README.md @@ -0,0 +1,5 @@ +# Docs Readme + +## Languages + +The docs-directory readme, not the root one. diff --git a/test/mdanchorfix/docs/a/README.md b/test/mdanchorfix/docs/a/README.md new file mode 100644 index 000000000..3e93c7af3 --- /dev/null +++ b/test/mdanchorfix/docs/a/README.md @@ -0,0 +1,3 @@ +# A readme + +Same filename as docs/b/README.md. A name-only guess from this directory lands here. diff --git a/test/mdanchorfix/docs/a/guide.md b/test/mdanchorfix/docs/a/guide.md new file mode 100644 index 000000000..e23235868 --- /dev/null +++ b/test/mdanchorfix/docs/a/guide.md @@ -0,0 +1,43 @@ +# Guide + +## Install + +The guide's own install section. + +## Self Collision + +See [partner install](../b/partner.md#install). + +## Path Choice + +See [setup](../b/notes.md#setup). + +## Root Readme Link + +See [languages](../../README.md#languages). + +## Dup Link + +See [notes](../b/dup.md#notes). + +## Func Name + +See [work](../b/partner.md#do_work). + +## Missing Anchor + +See [nothing](../b/partner.md#no-such-heading). + +## Fence Only + +```python +[fenced](../b/partner.md#install) +``` + +## Missed Readme + +See [gone](../b/README.md#no-such-heading). + +## Bare Readme + +See [the page](../b/README.md). diff --git a/test/mdanchorfix/docs/a/notes.md b/test/mdanchorfix/docs/a/notes.md new file mode 100644 index 000000000..ca62ba09e --- /dev/null +++ b/test/mdanchorfix/docs/a/notes.md @@ -0,0 +1,5 @@ +# Notes Alpha + +## Setup Alpha + +The notes beside the guide. diff --git a/test/mdanchorfix/docs/a/tools.py b/test/mdanchorfix/docs/a/tools.py new file mode 100644 index 000000000..8e33ea784 --- /dev/null +++ b/test/mdanchorfix/docs/a/tools.py @@ -0,0 +1,2 @@ +def do_work(): + return 1 diff --git a/test/mdanchorfix/docs/a/use_work.py b/test/mdanchorfix/docs/a/use_work.py new file mode 100644 index 000000000..9c5c09dce --- /dev/null +++ b/test/mdanchorfix/docs/a/use_work.py @@ -0,0 +1,2 @@ +def use_work(): + return do_work() diff --git a/test/mdanchorfix/docs/b/README.md b/test/mdanchorfix/docs/b/README.md new file mode 100644 index 000000000..4c7cbbd29 --- /dev/null +++ b/test/mdanchorfix/docs/b/README.md @@ -0,0 +1,3 @@ +# B readme + +The file a `../b/README.md` link names. It has no heading for the missing slug. diff --git a/test/mdanchorfix/docs/b/dup.md b/test/mdanchorfix/docs/b/dup.md new file mode 100644 index 000000000..5183d9cd9 --- /dev/null +++ b/test/mdanchorfix/docs/b/dup.md @@ -0,0 +1,9 @@ +# Dup + +## Notes + +First. + +## Notes + +Second. diff --git a/test/mdanchorfix/docs/b/notes.md b/test/mdanchorfix/docs/b/notes.md new file mode 100644 index 000000000..6e790643b --- /dev/null +++ b/test/mdanchorfix/docs/b/notes.md @@ -0,0 +1,5 @@ +# Notes Beta + +## Setup + +The notes in the other directory. diff --git a/test/mdanchorfix/docs/b/partner.md b/test/mdanchorfix/docs/b/partner.md new file mode 100644 index 000000000..5e7a3d4c0 --- /dev/null +++ b/test/mdanchorfix/docs/b/partner.md @@ -0,0 +1,9 @@ +# Partner + +## Install + +The partner install section. + +## do_work + +The partner heading named like a python function. diff --git a/test/mdsectioncheck.sh b/test/mdsectioncheck.sh index c662d070e..283c6c3a1 100755 --- a/test/mdsectioncheck.sh +++ b/test/mdsectioncheck.sh @@ -11,7 +11,8 @@ # canonical id is path::Parent::Child — identity stays PATH-QUALIFIED (the churn-keying # lesson: same heading text across files must never collide); # - markdown links are edges: [text](other.md) / [label]: other.md → doc→doc (file node, the -# wikilink machinery), [text](#anchor) → doc-section→doc-section IN-FILE; `backtick` +# wikilink machinery), [text](#anchor) → doc-section→doc-section IN-FILE, +# [text](other.md#slug) → that file's heading (stem if the heading is absent); `backtick` # mentions stay doc→code and now attribute to their ENCLOSING SECTION; # - fenced/tilde/indented code, html blocks, YAML front-matter and blockquoted headings are # NOT document structure: nothing inside them becomes a symbol, and fenced code is never @@ -76,6 +77,8 @@ guard "$G" '# zqhtmlphantom' 'the html-block decoy heading' guard "$G" '# zqindentphantom' 'the indented-code decoy heading' guard "$G" '> # Quoted Phantom' 'the blockquoted decoy heading' guard "$G" '(partner.md)' 'the inline link to partner.md' +guard "$G" '(partner.md#install-steps)' 'the cross-file heading link' +guard "$G" '(alt.markdown#no-such-heading)' 'the cross-file link whose heading is absent' guard "$G" '(#result-tables)' 'the in-file anchor link' guard "$G" '[[decoy]]' 'the wikilink to decoy' guard "$G" '[zqref]: decoy.md' 'the reference-style link definition' @@ -252,6 +255,8 @@ for m in re.finditer(r']*>(.*?)', xml, re.S): PYEOF edge(){ if grep -qF "$1 -> $2" "$TMP/edges.txt"; then ok "edge: $1 -> $2 ($3)"; else no "edge MISSING: $1 -> $2 ($3)"; fi; } edge "Cache Warm Path" "partner" 'inline [text](partner.md) link, attributed to its section' +edge "Cache Warm Path" "Install Steps" 'cross-file [text](partner.md#install-steps) — that file'\''s heading' +edge "Cache Warm Path" "alt" 'cross-file [text](alt.markdown#no-such-heading) falls back to the file stem' edge "Cache Warm Path" "Result Tables" 'in-file [text](#result-tables) anchor — doc-section→doc-section' edge "Result Tables" "decoy" '[[wikilink]], attributed to its section' edge "Deep Appendix" "decoy" 'reference-style [zqref]: decoy.md definition' @@ -351,6 +356,293 @@ $BIN "$FIX" --no-cache --mentions=codeIdentFn >"$TMP/men.xml" 2>/dev/null grep -qF 'guide.md' "$TMP/men.xml" && ok "--mentions=codeIdentFn names guide.md (backtick doc→code edge)" \ || no "--mentions=codeIdentFn lost the guide.md mention" +# ═══════════════════════════════════════════════════════════════════════════ +echo +echo "=== cross-file [text](path#slug) binds the EXACT (file, slug) heading, never a bare name ===" +# ═══════════════════════════════════════════════════════════════════════════ +# Each arm reads --callees of ONE linking section and compares the rows' n=@p= (file, no line). The +# fixture is built so a name-only resolver lands on the wrong target: the linking file has its own +# `## Install`; docs/a/notes.md and docs/b/notes.md both exist; docs/README.md shadows the root README. +AFIX="$ROOT/test/mdanchorfix" +[ -d "$AFIX" ] || { echo "no fixture at $AFIX"; exit 2; } +callees(){ # callees DIR SECTION → "n@file" per callee row, sorted, one per line + $BIN "$1" --no-cache --callees="$2" --legend=compact 2>/dev/null \ + | python3 -c ' +import sys, re +for m in re.finditer(r"/dev/null | grep -q 'n="partner"[^>]*anchor="missing"' \ + && ! $BIN "$AFIX" --no-cache --callees='Bare Readme' --legend=compact 2>/dev/null | grep -q 'anchor="missing"'; then + ok "a slug the file does not have is anchor=missing, and a bare link is not" +else + no "anchor=missing missed the slug fallback, or a bare link carried it" +fi +if $BIN "$AFIX" --no-cache --format=columnar --callees='Func Name' --legend=compact 2>/dev/null | grep -q '2'; then + ok "columnar via is 2 for a direct markdown link" +else + no "columnar via did not write 2 for the markdown link" +fi +want "$AFIX" "Fence Only" '' "a link inside a fenced block makes no edge" +# anchor="missing" is a CLAIM that the heading is absent, so it is made only when the slug is provably absent. Four shapes the +# slug function does not model reach the same file-node fallback as a truly missing heading (a non-ASCII or percent-encoded slug, +# GitHub's -N duplicate suffix, and a file that is only one heading line, whose heading IS the file node): each keeps its edge +# and carries NO mark. Each arm links from its own section, so the rows do not merge into one pair. +mkdir -p "$TMP/amiss/docs" +printf '%s\n' '# Doc' '' '## Überblick' '' 'x' '' '## 概要' '' 'y' '' '## Dup' '' 'a' '' '## Dup' '' 'b' '' '## Real Heading' '' 'z' > "$TMP/amiss/docs/t.md" +printf '%s\n' '# Title' > "$TMP/amiss/docs/one.md" +printf '%s\n' '# Links' '' \ + '## UberLink' '' '[a](t.md#überblick)' '' '## GaiLink' '' '[a](t.md#概要)' '' '## DupLink' '' '[a](t.md#dup-1)' '' \ + '## OneLink' '' '[a](one.md#title)' '' '## PctLink' '' '[a](t.md#real%20heading)' '' '## EncLink' '' '[a](t.md#%C3%BCberblick)' '' \ + '## MissLink' '' '[a](t.md#nonexistent)' '' '## MissNumLink' '' '[a](t.md#bar-2)' '' '## OneMissLink' '' '[a](one.md#nothere)' \ + > "$TMP/amiss/docs/links.md" +amiss_row(){ $BIN "$TMP/amiss" --no-cache --callees="$1" --legend=compact 2>/dev/null; } +amiss_unmarked(){ # amiss_unmarked SECTION TARGET WHY: the edge exists (via=link) and says nothing about a missing anchor + row="$( amiss_row "$1" )" + if printf '%s' "$row" | grep -q "n=\"$2\"[^>]*via=\"link\"" && ! printf '%s' "$row" | grep -q 'anchor="missing"'; then ok "$3"; else no "$3 — got [$( printf '%s' "$row" | grep -o ']*>' )]"; fi +} +amiss_marked(){ # amiss_marked SECTION TARGET WHY: the same fallback, marked, because the slug is absent + if amiss_row "$1" | grep -q "n=\"$2\"[^>]*via=\"link\"[^>]*anchor=\"missing\""; then ok "$3"; else no "$3"; fi +} +amiss_unmarked UberLink "t" "#überblick (a non-ASCII slug, heading present) keeps the file edge, no anchor=missing" +amiss_unmarked GaiLink "t" "#概要 (a CJK slug, heading present) keeps the file edge, no anchor=missing" +amiss_unmarked DupLink "t" "#dup-1 (GitHub's duplicate suffix, second ## Dup present) keeps the file edge, no anchor=missing" +amiss_unmarked OneLink "Title" "one.md#title (the file is just that heading) keeps the file edge, no anchor=missing" +amiss_unmarked PctLink "t" "#real%20heading (percent-encoded, heading present) keeps the file edge, no anchor=missing" +amiss_unmarked EncLink "t" "#%C3%BCberblick (percent-encoded UTF-8, heading present) keeps the file edge, no anchor=missing" +amiss_marked MissLink "t" "a plain ASCII slug no heading has is still anchor=missing" +amiss_marked MissNumLink "t" "#bar-2 with no ## Bar (the -N stem is absent) is still anchor=missing" +amiss_marked OneMissLink "Title" "one.md#nothere (a heading-only file, other slug) is still anchor=missing" + +# The same claim rests on mdSlugOf matching the slug GitHub gives the RENDERED heading. A heading that holds inline markup (a link, +# _emphasis_, an HTML tag, `*`) is slugged from its raw source by mdSlugOf, so a link that names the rendered slug misses it. A file +# with such a heading proves nothing by a miss: the edge stays, unmarked. A plain file, and snake_case (no emphasis), still mark. +mkdir -p "$TMP/mkup" +printf '%s\n' '# Doc' '' '## See [docs](x.md)' '' 'a' '' '## _Setup_' '' 'b' '' '## Plain Head' '' 'c' > "$TMP/mkup/m.md" +printf '%s\n' '# _Solo_' > "$TMP/mkup/solo.md" +printf '%s\n' '# Doc' '' '## snake_case' '' 'a' '' '## Other' '' 'b' > "$TMP/mkup/s.md" +printf '%s\n' '# Links' '' \ + '## SeeLink' '' '[a](m.md#see-docs)' '' '## SetupLink' '' '[a](m.md#setup)' '' '## PlainLink' '' '[a](m.md#plain-head)' '' \ + '## MarkupMiss' '' '[a](m.md#nonexistent)' '' '## SoloLink' '' '[a](solo.md#solo)' '' \ + '## SnakeLink' '' '[a](s.md#snake_case)' '' '## SnakeMiss' '' '[a](s.md#absent)' > "$TMP/mkup/links.md" +mkup_row(){ $BIN "$TMP/mkup" --no-cache --callees="$1" --legend=compact 2>/dev/null; } +mkup_unmarked(){ # mkup_unmarked SECTION TARGET WHY: the edge exists (via=link) and says nothing about a missing anchor + row="$( mkup_row "$1" )" + if printf '%s' "$row" | grep -q "n=\"$2\"[^>]*via=\"link\"" && ! printf '%s' "$row" | grep -q 'anchor="missing"'; then ok "$3"; else no "$3 — got [$( printf '%s' "$row" | grep -o ']*>' )]"; fi +} +mkup_unmarked SeeLink "m" "#see-docs over '## See [docs](x.md)' (rendered slug see-docs) keeps the file edge, no anchor=missing" +mkup_unmarked SetupLink "m" "#setup over '## _Setup_' (rendered slug setup) keeps the file edge, no anchor=missing" +mkup_unmarked MarkupMiss "m" "a slug no heading has, in a file whose headings hold inline markup, is not provably absent: no anchor=missing" +mkup_unmarked SoloLink "_Solo_" "#solo over a file that is just '# _Solo_' keeps the file edge, no anchor=missing" +if mkup_row PlainLink | grep -q 'n="Plain Head"[^>]*via="link"' && ! mkup_row PlainLink | grep -q 'anchor="missing"'; then ok "a plain heading in the markup file still binds exactly"; else no "a plain heading beside markup headings lost its exact bind"; fi +if mkup_row SnakeLink | grep -q 'n="snake_case"[^>]*via="link"' && ! mkup_row SnakeLink | grep -q 'anchor="missing"'; then ok "#snake_case binds the snake_case heading exactly"; else no "#snake_case did not bind its heading"; fi +if mkup_row SnakeMiss | grep -q 'n="s"[^>]*via="link"[^>]*anchor="missing"'; then ok "a missing slug beside a snake_case heading (no emphasis) is still anchor=missing"; else no "snake_case wrongly counted as inline markup: the plain miss lost its mark"; fi + +# --json map rows read the same edge-kind byte as the XML twin: a markdown link is "via":"link" (never "name"), and a missing +# anchor is "anchor":"missing". +if $BIN "$TMP/amiss" --no-cache --json 2>/dev/null | python3 -c ' +import sys, json +d = json.load(sys.stdin) +rows = {s["n"]: s for f in d["r"] for s in f.get("s", [])} +c = {x["n"]: x for x in rows["MissLink"]["c"]} +u = {x["n"]: x for x in rows["UberLink"]["c"]} +assert c["t"].get("via") == "link" and c["t"].get("anchor") == "missing", c +assert u["t"].get("via") == "link" and "anchor" not in u["t"], u +'; then ok "--json map: a link row is via=link (anchor=missing only on the provably missing one), not via=name"; else no "--json map rows spell a markdown link as via=name or drop its anchor mark"; fi + +# Callers that reach a namesake only through a markdown link are no by-name hit (near_sink): Caller links d.md#install, a different +# section than b.md's Install, whose name it shares. The link is proven; nothing was matched by name. +mkdir -p "$TMP/ngap" +printf '%s\n' '# B' '' '## Start' '' 'hello' '' '## Install' '' 'steps' > "$TMP/ngap/b.md" +printf '%s\n' '# C' '' '## Caller' '' '[x](d.md#install)' > "$TMP/ngap/c.md" +printf '%s\n' '# D' '' '## Install' '' 'other' > "$TMP/ngap/d.md" +NG="$( $BIN "$TMP/ngap" --no-cache --path='Start,b.md::B::Install' --legend=compact 2>/dev/null )" +if printf '%s' "$NG" | grep -q ']*>' )"; fi + +# names-only rows (--for hops) merge only rows of the same anchor state: Hub links a.md#b (a real heading named B, l=3) +# and b.md#nope (missing, so the file node B, l=1). The merged row used to read l="1,3" anchor="missing", marking the good link. +mkdir -p "$TMP/nmerge" +printf '%s\n' '# Hub' '' '## Hub Section' '' 'zqhubtoken [first](b.md#nope) and [second](a.md#b) and [third](e.md#zzz)' > "$TMP/nmerge/x.md" +printf '%s\n' '# A' '' '## B' '' 'text' > "$TMP/nmerge/a.md" +printf '%s\n' '# B' > "$TMP/nmerge/b.md" +printf '%s\n' '# E' '' '## Other' > "$TMP/nmerge/e.md" +NM="$( $BIN "$TMP/nmerge" --no-cache --for=zqhubtoken --legend=compact 2>/dev/null | grep -o '.*' )" +if printf '%s' "$NM" | grep -q '' && printf '%s' "$NM" | grep -q '' && ! printf '%s' "$NM" | grep -q 'l="1,3"'; then ok "--for hops: the good B link and the missing-anchor B link stay two rows"; else no "--for hops merged a good link into a missing-anchor row: [$NM]"; fi + +# A --for head that already defines via="name" must still define via="link" when a hop row carries it (run has a proven and a +# by-name callee; Hub Section links): the link reading rides on its own presence, not on the name legend's. +mkdir -p "$TMP/vlink/pkg/x" "$TMP/vlink/other" +printf 'class A:\n def process(self):\n return 1\n' > "$TMP/vlink/pkg/x/a.py" +printf 'def helper():\n return 3\n\ndef run(obj):\n # zqshared\n helper()\n return obj.process()\n' > "$TMP/vlink/pkg/x/c.py" +printf 'def target():\n return 2\n\nclass B:\n def process(self):\n return target()\n' > "$TMP/vlink/other/b.py" +printf '%s\n' '# Guide' '' '## Hub Section' '' 'zqshared see [more](dd.md#zzz) and [ok](dd.md#real-part)' > "$TMP/vlink/guide.md" +printf '%s\n' '# DD' '' '## Real Part' '' 'x' > "$TMP/vlink/dd.md" +for leg in default full; do + if [ "$leg" = default ]; then VL="$( $BIN "$TMP/vlink" --no-cache --for=zqshared 2>/dev/null )"; else VL="$( $BIN "$TMP/vlink" --no-cache --for=zqshared --legend=full 2>/dev/null )"; fi + if printf '%s' "$VL" | grep -q 'via="name"' && printf '%s' "$VL" | grep -q 'via="link"' && printf '%s' "$VL" | grep -q 'markdown link, not a call'; then ok "--for ($leg legend): a via=name head and a via=link hop row both carry their legend"; else no "--for ($leg legend): via=link rows ride without their legend, or the fixture lost a row kind"; fi +done +# --callers lists every matched def in (cross_kind when a heading and a function share the name). +# The edge is the row: it must name the section, never the .py function. +if $BIN "$AFIX" --no-cache --callers=do_work --legend=compact 2>/dev/null | python3 -c ' +import sys, re +xml = sys.stdin.read() +kind = { m.group(1): m.group(2) for m in re.finditer(r"]*)/?>", xml)) +ok = bool(rows) and "fn" in kind.values() and "sec" in kind.values() +saw_sec = False +saw_fn = False +for m in rows: + attrs = m.group(1) + p = re.search(r" p=\"([^\"]*)\"", attrs) + of = re.search(r"of_def=\"(\d+)", attrs) + via = re.search(r" via=\"([^\"]*)\"", attrs) + if p and "tools.py" in p.group(1): + ok = False + if of and kind.get(of.group(1)) == "fn": + saw_fn = True + if via and via.group(1) == "link": + ok = False + else: + if not via or via.group(1) != "link": + ok = False + else: + saw_sec = True +sys.exit(0 if ok and saw_sec and saw_fn else 1) +'; then ok "the section caller stays via=link and the Python caller of do_work is unmarked"; else no "the doc link leaked onto the .py function, or the section row lacks via=link"; fi +if ( cd "$AFIX" && $BIN . --no-cache --callers=do_work --legend=compact ) 2>/dev/null | grep -q 'n="Func Name"[^>]*via="link"'; then + ok "a dot crawl root still pins the markdown link (./docs/... matches docs/...)" +else + no "a dot crawl root dropped the markdown link pin" +fi +QUAL=$($BIN "$AFIX" --no-cache --callers='docs/a/tools.py:do_work' --legend=compact 2>/dev/null || true) +if printf '%s' "$QUAL" | grep -q 'count="[1-9]' && printf '%s' "$QUAL" | grep -q 'n="use_work"' && ! printf '%s' "$QUAL" | grep -q 'via="link"'; then + ok "the .py do_work has a real caller and that row is not via=link" +else + no "the qualified .py do_work callers were empty or carried via=link" +fi +if $BIN "$AFIX" --no-cache --callees='Self Collision' --legend=compact 2>/dev/null | grep -q 'n="Install"[^>]*via="link"'; then + ok "Self Collision's callee Install is via=link" +else + no "Self Collision's callee Install lacks via=link" +fi +if $BIN "$AFIX" --no-cache --json --callees='Self Collision' 2>/dev/null | grep -q '"via":"link"'; then + ok "the JSON callee row spells via=link" +else + no "the JSON callee row lacks via=link" +fi +if $BIN "$AFIX" --no-cache --json --callers=do_work 2>/dev/null | python3 -c ' +import json, sys +d = json.load(sys.stdin) +rows = d.get("callers", []) +sec = any(r.get("t") == "sec" and r.get("via") == "link" for r in rows) +fn = any(r.get("t") == "fn" and r.get("via") == "link" for r in rows) +sys.exit(0 if sec and d.get("via_link_note") and not fn else 1) +'; then ok "the JSON callers answer marks the section row and carries via_link_note"; else no "the JSON callers answer lacks via_link_note, or the function row is marked via=link"; fi +mcp_text(){ + printf '%s\n' '{"jsonrpc":"2.0","id":1,"method":"initialize"}' "$1" \ + | "$BIN" --mcp 2>/dev/null | tail -1 | python3 -c ' +import sys, json +r = json.load(sys.stdin) +print("__ERROR__:" + r["error"].get("message", "") if "error" in r else r["result"]["content"][0]["text"]) +' +} +call(){ printf '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"%s","arguments":%s}}' "$1" "$2"; } +M="$( mcp_text "$( call find_referencing_symbols '{"path":"'"$AFIX"'","symbol":"do_work"}' )" )" +printf '%s' "$M" | python3 -c ' +import json, sys +d = json.load(sys.stdin) +rows = d.get("calledBy", []) +sec = any(r.get("kind") == "sec" and r.get("via") == "link" for r in rows) +fn = any("tools.py" in r.get("file", "") and r.get("via") == "link" for r in rows) +sys.exit(0 if sec and d.get("via_link_note") and not fn else 1) +' && ok "find_referencing_symbols marks the section row via=link and carries via_link_note" \ + || no "find_referencing_symbols lacks via=link on the section row, or marks the .py caller" +M="$( mcp_text "$( call find_symbol '{"path":"'"$AFIX"'","symbol":"Self Collision"}' )" )" +printf '%s' "$M" | python3 -c ' +import json, sys +d = json.load(sys.stdin) +rows = d.get("calls", []) +hit = any(r.get("name") == "Install" and r.get("via") == "link" for r in rows) +sys.exit(0 if hit and d.get("via_link_note") else 1) +' && ok "find_symbol marks Install via=link and carries via_link_note" \ + || no "find_symbol lacks via=link on Install or lacks via_link_note" + +# A direct doc caller is via="link" on --safe-delete and --impact. The counts still include it. +mkdir -p "$TMP/runfix/src" "$TMP/runfix/docs" +printf '%s\n' 'def run():' ' return 1' '' 'def main():' ' run()' > "$TMP/runfix/src/app.py" +printf '%s\n' '# API' '' '## run' > "$TMP/runfix/docs/api.md" +printf '%s\n' '# Guide' '' '## Run Notes' '' 'see [run](api.md#run)' > "$TMP/runfix/docs/guide.md" +SD="$( $BIN "$TMP/runfix" --no-cache --safe-delete=run --legend=compact 2>/dev/null )" +printf '%s' "$SD" | grep -q 'callers="2"' \ + && printf '%s' "$SD" | grep -q 'n="Run Notes"[^>]*via="link"' \ + && ! printf '%s' "$SD" | grep -q 'n="main"[^>]*via="link"' \ + && ok "safe-delete marks Run Notes via=link and still counts both callers" \ + || no "safe-delete dropped the link mark or the caller count" +IM="$( $BIN "$TMP/runfix" --no-cache --impact=run --legend=compact 2>/dev/null )" +printf '%s' "$IM" | grep -q 'reaches="2"' \ + && printf '%s' "$IM" | grep -q 'n="Run Notes"[^>]*via="link"' \ + && ! printf '%s' "$IM" | grep -q 'n="main"[^>]*via="link"' \ + && ok "impact marks Run Notes via=link and still counts both rows" \ + || no "impact dropped the link mark or the reach count" +IJ="$( $BIN "$TMP/runfix" --no-cache --impact=run --json 2>/dev/null )" +printf '%s' "$IJ" | grep -q 'via_link_note' \ + && ok "impact json carries via_link_note" \ + || no "impact json omitted via_link_note" + +# In-file anchors, wikilinks, and root-relative links are via="link". A code call on the same name stays unmarked. +mkdir -p "$TMP/kinds/src" "$TMP/kinds/docs" "$TMP/kinds/notes" +printf '%s\n' 'def api():' ' return 1' '' 'def main():' ' api()' > "$TMP/kinds/src/app.py" +printf '%s\n' '# api' > "$TMP/kinds/docs/api.md" +printf '%s\n' '# decoy' > "$TMP/kinds/notes/api.md" +printf '%s\n' '# Guide' '' '## Hub' '' '[down](#later)' '' '[a](api.md)' '' '[b](/docs/api.md)' '' '[[api]]' '' '## Later' '' 'notes' > "$TMP/kinds/docs/guide.md" +HUB="$( $BIN "$TMP/kinds" --no-cache --callees=Hub --legend=compact 2>/dev/null )" +printf '%s' "$HUB" | grep -q 'n="Later"[^>]*via="link"' \ + && printf '%s' "$HUB" | grep -q 'n="api"[^>]*via="link"' \ + && ok "in-file, wikilink, and root-relative links are via=link" \ + || no "an in-file, wikilink, or root-relative link lost via=link" +API="$( $BIN "$TMP/kinds" --no-cache --callers=api --legend=compact 2>/dev/null )" +printf '%s' "$API" | grep -q 'n="Hub"[^>]*via="link"' \ + && printf '%s' "$API" | grep -q 'n="main"' \ + && ! printf '%s' "$API" | grep -q 'n="main"[^>]*via="link"' \ + && ok "a code call to api stays unmarked while the doc link stays via=link" \ + || no "the code call was marked via=link, or the doc link lost the mark" +printf '%s' "$HUB" | grep -q 'docs/api.md:[0-9]*"[^>]*via="link"' \ + && ! printf '%s' "$HUB" | grep -q 'notes/api.md' \ + && ok "a leading-slash link joins the crawl root, not the other api.md" \ + || no "the leading-slash link missed docs/api.md or hit the decoy" +PTH="$( $BIN "$TMP/kinds" --no-cache --path=Hub,Later --legend=compact 2>/dev/null )" +printf '%s' "$PTH" | grep -q 'n="Later"[^>]*via="link"' \ + && ok "path marks the Later hop via=link" \ + || no "path lost via=link on the Later hop" +CON="$( $BIN "$TMP/kinds" --no-cache --connect=Hub,Later --legend=compact 2>/dev/null )" +printf '%s' "$CON" | grep -q 'via="link"' \ + && ok "connect marks the Hub to Later edge via=link" \ + || no "connect lost via=link on the Hub to Later edge" + +echo "--- warm == cold after editing ONLY the target file (the cached ref must stay unbound) ---" +cp -R "$AFIX" "$TMP/anchorcopy" +rm -f "$TMP/anchor.cache" +$BIN "$TMP/anchorcopy" --cache="$TMP/anchor.cache" --callees="Self Collision" --legend=compact >/dev/null 2>&1 +sed -i.bak 's/^## Install$/## Install Steps Renamed/' "$TMP/anchorcopy/docs/b/partner.md" +rm -f "$TMP/anchorcopy/docs/b/partner.md.bak" +grep -q '^## Install Steps Renamed$' "$TMP/anchorcopy/docs/b/partner.md" || no "fixture edit did not apply (sed)" +$BIN "$TMP/anchorcopy" --cache="$TMP/anchor.cache" --callees="Self Collision" --legend=compact >"$TMP/ac_warm" 2>/dev/null +$BIN "$TMP/anchorcopy" --no-cache --callees="Self Collision" --legend=compact >"$TMP/ac_cold" 2>/dev/null +if [ -s "$TMP/ac_cold" ] && diff -q "$TMP/ac_cold" "$TMP/ac_warm" >/dev/null; then ok "warm run equals cold run after only the target heading changed"; else no "warm run differs from cold after a target-only edit (a stale bound ref)"; fi +if grep -q 'n="partner"' "$TMP/ac_warm"; then ok "…and the dead anchor degraded to the partner file edge"; else no "the renamed-heading link lost its edge on the warm run"; fi + # ═══════════════════════════════════════════════════════════════════════════ echo echo "=== determinism ×3 and cache transparency on the md fixture ===" diff --git a/test/mdsectionfix/guide.md b/test/mdsectionfix/guide.md index 80a59b31b..0407514ad 100644 --- a/test/mdsectionfix/guide.md +++ b/test/mdsectionfix/guide.md @@ -13,7 +13,9 @@ The orientation body carries zqorientbody as its unique marker token. The warm path body carries zqcachewarmbody and mentions `codeIdentFn` in a backtick. It links to [the partner doc](partner.md) and jumps to -[result tables](#result-tables) further down. An archived copy +[result tables](#result-tables) further down. The install section is +[over there](partner.md#install-steps). A missing anchor stays on the file: +[nowhere](alt.markdown#no-such-heading). An archived copy [lives here][zqref]. ```cpp diff --git a/test/printf_parity.manifest b/test/printf_parity.manifest index 66cb651a3..455323b63 100644 --- a/test/printf_parity.manifest +++ b/test/printf_parity.manifest @@ -1,4 +1,4 @@ -flagless 0 22b42aea387b3ba54ac0921ce1caed39aea23a4947579eec2276b72ad77330e6 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 +flagless 0 b0447db620f0c57726cc5be7851d6fb5dad0b3510eaa0318297ee7aaeee80904 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 lint 0 a3d29cee7631f2904dfeaa947f091d3e73181d325e6ddedd87a68580d5717ff5 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 lint_sarif 0 805e375b06b7755bcc6423607378e557ee82fef787cf09c34ba5c066639f4d7b e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 lint_select 0 740876ae2df8dbdced07c406c223f545e92a719d8bc3dee1cac56a85207f16c2 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 @@ -17,7 +17,7 @@ uses 0 5442767b638c8cac9eb0327cbfd5fde73f92fde5b85e2a995670a3982d903711 e3b0c442 path 0 976a050ece8954e7e0b245df5d61b693bb10dec1f3867918ec519c72f83f4aee e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 connect 0 6ceb99d6c5ecb9f57a1b7426da2ed2118705cb7cc6b3d0aa4456b84c0ff715f4 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 grep 0 ea5c66f3062d3ba805f271d18b146ee411f3b132f75d0c77062ecf2eda13f36e e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 -pack_signatures 0 91446d0fc143c8d08605da1a3e7430834de105b15f0d2658f675c6bc3bf641a8 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 +pack_signatures 0 66de39992132b15fdc91e088e5301976e2beb972cb95802441331350983fab25 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 pack_task 0 f3de4fc3a28342cf515fedb8a02de0fea7f149ce8e948d39a7037c955a9eafd1 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 arch 2 45b12ab02e3e9f2e32bf92fe0e1cef7b489e70711386e0749d50281743aeeec5 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 seams 0 069a723391d0a5687bdbe23d250a5dc901791fbcc676e5ac7435e9017cc30478 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 diff --git a/test/qschemetrip.hash b/test/qschemetrip.hash index 706fd9d6e..2ccfe2fbb 100644 --- a/test/qschemetrip.hash +++ b/test/qschemetrip.hash @@ -1 +1 @@ -b3bb57d4aa8bbcda9087d57babd7cf157b907c1912264e6de8fcda6ea02a21a4 +a667ffc55eff871ff6ea06c1068cc5c73c8fe06b5a582beb87dae733548f8a05 diff --git a/test/qschemetripcheck.sh b/test/qschemetripcheck.sh index 4e0d341a9..1d38afd18 100755 --- a/test/qschemetripcheck.sh +++ b/test/qschemetripcheck.sh @@ -34,6 +34,11 @@ SRC="$ROOT/src/quality.h" ING="$ROOT/src/ingest_cache.h" # extraction-identity constants moved here (2026-08-29 ingest.cpp section split); the hashed CONCAT label keeps its historical spelling so the pin holds PIN="$ROOT/test/qschemetrip.hash" # RE-PIN LOG (the pin is a bare hash, so its justification has to live here). +# 2026-10-19, PR #414 (#409 markdown section links) merged with main 90f6fae6 (train 31): RE-PINNED with UPDATE_GOLDEN=1 (hash +# a667ffc55e…8a05, unchanged from the PR's own earlier pin: main moved its hash with 198, the watched text here is 206 again). The only watched text that moved is the kParserVer line and quality.h's mirror: renumbered again at +# merge to 206 (rich file tag 207), above main 198 / 199 (train 31), 196 / 197 and the live branch builds 202 and 204. +# kCacheVersion stays 36 and kQSnapCacheScheme stays 22: the PR keeps a markdown link's written path in an existing field, +# no record format moved. The train 31 entry below is kept as written; its hash is superseded by this one. # 2026-10-18, train 31 (body-fold, hrr, h1-annot, callers-band2, dangling-warnings, ix2-slice merged onto main 939dbb69): # RE-DERIVED ONCE with UPDATE_GOLDEN=1 on the merged tree (hash b3bb57d4aa…a02a21a4). Watched text: kParserVer -> 198 (main # 196 / rich 197, lane h1-annot 194 / 195; the other five lanes kept their base 184; 198 / rich file tag 199 is above every diff --git a/test/regression.sh b/test/regression.sh index f9c74865f..4b9c55b8a 100755 --- a/test/regression.sh +++ b/test/regression.sh @@ -327,7 +327,8 @@ done # 4) golden snapshot — output matches the committed golden (catches ANY unintended output change). # --no-cache so the golden is the canonical cold parse, independent of any TMPDIR cache state. -"$BIN" "$CORPUS" --no-cache >"$TMP/cur" 2>/dev/null +# L1: the CLI default legend is compact; golden.xml is the full legend, so this compare asks for it. +"$BIN" "$CORPUS" --no-cache --legend=full >"$TMP/cur" 2>/dev/null if [ "${UPDATE_GOLDEN:-0}" = "1" ]; then cp "$TMP/cur" "$GOLD"; printf ' WROTE golden (%s B)\n' "$(wc -c <"$GOLD" | tr -d ' ')" elif [ -f "$GOLD" ]; then