From f39bef2df26c703eedc3dd37e5ee98f89f7bcd1f Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I <210578314+joyful-ii-V-I@users.noreply.github.com> Date: Wed, 21 Oct 2026 22:44:51 -0400 Subject: [PATCH 01/77] =?UTF-8?q?pathgaps:=20K5=20rule=202=20by=20namesake?= =?UTF-8?q?=20shape=20=E2=80=94=20file-owned=20needs=20an=20import=20(2b),?= =?UTF-8?q?=20override=20dispatch=20admits=20(2c)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A by-name hedge (near_sink why=name, a bridge's namesake crossing) to a FILE-OWNED namesake (a free definition, or a JS/TS method: no class scope) is now admitted only when the caller's file is or imports the namesake's file (precise import adjacency; C/C++/ObjC transitive #include). Owner reach no longer admits one: the IND4 Stage-0 hand-check rejected both such shown sites (koa accessor, nunjucks this.init). A CLASS-OWNED namesake is also admitted by override dispatch: its class derives, by name and transitively (resolve.h derivesFromName over buildGraph's chaUp, now kept on Graph), from the bound definition's class, and its file imports that class's file (any depth; FE-A's Python directive files count for src/ layouts). The import condition keeps an external same-named parent (ext.Base, from ext import Base) out. One helper (NamesakeOwnerReach::admits) serves both hedges. Gates K6-K12 in test/pathgapcheck.sh. Co-Authored-By: Claude Opus 5.5 --- src/graph.h | 10 ++++ src/pathgaps.h | 128 +++++++++++++++++++++++++++++++++++++------ test/pathgapcheck.sh | 74 +++++++++++++++++++++++++ 3 files changed, 195 insertions(+), 17 deletions(-) diff --git a/src/graph.h b/src/graph.h index 09a19a6fe..e6b461d3a 100644 --- a/src/graph.h +++ b/src/graph.h @@ -146,6 +146,13 @@ struct Graph // class open), so their base stayed on the final-segment byName rule — resolve.h RubyBaseScopeDisclosure. // A whole-corpus gauge beside unindexedFiles, emitted as ruby_bases_unscoped= and ABSENT AT ZERO. std::size_t rubyBasesUnscoped = 0; + // lane ind4b: two resolver tables kept past buildGraph for the by-name hedge rules (pathgaps.h NamesakeOwnerReach, K5 + // rules 2b/2c) — the precise import adjacency (file → the files its imports resolve to, forCallNarrow) and the CHA-lite + // name graph (class name → its direct base names, sorted), plus FE-A's Python import-directive files. Moved in at the end of + // buildGraph; not cached. + std::vector> importAdj; + std::vector> pyImportFiles; // FE-A's Python directive files (Step-A, else the suffix match: src/ layouts) + ChaUpNames chaUp; std::size_t externalCalls = 0; // Phase 5 (docs/EVALS.md "Phase 5"): call sites the external-name // VETO refused — a bare name or receiver provably bound OUTSIDE the // indexed tree (a builtin/stdlib name with no in-repo evidence, an @@ -10080,6 +10087,9 @@ inline Graph buildGraph( const IngestResult& ing, const ScipOverlay* scip = null g.priorWeight[ s.id ] = priorwt::weight( s.name, defCount ); } + g.importAdj = std::move( includeAdj ); // last use: the by-name hedge rules read them from here (pathgaps.h, K5 2b/2c) + g.pyImportFiles = extVeto.importFiles; // a copy: extVeto is const and Python-only (empty rows elsewhere) + g.chaUp = std::move( chaUp ); return g; } diff --git a/src/pathgaps.h b/src/pathgaps.h index d978df44f..bc12e8647 100644 --- a/src/pathgaps.h +++ b/src/pathgaps.h @@ -412,7 +412,7 @@ struct NearSinkRow std::size_t callers = 0; // its proven callers (in-edges), any depth NearSinkCounts counts{}; NearSinkSite site; // the one site the row names (none when counts are all 0) - std::size_t nameWithheld = 0; // by-name hits the K5 rules withheld (owner out of reach, or no non-builtin call): never "external" + std::size_t nameWithheld = 0; // by-name hits the K5 rules withheld (rule 2b/2c refused, or no non-builtin call): never "external" bool reasoned() const noexcept { return site.valid || callers == 0; } // a why= (external included) }; @@ -440,10 +440,12 @@ inline bool builtinSpelledCall( const Reference& call ) noexcept // `handler`). Both rules below hold for both hedges: // (1) SITE: the hedge's site is C's first call of NAME that is not builtinSpelledCall (the declined pass's own filter); // a caller whose every call of NAME is builtin-spelled offers no by-name hedge. -// (2) OWNER IN REACH: the namesake's OWNER — the class that owns it in its own file (Symbol::scope, the extractor's), or -// its file for a free definition — owns some definition in the forward reach set: near_sink takes -// PathSearchGaps::cone (every symbol the forward search reached), the bridge every symbol its layered search has -// reached so far (layers 0..k before crossing k+1). A namesake nothing reached shares an owner with is no way in. +// (2) OWNER IN REACH: the namesake's OWNER — the class that owns it in its own file (Symbol::scope, the extractor's) — +// owns some definition in the forward reach set: near_sink takes PathSearchGaps::cone (every symbol the forward +// search reached), the bridge every symbol its layered search has reached so far (layers 0..k before crossing k+1). +// A namesake nothing reached shares an owner with is no way in. Lane ind4b: a FILE-OWNED namesake needs the caller's +// file to be or import its file instead (2b), and a class-owned one is also admitted by override dispatch (2c) — +// NamesakeOwnerReach::admits below. // Each caller's call references in reference order, for the by-name site (rule 1). class ByNameCallSites @@ -486,18 +488,48 @@ class ByNameCallSites std::vector> calls_; // (caller, reference index), sorted }; -// The owners (rule 2) of a reach set: add() each reached symbol, then ask reaches() of a namesake. Membership only (no +// The owners (rule 2) of a reach set: add() each reached symbol, then ask admits() of a by-name hit. Membership only (no // iteration), so the hash order never reaches an answer; the key is "#" (scope "" = the file itself). +// Lane ind4b sharpened rule 2 per namesake shape (one place for both hedges — near_sink and the bridge's crossing): +// (2b) a FILE-OWNED namesake (scope "": a free function, or a method of a language whose extractor records no class +// scope — JS/TS) is admitted only when the caller's file IS the namesake's file or imports it: a direct edge of the +// index's precise import adjacency (Graph::importAdj), or for C/C++/ObjC any transitive #include. Owner reach no +// longer admits one: a file the search touched is not evidence the caller can name its function (the koa accessor +// and nunjucks `this.init` sites the IND4 Stage-0 hand-check rejected). +// (2c) a CLASS-OWNED namesake is admitted when its class's owner is in reach (rule 2, unchanged) OR — override dispatch +// — its class derives, by name and transitively (resolve.h derivesFromName over Graph::chaUp, buildGraph's CHA +// name graph), from the class owning the definition the call was bound to, AND the namesake's file is that +// definition's file or reaches it over the import adjacency (any depth; a Python file's FE-A directive files count, +// so a src/ layout's `from pkg.driver import Driver` does). The import condition keeps a class whose +// parent is an EXTERNAL same-named class (`class Sub(ext.Base)`, `from ext import Base`) out: chaUp is name-keyed. +// A residual it cannot see: the namesake's file imports the bound definition's file for another reason while its +// parent is external and same-named — that one stays admitted (a hedge, never a proven edge). class NamesakeOwnerReach { public: + NamesakeOwnerReach( const IngestResult& ing, const Graph& g ) + : ing_( ing ), g_( g ) {} void add( const Symbol& s ) { owners_.try_emplace( keyOf( s ), char( 1 ) ); } - bool reaches( const Symbol& namesake ) const + // May caller c's call, bound by name alone to `bound`, run `namesake` instead? (rules 2b / 2c above) + bool admits( NodeId caller, NodeId bound, NodeId namesake ) const { - return owners_.contains( keyOf( namesake ) ); + const Symbol& ns = ing_.symbols[namesake]; + if( ns.scope.empty() ) + { + const std::uint32_t from = ing_.symbols[caller].fileId; + const bool cFam = ns.lang == Lang::C || ns.lang == Lang::Cpp || ns.lang == Lang::ObjC; + return from == ns.fileId || fileSees( from, ns.fileId, cFam ); + } + if( owners_.contains( keyOf( ns ) ) ) + { + return true; + } + const Symbol& bs = ing_.symbols[bound]; + return !bs.scope.empty() && derivesFromName( classNameOf( ns.scope ), classNameOf( bs.scope ), g_.chaUp ) + && ( ns.fileId == bs.fileId || fileSees( ns.fileId, bs.fileId, true ) ); } private: @@ -505,7 +537,67 @@ class NamesakeOwnerReach { return std::to_string( s.fileId ) + "#" + s.scope; } - HashMap owners_; + // chaUp is keyed by a class's final-segment name; a scope may be written qualified (`Outer::Inner`, `Outer.Inner`) + static std::string_view classNameOf( std::string_view scope ) noexcept + { + const std::size_t at = scope.find_last_of( ".:" ); + return at == std::string_view::npos ? scope : scope.substr( at + 1 ); + } + // does file `from` import file `to` — directly (the precise adjacency only), or (transitive) over any number of import + // edges, where a Python file's FE-A directive files (the suffix match a src/ layout needs) count too? Memoized per `from`. + bool fileSees( std::uint32_t from, std::uint32_t to, bool transitive ) const + { + const std::vector>& adj = g_.importAdj; + if( from >= adj.size() ) + { + return false; + } + if( !transitive ) + { + return std::ranges::find( adj[from], to ) != adj[from].end(); + } + if( reach_.empty() ) + { + reach_.resize( adj.size() ); + reachDone_.resize( adj.size(), 0 ); + } + if( reachDone_[from] == 0 ) + { + reachDone_[from] = 1; + std::vector seen( adj.size(), 0 ); + std::vector stack{ from }; + seen[from] = 1; + while( !stack.empty() ) + { + const std::uint32_t v = stack.back(); + stack.pop_back(); + const auto visit = [ & ]( std::span next ) + { + for( const std::uint32_t w : next ) + { + if( w < adj.size() && seen[w] == 0 ) + { + seen[w] = 1; + stack.push_back( w ); + reach_[from].push_back( w ); + } + } + }; + visit( adj[v] ); + if( v < g_.pyImportFiles.size() ) + { + visit( g_.pyImportFiles[v] ); // a Python src/ layout import Step-A cannot place (FE-A's suffix match) + } + } + std::ranges::sort( reach_[from] ); + } + return std::ranges::binary_search( reach_[from], to ); + } + const IngestResult& ing_; + const Graph& g_; + HashMap owners_; + mutable std::vector> reach_; // file → the files its imports reach (sorted), on first ask + mutable std::vector reachDone_; }; // Is site a preferred over site b for one row? met first, then kind (table order), then path, line, order. @@ -668,7 +760,7 @@ inline PathNearSink pathNearSink( const IngestResult& ing, const Graph& g, std:: if( !g.outNameOnly.empty() ) { EXPECTS( g.outNameOnly.size() == g.outTargets.size(), "buildGraph allocates the hedge bits one per flattened edge" ); - NamesakeOwnerReach ownerReach; // K5 rule 2: the owners the forward search reached + NamesakeOwnerReach ownerReach( ing, g ); // K5 rule 2 (2b/2c): the owners the forward search reached for( const NodeId v : forwardCone ) { if( v < S ) @@ -722,10 +814,10 @@ inline PathNearSink pathNearSink( const IngestResult& ing, const Graph& g, std:: const NodeId u = out.rows[ it->row ].id; const auto from = g.outTargets.begin() + g.outOff[c]; const auto to = g.outTargets.begin() + g.outOff[c + 1]; - // outTargets ascend within a source; the row's owner must be in the forward reach (K5 rule 2) + // outTargets ascend within a source; the namesake must pass K5 rule 2 (2b/2c) if( u != w && u != c && !std::binary_search( from, to, u ) ) { - ( ownerReach.reaches( ing.symbols[u] ) ? nameHits : withheld ).emplace_back( c, it->row ); + ( ownerReach.admits( c, w, u ) ? nameHits : withheld ).emplace_back( c, it->row ); } } } @@ -798,7 +890,8 @@ inline constexpr std::string_view kNearSinkLegend = "entered with no edge: declined = a declined call lists it as a candidate (not one spelled like a builtin type's method), value = " "it is stored or passed as a value (not by a wrapper decorator such as @property), through = a " "call through a parameter or slot is matched to that value, name = a by-name call went to a namesake and has no edge to it (a call " - "not spelled like a builtin type's method, and the row's class, or its file for a free function, owns something the forward search reached), " + "not spelled like a builtin type's method; a row with no class needs the caller's file to be or import its file, a class method needs its class " + "to own something the forward search reached or to derive from the bound definition's class, in a file that imports that one), " "closure = it is a named function written inside in='s body (site= its own definition: in= runs it or hands it on), " "external = none of those and no proven caller: no indexed way in was found (an entry point, an out-of-tree or framework caller, " "a hand-off the index does not record, or dead); withheld=name:N = N callers whose by-name calls reach this namesake " @@ -1004,7 +1097,7 @@ inline PathBridge pathBridge( const IngestResult& ing, const Graph& g, std::span } } const ByNameCallSites byNameSites( ing, {} ); // K5 rule 1: a namesake crossing needs a call of the name not builtin-spelled - NamesakeOwnerReach ownerReach; // K5 rule 2: the owners of every symbol layers 0..k reached + NamesakeOwnerReach ownerReach( ing, g ); // K5 rule 2 (2b/2c): the owners of every symbol layers 0..k reached std::vector frontier; for( const NodeId s : srcDefs ) { @@ -1110,7 +1203,7 @@ inline PathBridge pathBridge( const IngestResult& ing, const Graph& g, std::span const std::size_t fan = std::size_t( last - first ) - ( std::find( first, last, w ) != last ? 1u : 0u ); for( auto it = first; fan <= kBridgeFanOut && it != last; ++it ) { - if( *it != w && ownerReach.reaches( ing.symbols[*it] ) ) // K5 rule 2: its owner is in reach + if( *it != w && ownerReach.admits( u, w, *it ) ) // K5 rule 2 (2b/2c) { cross( u, *it, BridgeVia::Namesake, fan ); } @@ -1149,8 +1242,9 @@ inline constexpr std::string_view kPathBridgeLegend = "runs through no test, bench, demo, generated or minified symbol, nor a .d.ts/.pyi declaration twin whose package ships its implementation; its rows read like a path's, and via= on a row names the crossing INTO it: declined = " "one of cands= candidates of a declined call in the previous row, value = a function the previous row stores or passes as a value, " "closure = a function written in the previous row's body, namesake = another same-language, same-kind definition of a name the previous row " - "calls by name alone, not spelled like a builtin type's method, whose class (or file, for a free function) owns something the " - "chain's earlier layers reached (via=name rows are proven edges bound by name). Fewest crossings first. It is NOT a path: reachable= and " + "calls by name alone, not spelled like a builtin type's method, in a file the previous row's file is or imports when it has no class, " + "else whose class owns something the chain's earlier layers reached or derives from the bound definition's class in a file that imports " + "that one (via=name rows are proven edges bound by name). Fewest crossings first. It is NOT a path: reachable= and " "hops= on the root stay 0, no count includes it, and every crossing is a hedge. "; inline std::string_view pathBridgeLegend( bool on ) noexcept { diff --git a/test/pathgapcheck.sh b/test/pathgapcheck.sh index e5aec16bf..363dd859a 100755 --- a/test/pathgapcheck.sh +++ b/test/pathgapcheck.sh @@ -60,6 +60,15 @@ # spelled like a builtin type's method and (2) the namesake's owner (its class in its file, else its file) owning # something the forward search reached — K1 owner out of reach, K2 builtin-spelled (owner in reach), K3 both, # K4 near miss (owner in reach, not builtin: both hedges stay; S4/B4 run on the same fixture). +# (K6-K12) lane ind4b, K5 rule 2 by namesake shape (pathgaps.h NamesakeOwnerReach::admits; one helper for both hedges): +# 2b a FILE-OWNED namesake (a free definition, or a JS/TS method: no class scope) needs the caller's file to be or +# import its file — K6 the koa accessor shape and K7 the nunjucks `this.init` shape (owner in reach, no import) are +# withheld, K8 the koa near miss whose caller file requires the namesake's file keeps why=name. 2c override dispatch: +# a class-owned namesake whose class derives (by name, transitively) from the bound definition's class and whose +# file imports that class's file is a way in (K9 direct, K10 via an in-file middle class: near_sink AND bridge; K10b +# a Python src/ layout, whose import only FE-A's suffix match places); +# K11 a same-named method on an UNRELATED class (§(a) 11) and K12 a class whose parent is an EXTERNAL same-named +# class (`ext.Base`, `from ext import Base`: §(a) 2) stay withheld. # # Usage: test/pathgapcheck.sh [BIN] (BIN defaults to RIPWIRE_BIN, then build/ripwire) set -u @@ -434,6 +443,39 @@ for nm in process update; do for own in in out; do fi printf 'def target():\n return 2\n\nclass B:\n @staticmethod\n def make():\n return 0\n\n def %s(self):\n return target()\n' "$nm" > "$d/other/b.py" done; done +# K6-K8 fixtures (rule 2b, JS). kq_*: a getter/setter pair `method` in lib/req.js, lib/ctx.js's file scope calls .method('a') +# — bound by name alone to one accessor, the other is the namesake; from= kind lives in req.js, so its owner (the file) is in +# reach. kq_imp's ctx.js also requires ./req (the near miss). nj_noimp: ui/pop.js's `this.init(el)` is bound by name alone to +# ui/tip.js's prototype init; core/parser.js's class method init is the namesake; from= render lives in parser.js. +for v in noimp imp; do d="$TMP/kq_$v"; mkdir -p "$d/lib" + printf "function target() {\n return 2;\n}\nmodule.exports = {\n kind() {\n return 1;\n },\n get method() {\n return target();\n },\n set method(val) {\n this.m = val;\n }\n};\n" > "$d/lib/req.js" + req=""; [ "$v" = imp ] && req="const req = require('./req');\n" + printf "${req}function delegate(p) {\n return p;\n}\nconst proto = {};\ndelegate(proto)\n .method('a');\n" > "$d/lib/ctx.js" +done +mkdir -p "$TMP/nj_noimp/ui" "$TMP/nj_noimp/core" +printf 'function Tip(el) {\n this.el = el;\n}\nTip.prototype.init = function (el) {\n this.el = el;\n};\n' > "$TMP/nj_noimp/ui/tip.js" +printf 'function target() {\n return 2;\n}\nfunction render() {\n return 0;\n}\nclass Parser {\n init(tokens) {\n return target();\n }\n}\nmodule.exports = { Parser, render };\n' > "$TMP/nj_noimp/core/parser.js" +printf 'var Pop = function (el) {\n this.init(el);\n};\n' > "$TMP/nj_noimp/ui/pop.js" +# K10b fixture (rule 2c, Python src/ layout — textual's shape): `from tx.driver import Driver` names src/tx/driver.py, which +# the precise import adjacency's Step-A cannot place (no src/ root); FE-A's suffix match can. +mkdir -p "$TMP/c5_src/src/tx/drivers"; : > "$TMP/c5_src/src/tx/__init__.py"; : > "$TMP/c5_src/src/tx/drivers/__init__.py" +printf 'class Driver:\n def stop_mode(self):\n return 1\n' > "$TMP/c5_src/src/tx/driver.py" +printf 'from tx.driver import Driver\n\ndef target():\n return 2\n\nclass LinuxDriver(Driver):\n def stop_mode(self):\n return target()\n' > "$TMP/c5_src/src/tx/drivers/linux.py" +printf 'from tx.driver import Driver\ndef run(driver):\n return driver.stop_mode()\n' > "$TMP/c5_src/src/tx/app.py" +# K9-K12 fixtures (rule 2c, Python): run(obj) in pkg/x/c.py calls obj.render_line(), bound by name alone to the imported +# Base.render_line; other/b.py's Sub.render_line (calls target) is the namesake and nothing run reaches is owned by Sub. +for v in der der2 unrel ext extbare; do d="$TMP/c5_$v"; mkdir -p "$d/pkg/x" "$d/other" + printf 'class Base:\n def render_line(self):\n return 1\n' > "$d/pkg/x/a.py" + printf 'from pkg.x.a import Base\ndef run(obj):\n return obj.render_line()\n' > "$d/pkg/x/c.py" + case "$v" in + der) hdr='from pkg.x.a import Base\n'; cls='class Sub(Base):' ;; + der2) hdr='from pkg.x.a import Base\n'; cls='class Mid(Base):\n pass\n\nclass Sub(Mid):' ;; + unrel) hdr=''; cls='class Sub:' ;; + ext) hdr='import ext\n'; cls='class Sub(ext.Base):' ;; + extbare) hdr='from ext import Base\n'; cls='class Sub(Base):' ;; + esac + printf "${hdr}\ndef target():\n return 2\n\n${cls}\n def render_line(self):\n return target()\n" > "$d/other/b.py" +done nsblock(){ grep -o ']*>.*' "$1"; } nshead(){ grep -o ']*>' "$1"; } nsrows(){ grep -o ']*/>' "$1"; } @@ -841,6 +883,38 @@ else no "(K5x) premise: K1's answer is missing" fi +# K6-K12 (lane ind4b): K5 rule 2 by namesake shape. Exact rows: a withheld="name:1" row proves the by-name hit exists (the +# premise), so a withheld arm cannot pass because the resolver stopped making the hedged edge. +nsArm "(K6) 2b koa accessor: ctx.js never imports req.js, so the by-name .method() is withheld though req.js is in reach" "$TMP/kq_noimp" "kind,target" \ + '' \ + '' +nsArm "(K7) 2b nunjucks this.init: pop.js never imports parser.js, so Parser.init is withheld though parser.js is in reach" "$TMP/nj_noimp" "render,target" \ + '' \ + '' +nsArm "(K8) 2b near miss: ctx.js requires ./req, so the by-name .method() stays a way in" "$TMP/kq_imp" "kind,target" \ + '' \ + '' +nsArm "(K9) 2c override dispatch: Sub(Base) overrides render_line, so run's obj.render_line() may run it" "$TMP/c5_der" "run,target" \ + '' \ + '' +brArm "(K9b) 2c: the bridge crosses to the override the same way" "$TMP/c5_der" "run,target" \ + '' +nsArm "(K10) 2c transitive: Sub(Mid), Mid(Base) — the override two classes down is a way in" "$TMP/c5_der2" "run,target" \ + '' \ + '' +nsArm "(K10b) 2c src/ layout: LinuxDriver(Driver) imports src/tx/driver.py by its package name — the override is a way in" "$TMP/c5_src" "run,target" \ + '' \ + '' +nsArm "(K11) §(a) 11: render_line on an UNRELATED class Sub (no base) stays withheld" "$TMP/c5_unrel" "run,target" \ + '' \ + '' +for fx in ext extbare; do + nsArm "(K12) §(a) 2 external parent ($fx): Sub's base is an external same-named Base, so its render_line stays withheld" "$TMP/c5_$fx" "run,target" \ + '' \ + '' + brArm "(K12b) §(a) 2 external parent ($fx): no namesake bridge to it either" "$TMP/c5_$fx" "run,target" '' +done + echo "(M) MCP path_between carries the same clause" # Two MCP legend postures (lean-answers): the unchanged assertions run on --mcp-legend=inline, the pre-posture spelling # where every answer carries its root facts on ; a TWIN runs the default session posture, whose answers after the From f9a082dfaaa8f1a7ab70f294e4e6ca6af57f5bd2 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I <210578314+joyful-ii-V-I@users.noreply.github.com> Date: Wed, 21 Oct 2026 22:55:28 -0400 Subject: [PATCH 02/77] CHANGELOG: by-name hedge rules 2b/2c (lane ind4b) Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index fd2023951..8eaaca5a5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,24 @@ not published here — see `docs/EVALS.md` for the instruments behind the headli ## [Unreleased] +### Changed — by-name hedges: a free namesake needs an import, an override is a way in + +- A by-name hedge (`` `why="name"`, a `` `via="namesake"` crossing) to a namesake with no class (a free + function, or a JavaScript/TypeScript method, whose class the extractor does not record) now needs the caller's file to be, + or to import, the namesake's file: a direct import edge, or any transitive `#include` in C, C++ and Objective-C. A namesake + file the search merely touched no longer counts. On the 880-pair seed-18 `--path` draw over 11 in-sample repos this + withholds the 2 shown by-name sites a hand-check had rejected (koa's `delegate( proto, 'response' ).method( … )` reaching + `request.js`'s `get method`, and a bundled Bootstrap `this.init( … )` reaching nunjucks' `Parser.init`). The one accepted + site, flake8's `report()`, stays. 16 rows that already showed a value or declined site keep it and now also count a by-name way in. +- A class method namesake is now also a way in when its class derives from the class of the definition the call was bound + to (by name, any depth), and its file imports that class's file (any depth; for Python, an import that resolves by package + name in a `src/` layout counts). This is override dispatch. A parent that is an external class of the same name + (`class Sub(ext.Base)`, `from ext import Base`) does not count. On the same draw this shows 6 sites, and a hand-check read + all 6 as correct: textual's `driver.start_application_mode()` / `stop_application_mode()` → `LinuxDriver`, + `widget.render_line( y )` → `TextArea`, and `node.compose()` → three `App` subclasses in examples. Like every by-name hedge, + each is a hedge and never a proven edge. The remaining blind spot: a file that imports the parent's module for some other + reason, while the parent it actually names is an external class of the same name. + ### Added — `--for` names the members of its shown owner rows on the compact route (`mem=`) On a conceptual `--for` answer (root `bundle="compact"`), a shown class, struct or interface/trait row now carries From 5fa806b344008d39310037d2d45435e4ae4720ee Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I <210578314+joyful-ii-V-I@users.noreply.github.com> Date: Wed, 21 Oct 2026 23:22:49 -0400 Subject: [PATCH 03/77] pathgaps: rule 2b/2c in a new NamesakeRule; NamesakeOwnerReach back to its base shape quality-delta gated the grown NamesakeOwnerReach (constructor contract change, class verbosity) and a re-written last-segment helper; NamesakeRule composes the unchanged owner set and reuses namesplit::afterLast. Co-Authored-By: Claude Opus 5.5 --- src/pathgaps.h | 55 +++++++++++++++++++++++++++++++------------------- 1 file changed, 34 insertions(+), 21 deletions(-) diff --git a/src/pathgaps.h b/src/pathgaps.h index bc12e8647..3552d68f0 100644 --- a/src/pathgaps.h +++ b/src/pathgaps.h @@ -40,6 +40,7 @@ #include "closures.h" // ClosureIndex — a near-sink row written inside another function's body (why=closure) #include "infra/sortutil.h" // svLess: the explicit byte order every string_view sort here takes (portablebuildcheck #6) #include "infra/enumcount.h" // enumCountIsExact — the kNearSinkKindCount / kBridgeViaCount sizes follow their enums +#include "infra/namesplit.h" // afterLast — a scope's final segment, the CHA name graph's key (K5 rule 2c) #include #include @@ -445,7 +446,7 @@ inline bool builtinSpelledCall( const Reference& call ) noexcept // search reached), the bridge every symbol its layered search has reached so far (layers 0..k before crossing k+1). // A namesake nothing reached shares an owner with is no way in. Lane ind4b: a FILE-OWNED namesake needs the caller's // file to be or import its file instead (2b), and a class-owned one is also admitted by override dispatch (2c) — -// NamesakeOwnerReach::admits below. +// NamesakeRule::admits below. // Each caller's call references in reference order, for the by-name site (rule 1). class ByNameCallSites @@ -488,8 +489,28 @@ class ByNameCallSites std::vector> calls_; // (caller, reference index), sorted }; -// The owners (rule 2) of a reach set: add() each reached symbol, then ask admits() of a by-name hit. Membership only (no +// The owners (rule 2) of a reach set: add() each reached symbol, then ask reaches() of a namesake. Membership only (no // iteration), so the hash order never reaches an answer; the key is "#" (scope "" = the file itself). +class NamesakeOwnerReach +{ +public: + void add( const Symbol& s ) + { + owners_.try_emplace( keyOf( s ), char( 1 ) ); + } + bool reaches( const Symbol& namesake ) const + { + return owners_.contains( keyOf( namesake ) ); + } + +private: + static std::string keyOf( const Symbol& s ) + { + return std::to_string( s.fileId ) + "#" + s.scope; + } + HashMap owners_; +}; + // Lane ind4b sharpened rule 2 per namesake shape (one place for both hedges — near_sink and the bridge's crossing): // (2b) a FILE-OWNED namesake (scope "": a free function, or a method of a language whose extractor records no class // scope — JS/TS) is admitted only when the caller's file IS the namesake's file or imports it: a direct edge of the @@ -504,14 +525,14 @@ class ByNameCallSites // parent is an EXTERNAL same-named class (`class Sub(ext.Base)`, `from ext import Base`) out: chaUp is name-keyed. // A residual it cannot see: the namesake's file imports the bound definition's file for another reason while its // parent is external and same-named — that one stays admitted (a hedge, never a proven edge). -class NamesakeOwnerReach +class NamesakeRule { public: - NamesakeOwnerReach( const IngestResult& ing, const Graph& g ) + NamesakeRule( const IngestResult& ing, const Graph& g ) : ing_( ing ), g_( g ) {} void add( const Symbol& s ) { - owners_.try_emplace( keyOf( s ), char( 1 ) ); + owners_.add( s ); } // May caller c's call, bound by name alone to `bound`, run `namesake` instead? (rules 2b / 2c above) bool admits( NodeId caller, NodeId bound, NodeId namesake ) const @@ -523,26 +544,18 @@ class NamesakeOwnerReach const bool cFam = ns.lang == Lang::C || ns.lang == Lang::Cpp || ns.lang == Lang::ObjC; return from == ns.fileId || fileSees( from, ns.fileId, cFam ); } - if( owners_.contains( keyOf( ns ) ) ) + if( owners_.reaches( ns ) ) { return true; } - const Symbol& bs = ing_.symbols[bound]; - return !bs.scope.empty() && derivesFromName( classNameOf( ns.scope ), classNameOf( bs.scope ), g_.chaUp ) + // chaUp is keyed by a class's final-segment name; a scope may be written qualified (`Outer::Inner`, `Outer.Inner`) + const auto leaf = []( std::string_view scope ) { return namesplit::afterLast( namesplit::afterLast( scope, "::" ), "." ); }; + const Symbol& bs = ing_.symbols[bound]; + return !bs.scope.empty() && derivesFromName( leaf( ns.scope ), leaf( bs.scope ), g_.chaUp ) && ( ns.fileId == bs.fileId || fileSees( ns.fileId, bs.fileId, true ) ); } private: - static std::string keyOf( const Symbol& s ) - { - return std::to_string( s.fileId ) + "#" + s.scope; - } - // chaUp is keyed by a class's final-segment name; a scope may be written qualified (`Outer::Inner`, `Outer.Inner`) - static std::string_view classNameOf( std::string_view scope ) noexcept - { - const std::size_t at = scope.find_last_of( ".:" ); - return at == std::string_view::npos ? scope : scope.substr( at + 1 ); - } // does file `from` import file `to` — directly (the precise adjacency only), or (transitive) over any number of import // edges, where a Python file's FE-A directive files (the suffix match a src/ layout needs) count too? Memoized per `from`. bool fileSees( std::uint32_t from, std::uint32_t to, bool transitive ) const @@ -595,7 +608,7 @@ class NamesakeOwnerReach } const IngestResult& ing_; const Graph& g_; - HashMap owners_; + NamesakeOwnerReach owners_; mutable std::vector> reach_; // file → the files its imports reach (sorted), on first ask mutable std::vector reachDone_; }; @@ -760,7 +773,7 @@ inline PathNearSink pathNearSink( const IngestResult& ing, const Graph& g, std:: if( !g.outNameOnly.empty() ) { EXPECTS( g.outNameOnly.size() == g.outTargets.size(), "buildGraph allocates the hedge bits one per flattened edge" ); - NamesakeOwnerReach ownerReach( ing, g ); // K5 rule 2 (2b/2c): the owners the forward search reached + NamesakeRule ownerReach( ing, g ); // K5 rule 2 (2b/2c): the owners the forward search reached for( const NodeId v : forwardCone ) { if( v < S ) @@ -1097,7 +1110,7 @@ inline PathBridge pathBridge( const IngestResult& ing, const Graph& g, std::span } } const ByNameCallSites byNameSites( ing, {} ); // K5 rule 1: a namesake crossing needs a call of the name not builtin-spelled - NamesakeOwnerReach ownerReach( ing, g ); // K5 rule 2 (2b/2c): the owners of every symbol layers 0..k reached + NamesakeRule ownerReach( ing, g ); // K5 rule 2 (2b/2c): the owners of every symbol layers 0..k reached std::vector frontier; for( const NodeId s : srcDefs ) { From eae3b235eade8e84c8ede9342004089507008847 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I <210578314+joyful-ii-V-I@users.noreply.github.com> Date: Thu, 22 Oct 2026 01:04:58 -0400 Subject: [PATCH 04/77] langfacts: transitiveIncludes column for K5 rule 2b (langidentitycheck ratchet); CHANGELOG C-family note Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 5 +++-- src/langfacts.h | 11 ++++++++--- src/pathgaps.h | 4 ++-- 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8eaaca5a5..b82c68f8f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,8 +19,9 @@ not published here — see `docs/EVALS.md` for the instruments behind the headli - A by-name hedge (`` `why="name"`, a `` `via="namesake"` crossing) to a namesake with no class (a free function, or a JavaScript/TypeScript method, whose class the extractor does not record) now needs the caller's file to be, - or to import, the namesake's file: a direct import edge, or any transitive `#include` in C, C++ and Objective-C. A namesake - file the search merely touched no longer counts. On the 880-pair seed-18 `--path` draw over 11 in-sample repos this + or to import, the namesake's file (a direct import edge). A namesake file the search merely touched no longer counts. For + C, C++ and Objective-C the rule follows the transitive `#include` closure, but no probe of ours produced a by-name hedge + to a C-family free function: those calls are declined, not bound by name. On the 880-pair seed-18 `--path` draw over 11 in-sample repos this withholds the 2 shown by-name sites a hand-check had rejected (koa's `delegate( proto, 'response' ).method( … )` reaching `request.js`'s `get method`, and a bundled Bootstrap `this.init( … )` reaching nunjucks' `Parser.init`). The one accepted site, flake8's `report()`, stays. 16 rows that already showed a value or declined site keep it and now also count a by-name way in. diff --git a/src/langfacts.h b/src/langfacts.h index d3961171c..2d74dd107 100644 --- a/src/langfacts.h +++ b/src/langfacts.h @@ -85,17 +85,22 @@ struct LangFacts /// decides only the closure relation, no symbol or edge). A lambda or a block is anonymous, and a C++ local class's /// methods sit inside the class's scope, not directly in the function, so neither is a closure either way. bool noNestedNamedFunctions = false; + + /// C, C++, Objective-C: a file sees every file its #includes reach, transitively (a header's own #include is in force in + /// the includer), so the by-name hedge's import rule (pathgaps.h NamesakeRule, K5 rule 2b) follows the include closure; + /// every other language's import names one module, so that rule reads the direct import edge only. + bool transitiveIncludes = false; }; /// The rows, one per Lang in enum order. A language says only the facts it has. inline constexpr std::array kLangFacts = { { - { .lang = Lang::Cpp, .defectShapeNeedsMacroSet = true, .noNestedNamedFunctions = true }, + { .lang = Lang::Cpp, .defectShapeNeedsMacroSet = true, .noNestedNamedFunctions = true, .transitiveIncludes = true }, { .lang = Lang::Python }, { .lang = Lang::TypeScript, .overloadSignatures = true }, { .lang = Lang::Go }, { .lang = Lang::Rust }, { .lang = Lang::Swift }, - { .lang = Lang::ObjC, .noNestedNamedFunctions = true }, + { .lang = Lang::ObjC, .noNestedNamedFunctions = true, .transitiveIncludes = true }, { .lang = Lang::Markdown, .linkSections = true }, { .lang = Lang::JavaScript }, { .lang = Lang::Bash }, @@ -104,7 +109,7 @@ inline constexpr std::array kLangFacts = { { { .lang = Lang::Unknown }, { .lang = Lang::Json }, { .lang = Lang::CSharp }, - { .lang = Lang::C, .noNestedNamedFunctions = true }, + { .lang = Lang::C, .noNestedNamedFunctions = true, .transitiveIncludes = true }, { .lang = Lang::Toml }, { .lang = Lang::Yaml }, { .lang = Lang::Php }, diff --git a/src/pathgaps.h b/src/pathgaps.h index 3552d68f0..bbf6d31f2 100644 --- a/src/pathgaps.h +++ b/src/pathgaps.h @@ -40,6 +40,7 @@ #include "closures.h" // ClosureIndex — a near-sink row written inside another function's body (why=closure) #include "infra/sortutil.h" // svLess: the explicit byte order every string_view sort here takes (portablebuildcheck #6) #include "infra/enumcount.h" // enumCountIsExact — the kNearSinkKindCount / kBridgeViaCount sizes follow their enums +#include "langfacts.h" // langFacts(l).transitiveIncludes — the import rule's C-family closure (K5 rule 2b) #include "infra/namesplit.h" // afterLast — a scope's final segment, the CHA name graph's key (K5 rule 2c) #include @@ -541,8 +542,7 @@ class NamesakeRule if( ns.scope.empty() ) { const std::uint32_t from = ing_.symbols[caller].fileId; - const bool cFam = ns.lang == Lang::C || ns.lang == Lang::Cpp || ns.lang == Lang::ObjC; - return from == ns.fileId || fileSees( from, ns.fileId, cFam ); + return from == ns.fileId || fileSees( from, ns.fileId, langFacts( ns.lang ).transitiveIncludes ); } if( owners_.reaches( ns ) ) { From 9ac45071a8e73bcf92aeb24627afd9a14d36c6e2 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I <210578314+joyful-ii-V-I@users.noreply.github.com> Date: Thu, 22 Oct 2026 02:29:02 -0400 Subject: [PATCH 05/77] --lego: a base the file imports from outside the tree is no implementor of an in-tree namesake `implementors` was keyed by the base NAME: `class Sub(ext.Base)`, `from ext import Base; class Sub(Base)`, `import { Base } from 'lib'`, `use ext::Base; impl Base for S` and `import org.ext.Base;` were all listed as implementors of an unrelated in-tree `Base`. buildGraph now asks baseBoundOutsideTree first: when the file's own import evidence binds the written base (bare name, or its qualifier root) outside the tree, the clause draws no edge. The evidence is the tables the resolver already builds (the Phase-5 import verdict, the JS import tables and qualifier verdicts, FE-A rustOutsideUse, the JVM declared-package table); which one a language reads is a LangFacts column (baseImportEvidence). An undecided binding (a star or wildcard import, an unplaceable import, a same-file declaration) keeps its by-name edge. Not decided (edge kept): C++ `ext::Base` / `using ext::Base`, C# `using`, Swift `import`, PHP `use`, a JS CommonJS `require` destructure, a Rust `impl ext::Base for S` with no `use`. Gate: test/legotransitivecheck.sh (XP), corpus test/legotransitivefix/xp; red on 27eddee1 (11 arms). Co-Authored-By: Claude Opus 5.5 --- src/graph.h | 91 ++++++++++++++++++- src/langfacts.h | 27 ++++-- test/legotransitivecheck.sh | 36 ++++++++ .../xp/java/com/xp/a/XpJBase.java | 2 + .../xp/java/com/xp/a/XpJSamePkg.java | 2 + .../xp/java/com/xp/b/XpJOutImport.java | 3 + .../xp/java/com/xp/b/XpJOutQual.java | 2 + .../xp/java/com/xp/b/XpJOutWild.java | 3 + .../xp/java/com/xp/c/XpJInImport.java | 3 + .../xp/java/com/xp/c/XpJInQual.java | 2 + test/legotransitivefix/xp/js/base.js | 1 + test/legotransitivefix/xp/js/inside.js | 2 + test/legotransitivefix/xp/js/outside.js | 2 + .../xp/kt/com/xp/k/XpKBase.kt | 2 + .../legotransitivefix/xp/kt/com/xp/m/XpKIn.kt | 3 + .../xp/kt/com/xp/m/XpKOut.kt | 3 + test/legotransitivefix/xp/py/inside/bare.py | 3 + .../xp/py/inside/qualified.py | 3 + .../xp/py/inside/reexport.py | 3 + .../xp/py/inside/relative.py | 3 + test/legotransitivefix/xp/py/outside/bare.py | 3 + .../legotransitivefix/xp/py/outside/dotted.py | 3 + .../xp/py/outside/qualified.py | 3 + .../legotransitivefix/xp/py/outside/shadow.py | 5 + test/legotransitivefix/xp/py/outside/star.py | 3 + test/legotransitivefix/xp/py/xpkg/__init__.py | 1 + test/legotransitivefix/xp/py/xpkg/core.py | 3 + test/legotransitivefix/xp/rs/src/base.rs | 1 + test/legotransitivefix/xp/rs/src/inside.rs | 3 + test/legotransitivefix/xp/rs/src/lib.rs | 3 + test/legotransitivefix/xp/rs/src/outside.rs | 3 + test/legotransitivefix/xp/ts/base.ts | 1 + test/legotransitivefix/xp/ts/inside.ts | 2 + test/legotransitivefix/xp/ts/insidens.ts | 2 + test/legotransitivefix/xp/ts/outside.ts | 2 + test/legotransitivefix/xp/ts/outsidens.ts | 2 + 36 files changed, 227 insertions(+), 9 deletions(-) create mode 100644 test/legotransitivefix/xp/java/com/xp/a/XpJBase.java create mode 100644 test/legotransitivefix/xp/java/com/xp/a/XpJSamePkg.java create mode 100644 test/legotransitivefix/xp/java/com/xp/b/XpJOutImport.java create mode 100644 test/legotransitivefix/xp/java/com/xp/b/XpJOutQual.java create mode 100644 test/legotransitivefix/xp/java/com/xp/b/XpJOutWild.java create mode 100644 test/legotransitivefix/xp/java/com/xp/c/XpJInImport.java create mode 100644 test/legotransitivefix/xp/java/com/xp/c/XpJInQual.java create mode 100644 test/legotransitivefix/xp/js/base.js create mode 100644 test/legotransitivefix/xp/js/inside.js create mode 100644 test/legotransitivefix/xp/js/outside.js create mode 100644 test/legotransitivefix/xp/kt/com/xp/k/XpKBase.kt create mode 100644 test/legotransitivefix/xp/kt/com/xp/m/XpKIn.kt create mode 100644 test/legotransitivefix/xp/kt/com/xp/m/XpKOut.kt create mode 100644 test/legotransitivefix/xp/py/inside/bare.py create mode 100644 test/legotransitivefix/xp/py/inside/qualified.py create mode 100644 test/legotransitivefix/xp/py/inside/reexport.py create mode 100644 test/legotransitivefix/xp/py/inside/relative.py create mode 100644 test/legotransitivefix/xp/py/outside/bare.py create mode 100644 test/legotransitivefix/xp/py/outside/dotted.py create mode 100644 test/legotransitivefix/xp/py/outside/qualified.py create mode 100644 test/legotransitivefix/xp/py/outside/shadow.py create mode 100644 test/legotransitivefix/xp/py/outside/star.py create mode 100644 test/legotransitivefix/xp/py/xpkg/__init__.py create mode 100644 test/legotransitivefix/xp/py/xpkg/core.py create mode 100644 test/legotransitivefix/xp/rs/src/base.rs create mode 100644 test/legotransitivefix/xp/rs/src/inside.rs create mode 100644 test/legotransitivefix/xp/rs/src/lib.rs create mode 100644 test/legotransitivefix/xp/rs/src/outside.rs create mode 100644 test/legotransitivefix/xp/ts/base.ts create mode 100644 test/legotransitivefix/xp/ts/inside.ts create mode 100644 test/legotransitivefix/xp/ts/insidens.ts create mode 100644 test/legotransitivefix/xp/ts/outside.ts create mode 100644 test/legotransitivefix/xp/ts/outsidens.ts diff --git a/src/graph.h b/src/graph.h index 7e1e571bd..59992f622 100644 --- a/src/graph.h +++ b/src/graph.h @@ -6207,6 +6207,7 @@ struct BaseEvidenceTables const std::vector>& includeAdj; // every language: the files a file's imports resolve to const std::vector& fileDir; // file → interned directory id const ReceiverEvidence* recvEv; // JS/TS: a CommonJS require alias's module (moduleNames) + const FalseEdgeRules* falseEdges; // Rust: a `use` of a path outside the crate (baseBoundOutsideTree) }; // is `path` the module `name` inside the package directory `pkgDir` (`pkg/name.py` or `pkg/name/__init__.py`)? @@ -6442,6 +6443,90 @@ inline void provenBasesOf( const BaseEvidenceTables& t, const Reference& r, cons } } +// Java / Kotlin: is `pkg` (`org.ext`, from `import org.ext.Base` or `extends org.ext.Base`) a package from outside the tree — +// the tree declares packages and not this one, and its last segment is no in-tree class (`import a.b.Outer.Base` names a +// nested class of an in-tree Outer: not decided here)? A tree that declares no package decides nothing. +inline bool jvmPackageOutsideTree( const ReceiverEvidence& ev, std::string_view pkg ) +{ + if( pkg.empty() || ev.types.jvmPackages.empty() || ev.types.packageFiles.contains( std::string( pkg ) ) ) + { + return false; + } + const std::size_t dot = pkg.rfind( '.' ); + return !ev.classNames.contains( std::string( dot == std::string_view::npos ? pkg : pkg.substr( dot + 1 ) ) ); +} + +// Lane fc2-lego-extparent: does the file's OWN import evidence bind the base clause `r` writes to a type from OUTSIDE the +// tree, so that no in-tree class of that name is its base (`class Sub(ext.Base)`, `from ext import Base; class Sub(Base)`, +// `import { Base } from 'lib'`, `use ext::Base; impl Base for S`, `import org.ext.Base;`)? Then the clause draws no +// implementors edge at all. Only a binding the language's lookup cannot get past answers true; anything undecided — a star +// or glob import, an import the tree cannot place, two bindings that disagree (Python), a same-file declaration the +// clause's scopes hold (tier 1 of provenBasesOf) — answers false and the by-name edge stays as before. +// What it does NOT decide: a C++ `ext::Base` or using-declaration, a C# `using`, a Swift `import`, a PHP `use`, a Rust +// `impl ext::Base for S` (the qualifier is not recorded on a Rust inherit reference) — those clauses keep their edge. +inline bool baseBoundOutsideTree( const BaseEvidenceTables& t, const Reference& r ) +{ + if( r.baseBind != 0 ) + { + return false; // a same-file declaration answers (or a scope too large to read): the local tier decides + } + const std::string_view qual = r.memberRoot; + const std::string_view root = qual.empty() ? std::string_view( r.calleeName ) : qual.substr( 0, qual.find( '.' ) ); + std::string key; + switch( langFacts( r.lang ).baseImportEvidence ) + { + case BaseImportEvidence::PythonImport: + { + const auto it = t.extVeto.importBind.find( fileNameKey( key, r.fileId, root ) ); + return it != t.extVeto.importBind.end() && it->second == 'x'; // every import binding of the name is outside + } + case BaseImportEvidence::JsImport: + { + if( qual.empty() ) + { + const auto pin = t.jsImports.targets.find( jsImportKey( r.fileId, root ) ); + return pin != t.jsImports.targets.end() && pin->second.outcome == JsImportOutcome::External; + } + const HashMap* verdicts = t.recvEv != nullptr ? t.recvEv->qualifierVerdicts : nullptr; + if( verdicts == nullptr ) + { + return false; + } + const auto it = verdicts->find( fileNameKey( key, r.fileId, root ) ); + return it != verdicts->end() && it->second == 'x'; // the namespace import's module is outside (buildQualifierVerdicts) + } + case BaseImportEvidence::RustUse: + return qual.empty() && t.falseEdges != nullptr && t.falseEdges->lookup( r.fileId, r.calleeName, t.falseEdges->rustOutsideUse ); + case BaseImportEvidence::JvmImport: + { + if( t.recvEv == nullptr ) + { + return false; + } + if( !qual.empty() ) + { + return jvmPackageOutsideTree( *t.recvEv, qual ); + } + bool outside = false; + bool inside = false; + t.recvEv->types.forEachImport( r.fileId, r.calleeName, [ & ]( const Binding& b ) + { + if( b.kind != LocalBindKind::TypeImport || b.var == "*" ) + { + return; + } + const std::size_t dot = b.importedName.rfind( '.' ); + const bool out = dot != std::string::npos && jvmPackageOutsideTree( *t.recvEv, std::string_view( b.importedName ).substr( 0, dot ) ); + ( out ? outside : inside ) = true; + } ); + return outside && !inside; // a single-type import of an outside package names the clause's type + } + case BaseImportEvidence::None: + return false; + } + return false; +} + // one implementors edge as drawn, with how its base was bound (merged into Graph::implementorBound after the dedup) struct ImplementorBoundEdge { @@ -9795,7 +9880,7 @@ inline Graph buildGraph( const IngestResult& ing, const ScipOverlay* scip = null std::vector baseCand; // one inheritance reference's base candidates, reused across references std::vector baseProven; // … and the one of them the language's lookup proves (provenBasesOf), reused std::vector boundEdges; // every edge drawn below with how it was bound; merged after the dedup - const BaseEvidenceTables baseEvidence{ ing, extVeto, jsImports, fileIncludes, includeAdj, fileDir, &recvEv }; + const BaseEvidenceTables baseEvidence{ ing, extVeto, jsImports, fileIncludes, includeAdj, fileDir, &recvEv, &falseEdges }; { PROFILE_SCOPE_DESCRIBE( "buildGraph/5: inheritance edges" ); for( const Reference& r : ing.references ) @@ -9828,9 +9913,9 @@ inline Graph buildGraph( const IngestResult& ing, const ScipOverlay* scip = null { derived = cand; break; } // ids are ascending → lowest-id match (deterministic) } } - if( derived == kNoNode ) + if( derived == kNoNode || baseBoundOutsideTree( baseEvidence, r ) ) { - continue; + continue; // fc2-lego-extparent: the file's own import binds the written base outside the tree — no in-tree base } if( ing.symbols[ derived ].lang == Lang::Ruby ) { diff --git a/src/langfacts.h b/src/langfacts.h index d3961171c..01da0f6d0 100644 --- a/src/langfacts.h +++ b/src/langfacts.h @@ -41,6 +41,18 @@ struct Must constexpr operator T() const noexcept { return value; } }; +/// Lane fc2-lego-extparent: which of the file's own import facts can show that an extends / implements clause names a type +/// from OUTSIDE the tree (graph.h baseBoundOutsideTree), so no in-tree namesake is listed as its base. None: the language's +/// imports cannot rule one out here (a C# `using`, a Swift `import`, a C++ include: the clause keeps its by-name edge). +enum class BaseImportEvidence : std::uint8_t +{ + None, // no import fact read + PythonImport, // `from ext import Base` / `import ext` + `ext.Base`: the Phase-5 import verdict ('x' = every binding outside) + JsImport, // `import { Base } from 'lib'` (the import tables' External outcome) / `lib.Base` (the qualifier verdict) + RustUse, // `use ext::Base;` whose first segment is not crate/self/super/an in-tree module (FE-A rustOutsideUse) + JvmImport, // `import org.ext.Base` / `extends org.ext.Base` of a package the tree does not declare +}; + /// What shared code asks about a language. Every optional column is false for a language that says nothing. struct LangFacts { @@ -85,21 +97,24 @@ struct LangFacts /// decides only the closure relation, no symbol or edge). A lambda or a block is anonymous, and a C++ local class's /// methods sit inside the class's scope, not directly in the function, so neither is a closure either way. bool noNestedNamedFunctions = false; + + /// The import facts that can prove a base clause names an outside type (BaseImportEvidence; graph.h baseBoundOutsideTree). + BaseImportEvidence baseImportEvidence = BaseImportEvidence::None; }; /// The rows, one per Lang in enum order. A language says only the facts it has. inline constexpr std::array kLangFacts = { { { .lang = Lang::Cpp, .defectShapeNeedsMacroSet = true, .noNestedNamedFunctions = true }, - { .lang = Lang::Python }, - { .lang = Lang::TypeScript, .overloadSignatures = true }, + { .lang = Lang::Python, .baseImportEvidence = BaseImportEvidence::PythonImport }, + { .lang = Lang::TypeScript, .overloadSignatures = true, .baseImportEvidence = BaseImportEvidence::JsImport }, { .lang = Lang::Go }, - { .lang = Lang::Rust }, + { .lang = Lang::Rust, .baseImportEvidence = BaseImportEvidence::RustUse }, { .lang = Lang::Swift }, { .lang = Lang::ObjC, .noNestedNamedFunctions = true }, { .lang = Lang::Markdown, .linkSections = true }, - { .lang = Lang::JavaScript }, + { .lang = Lang::JavaScript, .baseImportEvidence = BaseImportEvidence::JsImport }, { .lang = Lang::Bash }, - { .lang = Lang::Java }, + { .lang = Lang::Java, .baseImportEvidence = BaseImportEvidence::JvmImport }, { .lang = Lang::Ruby, .classObjectLookup = true, .mixinAncestry = true, .keepsUnshapedSubscript = true, .constantPathReceiver = true }, { .lang = Lang::Unknown }, { .lang = Lang::Json }, @@ -111,7 +126,7 @@ inline constexpr std::array kLangFacts = { { { .lang = Lang::Lua }, { .lang = Lang::Elixir, .hasInertQuoteRegions = true }, { .lang = Lang::Dart }, - { .lang = Lang::Kotlin }, + { .lang = Lang::Kotlin, .baseImportEvidence = BaseImportEvidence::JvmImport }, { .lang = Lang::GDScript }, { .lang = Lang::Clojure }, } }; diff --git a/test/legotransitivecheck.sh b/test/legotransitivecheck.sh index 31756d6f3..7b25c6863 100755 --- a/test/legotransitivecheck.sh +++ b/test/legotransitivecheck.sh @@ -25,6 +25,11 @@ # an include, the package, a qualifier). A name-only edge's row is hedged base_by="name" and not walked; an edge the # evidence binds to ANOTHER same-named type adds no row and is counted transitive_other_base=. Python's `A.B` base is # B (inside A), `Generic[K]`'s type argument and `metaclass=M` are no base. Red on 4d59c804 (the namesake rows listed). +# (XP) lane fc2-lego-extparent: a class whose base clause the file's OWN import binds OUTSIDE the tree is no implementor of an +# in-tree namesake (Python `ext.Base` / `from ext import Base`, TS/JS `import { Base } from 'lib'` / `lib.Base`, Rust +# `use ext::Base`, Java/Kotlin `import org.ext.Base` / `extends org.ext.Base`); a real in-tree implementor by bare, +# qualified, re-exported, relative, same-package or imported spelling stays; an undecided binding (a star or wildcard +# import) keeps its row. Red on 27eddee1 (every outside namesake listed). Corpus xp/{py,ts,js,rs,java,kt}. # (UW) fix 2 (rule 5): a DIRECT row whose own extends clause is not proven (a cross-package Java import, a Kotlin import, a C# # using, an ES import of a CommonJS base) is not walked below, and the answer SAYS so: the row carries unwalked="1", the # counts those rows transitive_unwalked=N and names the first one's --lego as unwalked_next=; a present-only @@ -357,6 +362,37 @@ if "$BIN" "$FIX" --no-cache --lego=BaseFormatter --legend=full >"$DIR/full" 2>"$ M="$( printf '%s\n' "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"tools/call\",\"params\":{\"name\":\"lego\",\"arguments\":{\"path\":\"$FIX\",\"type\":\"UwCRoot\"}}}" | "$BIN" --mcp 2>/dev/null )" if printf '%s' "$M" | grep -q 'transitive_unwalked=\\"1\\" unwalked_next=\\"--lego=uw/cs/UwCMid.cs:UwCMid\\"' && printf '%s' "$M" | grep -q 'unwalked=\\"1\\"/>'; then ok "MCP lego carries the same count, next and row mark"; else no "MCP lego UwCRoot: $( printf '%s' "$M" | head -c 400 )"; fi +echo "=== (XP) a base the file imports from OUTSIDE the tree is no implementor of an in-tree namesake ===" +# xp SEL "absent rows" "present rows": one --lego answer; every absent row must be missing, every present row listed +xp(){ + local sel="$1" absent="$2" present="$3" n + lego "$sel" || return 0 + for n in $absent + do + if has " Date: Thu, 22 Oct 2026 02:33:05 -0400 Subject: [PATCH 06/77] CHANGELOG: --lego drops outside-bound base namesakes Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 47b9ee6b1..700d1853c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,12 @@ not published here — see `docs/EVALS.md` for the instruments behind the headli ## [Unreleased] +- `--lego` no longer lists a class as an implementor of an in-tree type when the class's own file imports its base from + outside the tree: `class Sub(ext.Base)`, `from ext import Base`, `import { Base } from 'lib'` / `lib.Base`, `use ext::Base; + impl Base for S`, Java/Kotlin `import org.ext.Base` / `extends org.ext.Base` (a package the tree does not declare). A star or + wildcard import, an import the tree cannot place and a same-file declaration still keep the by-name row. This does NOT + cover a C++ `ext::Base` or `using ext::Base`, a C# `using`, a Swift `import`, a PHP `use`, a JS `require` destructure or a + Rust `impl ext::Base for S` with no `use`: those rows are still listed by name. Gate: `test/legotransitivecheck.sh` (XP). - `--quality-delta=A..B` extracts its per-commit archive through stdin (`tar -x -f -`), the same Windows drive-colon fix as #402 (thanks @lennix1337). ## [0.6.8] — 2026-10-19 From 5af40f038a6542fab426d70da9115a759180da7e Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I <210578314+joyful-ii-V-I@users.noreply.github.com> Date: Thu, 22 Oct 2026 03:28:15 -0400 Subject: [PATCH 07/77] --lego outside-base rule: keep a workspace crate and the tree's own package MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The census found two over-drops of the first cut, both real implementors: nushell's `use nu_glob::Interruptible` (a workspace crate whose directory is crates/nu-glob — FE-A's outside-use table reads `nu_glob` as outside) and commander's examples' `import { Command } from 'commander'` (Node self-reference to the tree's own package). The base reader now builds its own crate-dir-aware outside-use table (rustOutsideUses, shared code with FE-A, whose table and call edges are unchanged) and keeps any import of a package name an in-tree package.json declares. Arms: XpWsBase / XpSelfBase in legotransitivecheck (XP). Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 3 +- src/graph.h | 80 +++++++++++++++++-- test/legotransitivecheck.sh | 7 +- .../xp/jsself/examples/ex.js | 2 + test/legotransitivefix/xp/jsself/index.js | 1 + test/legotransitivefix/xp/jsself/package.json | 1 + .../xp/rsws/crates/app/src/lib.rs | 3 + .../xp/rsws/crates/xp-core/src/lib.rs | 1 + 8 files changed, 89 insertions(+), 9 deletions(-) create mode 100644 test/legotransitivefix/xp/jsself/examples/ex.js create mode 100644 test/legotransitivefix/xp/jsself/index.js create mode 100644 test/legotransitivefix/xp/jsself/package.json create mode 100644 test/legotransitivefix/xp/rsws/crates/app/src/lib.rs create mode 100644 test/legotransitivefix/xp/rsws/crates/xp-core/src/lib.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 700d1853c..81f611af3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,7 +18,8 @@ not published here — see `docs/EVALS.md` for the instruments behind the headli - `--lego` no longer lists a class as an implementor of an in-tree type when the class's own file imports its base from outside the tree: `class Sub(ext.Base)`, `from ext import Base`, `import { Base } from 'lib'` / `lib.Base`, `use ext::Base; impl Base for S`, Java/Kotlin `import org.ext.Base` / `extends org.ext.Base` (a package the tree does not declare). A star or - wildcard import, an import the tree cannot place and a same-file declaration still keep the by-name row. This does NOT + wildcard import, an import the tree cannot place, an import of the tree's own package name (Node self-reference), a + workspace crate spelled with `_` for its `-` directory, and a same-file declaration still keep the by-name row. This does NOT cover a C++ `ext::Base` or `using ext::Base`, a C# `using`, a Swift `import`, a PHP `use`, a JS `require` destructure or a Rust `impl ext::Base for S` with no `use`: those rows are still listed by name. Gate: `test/legotransitivecheck.sh` (XP). - `--quality-delta=A..B` extracts its per-commit archive through stdin (`tar -x -f -`), the same Windows drive-colon fix as #402 (thanks @lennix1337). diff --git a/src/graph.h b/src/graph.h index 59992f622..40527e51a 100644 --- a/src/graph.h +++ b/src/graph.h @@ -6100,14 +6100,18 @@ inline HashMap rustModuleNames( const IngestResult& ing ) return names; } -// FE-A Rust: `use a::b::c;` whose first segment is not crate/self/super/a module of this tree names an outside item `c`. -inline void collectRustOutsideUses( const IngestResult& ing, FalseEdgeRules& rules ) +// Rust: `use a::b::c;` whose first segment is not crate/self/super/a module of this tree names an outside item `c`, into +// `out` ("#c"). `crateDirAware` (lane fc2-lego-extparent's base clause reader) also reads a head spelled with `_` as the +// workspace crate directory spelled with `-` (`use nu_glob::X` beside crates/nu-glob): such a head is in the tree, not outside. +// FE-A's own table keeps the plain rule (crateDirAware false), so the call edges stay as they were. +inline void rustOutsideUses( const IngestResult& ing, bool crateDirAware, HashMap& out ) { const HashMap modules = rustModuleNames( ing ); if( modules.empty() ) { return; // no Rust in the tree } + std::string dashed; for( const Include& inc : ing.includes ) { const std::string_view path = inc.target; @@ -6121,13 +6125,29 @@ inline void collectRustOutsideUses( const IngestResult& ing, FalseEdgeRules& rul continue; // `use x;` (a crate root) or a brace group: no single named item } const std::string_view head = path.substr( 0, first ); - if( head != "crate" && head != "self" && head != "super" && !modules.contains( std::string( head ) ) ) + if( head == "crate" || head == "self" || head == "super" || modules.contains( std::string( head ) ) ) { - rules.rustOutsideUse.try_emplace( jsImportKey( inc.fileId, path.substr( path.rfind( "::" ) + 2 ) ), '\0' ); + continue; } + if( crateDirAware && head.find( '_' ) != std::string_view::npos ) + { + dashed.assign( head ); + std::replace( dashed.begin(), dashed.end(), '_', '-' ); + if( modules.contains( dashed ) ) + { + continue; // a workspace crate: its directory spells the crate name with `-` + } + } + out.try_emplace( jsImportKey( inc.fileId, path.substr( path.rfind( "::" ) + 2 ) ), '\0' ); } } +// FE-A Rust: the plain outside-use table (rustOutsideUses, crateDirAware false) +inline void collectRustOutsideUses( const IngestResult& ing, FalseEdgeRules& rules ) +{ + rustOutsideUses( ing, /*crateDirAware=*/false, rules.rustOutsideUse ); +} + inline FalseEdgeRules buildFalseEdgeRules( const IngestResult& ing, const HashMap& classNames, const ExternalVeto& veto, const ExternalVetoTables& vetoTables, const std::vector>& directIncludes ) { @@ -6207,9 +6227,45 @@ struct BaseEvidenceTables const std::vector>& includeAdj; // every language: the files a file's imports resolve to const std::vector& fileDir; // file → interned directory id const ReceiverEvidence* recvEv; // JS/TS: a CommonJS require alias's module (moduleNames) - const FalseEdgeRules* falseEdges; // Rust: a `use` of a path outside the crate (baseBoundOutsideTree) + const HashMap* rustOutsideUse; // Rust: "#X" a `use` of an outside path binds (crate-dir aware) + const HashMap* jsSelfImport; // JS/TS: "#X" an import of the tree's OWN package name binds }; +// JS/TS (lane fc2-lego-extparent): "#var" for every ES import / module alias whose bare specifier names a package +// this tree's own package.json files declare (`import { Command } from 'commander'` inside commander: Node's self-reference, +// or a workspace member) — the import tables may call that module external, but the clause can mean the in-tree class. +inline HashMap jsSelfPackageImports( const IngestResult& ing ) +{ + HashMap names; + for( std::uint32_t f = 0; f < ing.files.size(); ++f ) + { + const std::string_view path = ing.files[ f ]; + if( path == "package.json" || path.ends_with( "/package.json" ) ) + { + tsimport::JsonNode pj; + const tsimport::JsonNode* name = tsimport::parseJsonObject( readConfigBytes( std::string( path ) ), pj ) ? pj.get( "name" ) : nullptr; + if( name != nullptr && name->kind == tsimport::JsonNode::Kind::Str && !name->str.empty() ) + { + names.try_emplace( name->str, char( 1 ) ); + } + } + } + HashMap out; + if( names.empty() ) + { + return out; + } + for( const Binding& b : ing.bindings ) + { + if( ( b.kind == LocalBindKind::JsImport || b.kind == LocalBindKind::ModuleAlias ) && !b.var.empty() && !b.typeName.empty() + && names.contains( std::string( tsimport::splitPackageSpecifier( b.typeName ).first ) ) ) + { + out.try_emplace( jsImportKey( b.fileId, b.var ), char( 1 ) ); + } + } + return out; +} + // is `path` the module `name` inside the package directory `pkgDir` (`pkg/name.py` or `pkg/name/__init__.py`)? inline bool pythonSubmoduleOf( std::string_view path, std::string_view pkgDir, std::string_view name ) noexcept { @@ -6482,6 +6538,10 @@ inline bool baseBoundOutsideTree( const BaseEvidenceTables& t, const Reference& } case BaseImportEvidence::JsImport: { + if( t.jsSelfImport != nullptr && t.jsSelfImport->contains( fileNameKey( key, r.fileId, root ) ) ) + { + return false; // the tree's own package name: the import may land on the in-tree class + } if( qual.empty() ) { const auto pin = t.jsImports.targets.find( jsImportKey( r.fileId, root ) ); @@ -6496,7 +6556,7 @@ inline bool baseBoundOutsideTree( const BaseEvidenceTables& t, const Reference& return it != verdicts->end() && it->second == 'x'; // the namespace import's module is outside (buildQualifierVerdicts) } case BaseImportEvidence::RustUse: - return qual.empty() && t.falseEdges != nullptr && t.falseEdges->lookup( r.fileId, r.calleeName, t.falseEdges->rustOutsideUse ); + return qual.empty() && t.rustOutsideUse != nullptr && t.rustOutsideUse->contains( fileNameKey( key, r.fileId, r.calleeName ) ); case BaseImportEvidence::JvmImport: { if( t.recvEv == nullptr ) @@ -9880,7 +9940,13 @@ inline Graph buildGraph( const IngestResult& ing, const ScipOverlay* scip = null std::vector baseCand; // one inheritance reference's base candidates, reused across references std::vector baseProven; // … and the one of them the language's lookup proves (provenBasesOf), reused std::vector boundEdges; // every edge drawn below with how it was bound; merged after the dedup - const BaseEvidenceTables baseEvidence{ ing, extVeto, jsImports, fileIncludes, includeAdj, fileDir, &recvEv, &falseEdges }; + HashMap rustOutsideBase; // fc2-lego-extparent: built only when FE-A saw an outside `use` at all + if( !falseEdges.rustOutsideUse.empty() ) + { + rustOutsideUses( ing, /*crateDirAware=*/true, rustOutsideBase ); + } + const HashMap jsSelfImport = jsImports.targets.empty() ? HashMap{} : jsSelfPackageImports( ing ); + const BaseEvidenceTables baseEvidence{ ing, extVeto, jsImports, fileIncludes, includeAdj, fileDir, &recvEv, &rustOutsideBase, &jsSelfImport }; { PROFILE_SCOPE_DESCRIBE( "buildGraph/5: inheritance edges" ); for( const Reference& r : ing.references ) diff --git a/test/legotransitivecheck.sh b/test/legotransitivecheck.sh index 7b25c6863..113284200 100755 --- a/test/legotransitivecheck.sh +++ b/test/legotransitivecheck.sh @@ -29,7 +29,8 @@ # in-tree namesake (Python `ext.Base` / `from ext import Base`, TS/JS `import { Base } from 'lib'` / `lib.Base`, Rust # `use ext::Base`, Java/Kotlin `import org.ext.Base` / `extends org.ext.Base`); a real in-tree implementor by bare, # qualified, re-exported, relative, same-package or imported spelling stays; an undecided binding (a star or wildcard -# import) keeps its row. Red on 27eddee1 (every outside namesake listed). Corpus xp/{py,ts,js,rs,java,kt}. +# import), a workspace crate spelled with `_` for its `-` directory, and the tree's own package name keep their row. +# Red on 27eddee1 (every outside namesake listed). Corpus xp/{py,ts,js,rs,java,kt}. # (UW) fix 2 (rule 5): a DIRECT row whose own extends clause is not proven (a cross-package Java import, a Kotlin import, a C# # using, an ES import of a CommonJS base) is not walked below, and the answer SAYS so: the row carries unwalked="1", the # counts those rows transitive_unwalked=N and names the first one's --lego as unwalked_next=; a present-only @@ -382,6 +383,10 @@ xp XpJsBase "XpJsOutBare" "XpJsInBare" xp XpRsBase "XpRsOutUse" "XpRsInUse" xp XpJBase "XpJOutImport XpJOutQual" "XpJSamePkg XpJInImport XpJInQual XpJOutWild" xp XpKBase "XpKOutImport" "XpKInImport" +# near misses an outside-looking spelling must NOT drop: a workspace crate whose directory spells the crate with `-` +# (`use xp_core::XpWsBase` beside crates/xp-core), and an import of the tree's OWN package name (Node self-reference) +xp XpWsBase "" "XpWsImpl" +xp XpSelfBase "" "XpSelfUser" if lego py/xpkg/core.py:XpBase then if iface | grep -q 'implementors="6"'; then ok "implementors= counts the in-tree rows alone (five + XpShadowed, a same-file namesake's: card)"; else no "XpBase count: $( iface )"; fi diff --git a/test/legotransitivefix/xp/jsself/examples/ex.js b/test/legotransitivefix/xp/jsself/examples/ex.js new file mode 100644 index 000000000..1ca4e97db --- /dev/null +++ b/test/legotransitivefix/xp/jsself/examples/ex.js @@ -0,0 +1,2 @@ +import { XpSelfBase } from 'xp-self-pkg'; +export class XpSelfUser extends XpSelfBase {} diff --git a/test/legotransitivefix/xp/jsself/index.js b/test/legotransitivefix/xp/jsself/index.js new file mode 100644 index 000000000..0074c7c30 --- /dev/null +++ b/test/legotransitivefix/xp/jsself/index.js @@ -0,0 +1 @@ +export class XpSelfBase { run() { return 1; } } diff --git a/test/legotransitivefix/xp/jsself/package.json b/test/legotransitivefix/xp/jsself/package.json new file mode 100644 index 000000000..8282b3109 --- /dev/null +++ b/test/legotransitivefix/xp/jsself/package.json @@ -0,0 +1 @@ +{ "name": "xp-self-pkg", "exports": { ".": "./index.js" } } diff --git a/test/legotransitivefix/xp/rsws/crates/app/src/lib.rs b/test/legotransitivefix/xp/rsws/crates/app/src/lib.rs new file mode 100644 index 000000000..226e46157 --- /dev/null +++ b/test/legotransitivefix/xp/rsws/crates/app/src/lib.rs @@ -0,0 +1,3 @@ +use xp_core::XpWsBase; +pub struct XpWsImpl; +impl XpWsBase for XpWsImpl { fn run(&self) {} } diff --git a/test/legotransitivefix/xp/rsws/crates/xp-core/src/lib.rs b/test/legotransitivefix/xp/rsws/crates/xp-core/src/lib.rs new file mode 100644 index 000000000..dd6bc9514 --- /dev/null +++ b/test/legotransitivefix/xp/rsws/crates/xp-core/src/lib.rs @@ -0,0 +1 @@ +pub trait XpWsBase { fn run(&self); } From 149b0b1b4edb5ac5de1ce44f5e55d6f0bfc5ba5a Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I <210578314+joyful-ii-V-I@users.noreply.github.com> Date: Thu, 22 Oct 2026 11:46:11 -0400 Subject: [PATCH 08/77] --lego outside-base rule, fix round 1: Cargo crate names, multi-root self-package, root-package Python, no floor for a decided clause A crate's own Cargo package/[lib] name (tests/, examples/, main.rs) and a workspace crate named differently from its directory are in the tree; jsSelfPackageImports reads through diskPath (multi-root); a Python repo whose root is the package keeps its self-import rows; a clause dropped for a decided outside binding adds no implementors_floor. Co-Authored-By: Claude Sonnet 5.5 --- CHANGELOG.md | 7 +- src/countfloor.h | 12 +- src/graph.h | 169 ++++++++++++++++-- src/mcpverbs.h | 8 +- src/serialize.h | 8 +- src/verbs_for.h | 10 +- test/legotransitivecheck.sh | 35 +++- test/legotransitivefix/xp/mrroot/u.js | 1 + test/legotransitivefix/xp/pyflat/__init__.py | 0 test/legotransitivefix/xp/pyflat/base.py | 2 + test/legotransitivefix/xp/pyflat/other.py | 3 + test/legotransitivefix/xp/pyflat/sub.py | 3 + test/legotransitivefix/xp/rsself/Cargo.toml | 3 + .../xp/rsself/examples/ex.rs | 3 + test/legotransitivefix/xp/rsself/src/lib.rs | 1 + test/legotransitivefix/xp/rsself/src/main.rs | 4 + test/legotransitivefix/xp/rsself/tests/it.rs | 3 + .../xp/rsws/crates/app/src/ren.rs | 3 + .../xp/rsws/crates/ren-dir/Cargo.toml | 3 + .../xp/rsws/crates/ren-dir/src/lib.rs | 1 + 20 files changed, 251 insertions(+), 28 deletions(-) create mode 100644 test/legotransitivefix/xp/mrroot/u.js create mode 100644 test/legotransitivefix/xp/pyflat/__init__.py create mode 100644 test/legotransitivefix/xp/pyflat/base.py create mode 100644 test/legotransitivefix/xp/pyflat/other.py create mode 100644 test/legotransitivefix/xp/pyflat/sub.py create mode 100644 test/legotransitivefix/xp/rsself/Cargo.toml create mode 100644 test/legotransitivefix/xp/rsself/examples/ex.rs create mode 100644 test/legotransitivefix/xp/rsself/src/lib.rs create mode 100644 test/legotransitivefix/xp/rsself/src/main.rs create mode 100644 test/legotransitivefix/xp/rsself/tests/it.rs create mode 100644 test/legotransitivefix/xp/rsws/crates/app/src/ren.rs create mode 100644 test/legotransitivefix/xp/rsws/crates/ren-dir/Cargo.toml create mode 100644 test/legotransitivefix/xp/rsws/crates/ren-dir/src/lib.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 81f611af3..b53352b70 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,8 +18,11 @@ not published here — see `docs/EVALS.md` for the instruments behind the headli - `--lego` no longer lists a class as an implementor of an in-tree type when the class's own file imports its base from outside the tree: `class Sub(ext.Base)`, `from ext import Base`, `import { Base } from 'lib'` / `lib.Base`, `use ext::Base; impl Base for S`, Java/Kotlin `import org.ext.Base` / `extends org.ext.Base` (a package the tree does not declare). A star or - wildcard import, an import the tree cannot place, an import of the tree's own package name (Node self-reference), a - workspace crate spelled with `_` for its `-` directory, and a same-file declaration still keep the by-name row. This does NOT + wildcard import, an import the tree cannot place, an import of the tree's own package name (Node self-reference, also on a + multi-root run; a Python repo whose root is the package, by its directory or `[project] name`; a Rust crate's Cargo + `[package]`/`[lib]` name from its tests/, examples/ and main.rs), a workspace crate spelled with `_` for its `-` directory + or named in its Cargo.toml, and a same-file declaration still keep the by-name row, and a clause dropped as outside adds no + `implementors_floor=`. This does NOT cover a C++ `ext::Base` or `using ext::Base`, a C# `using`, a Swift `import`, a PHP `use`, a JS `require` destructure or a Rust `impl ext::Base for S` with no `use`: those rows are still listed by name. Gate: `test/legotransitivecheck.sh` (XP). - `--quality-delta=A..B` extracts its per-commit archive through stdin (`tar -x -f -`), the same Windows drive-colon fix as #402 (thanks @lennix1337). diff --git a/src/countfloor.h b/src/countfloor.h index 9538e9c34..2287ee1f2 100644 --- a/src/countfloor.h +++ b/src/countfloor.h @@ -319,7 +319,7 @@ inline std::vector callerFloors( const IngestResult& ing, const Gra // The structural-typing limit (a TS class or Go type that satisfies an interface without naming it) leaves no // reference at all, and is stated in the legend rather than guessed at here. inline std::vector implementorFloors( const IngestResult& ing, const std::vector>& graphImplementors, - std::span ifaces ) + std::span ifaces, std::span decidedOutside = {} ) { std::vector out( ifaces.size(), 0 ); countfloor::RowsOfName rowsOfName; @@ -336,7 +336,10 @@ inline std::vector implementorFloors( const IngestResult& ing, const std:: } const countfloor::IdsOfName boundDerived = countfloor::relatedByName( ing, rowsOfName, graphImplementors.size(), [ & ]( NodeId id, std::vector& into ) { into.insert( into.end(), graphImplementors[id].begin(), graphImplementors[id].end() ); } ); - countfloor::markUnboundSpellings( ing, rowsOfName, boundDerived, []( const Reference& r ) { return r.isInherit || r.role == RefRole::Extends; }, + // a clause whose own import binds its base OUTSIDE the tree (Graph::baseOutsideClauses, sorted) is decided, not unbound + const auto notDecided = [ & ]( const Reference& r ) + { return !std::binary_search( decidedOutside.begin(), decidedOutside.end(), std::uint32_t( &r - ing.references.data() ) ); }; + countfloor::markUnboundSpellings( ing, rowsOfName, boundDerived, [ & ]( const Reference& r ) { return ( r.isInherit || r.role == RefRole::Extends ) && notDecided( r ); }, [ & ]( const Reference& r, std::span ids ) { return countfloor::inheritIsBound( ing, r, ids ); }, {}, // an inherit clause is never a forwarded call [ & ]( std::uint32_t row ) { out[row] = 1; } ); @@ -396,10 +399,11 @@ inline std::string legoClosureLegendComment( const std::vector>& graphImplementors, NodeId focus ) +inline const char* implementorsFloorLegendComment( const IngestResult& ing, const std::vector>& graphImplementors, NodeId focus, + std::span decidedOutside = {} ) { const NodeId one[1] = { focus }; - return implementorFloors( ing, graphImplementors, std::span( one ) )[0] ? kImplementorsFloorLegendComment : ""; + return implementorFloors( ing, graphImplementors, std::span( one ), decidedOutside )[0] ? kImplementorsFloorLegendComment : ""; } } // namespace rw diff --git a/src/graph.h b/src/graph.h index 40527e51a..2026deb94 100644 --- a/src/graph.h +++ b/src/graph.h @@ -91,6 +91,10 @@ struct Graph // lane lego-transitive fix 1: parallel to implementors — how each base→derived edge was BOUND (BaseBound). The direct rows // and every other reader keep reading implementors alone; only the --lego closure (legoImplementorClosure) walks this. std::vector> implementorBound; + // lane fc2-lego-extparent: the ascending IngestResult::references indices of the extends/implements clauses the file's own + // import binds OUTSIDE the tree (baseBoundOutsideTree) — drawn no edge, and DECIDED, so --lego's implementors_floor does not + // count them as a clause bound nowhere (countfloor.h implementorFloors). + std::vector baseOutsideClauses; // lane ruby-pair: the Ruby main object as an implementor — each top-level `include M` / `extend M` whose constant the // tree opens (rubyMainObjectMixins), sorted by (base, fileId, line). Not in `implementors`: main is no symbol, so the // graph's class readers (closure, CHA, ranks) never see it; only --lego's targeted answer lists it. @@ -6100,9 +6104,78 @@ inline HashMap rustModuleNames( const IngestResult& ing ) return names; } +// Every `name = "x"` line of a TOML text's `[section]` tables listed in `sections`, appended to `out` (a bare line scan: a +// dotted key, an inline table or a multi-line string is not read, which can only miss a name, never invent one). +inline void tomlSectionNames( std::string_view text, std::initializer_list sections, std::vector& out ) +{ + bool inSection = false; + while( !text.empty() ) + { + const std::size_t end = text.find( '\n' ); + std::string_view line = text.substr( 0, end ); + text = end == std::string_view::npos ? std::string_view() : text.substr( end + 1 ); + line.remove_prefix( std::min( line.find_first_not_of( " \t\r" ), line.size() ) ); + if( line.starts_with( '[' ) ) + { + inSection = std::ranges::any_of( sections, [ & ]( std::string_view h ) { return line.starts_with( h ); } ); + continue; + } + if( !inSection || !line.starts_with( "name" ) ) + { + continue; + } + line.remove_prefix( 4 ); + line.remove_prefix( std::min( line.find_first_not_of( " \t" ), line.size() ) ); + if( !line.starts_with( '=' ) ) + { + continue; + } + line.remove_prefix( 1 ); + line.remove_prefix( std::min( line.find_first_not_of( " \t" ), line.size() ) ); + if( line.size() < 2 || ( line.front() != '"' && line.front() != '\'' ) ) + { + continue; + } + const std::size_t close = line.find( line.front(), 1 ); + if( close != std::string_view::npos && close > 1 ) + { + out.emplace_back( line.substr( 1, close - 1 ) ); + } + } +} + +// The crate names this tree's Cargo.toml files declare, spelled the way a `use` writes them (`foo-core` -> `foo_core`): each +// manifest's `[package] name` and `[lib] name`. A crate's own name is no path segment of any .rs file, yet `use mycrate::T` in +// its tests/, examples/, benches/ and src/main.rs names the crate itself, and a workspace crate's package name need not be its +// directory's. Read through diskPath (the multi-root seam). +inline HashMap cargoCrateNames( const IngestResult& ing ) +{ + HashMap names; + std::vector found; + for( std::uint32_t f = 0; f < ing.files.size(); ++f ) + { + const std::string_view rel = rootRelPath( ing, f ); + if( rel != "Cargo.toml" && !rel.ends_with( "/Cargo.toml" ) ) + { + continue; + } + if( const std::optional text = docparse::detail::readWholeFile( diskPath( ing, f ) ) ) + { + tomlSectionNames( *text, { "[package]", "[lib]" }, found ); + } + } + for( std::string& name : found ) + { + std::replace( name.begin(), name.end(), '-', '_' ); + names.try_emplace( std::move( name ), char( 1 ) ); + } + return names; +} + // Rust: `use a::b::c;` whose first segment is not crate/self/super/a module of this tree names an outside item `c`, into // `out` ("#c"). `crateDirAware` (lane fc2-lego-extparent's base clause reader) also reads a head spelled with `_` as the -// workspace crate directory spelled with `-` (`use nu_glob::X` beside crates/nu-glob): such a head is in the tree, not outside. +// workspace crate directory spelled with `-` (`use nu_glob::X` beside crates/nu-glob), and a head that is a Cargo package or +// [lib] name of this tree (`use mycrate::X` from its tests/): such a head is in the tree, not outside. // FE-A's own table keeps the plain rule (crateDirAware false), so the call edges stay as they were. inline void rustOutsideUses( const IngestResult& ing, bool crateDirAware, HashMap& out ) { @@ -6112,6 +6185,7 @@ inline void rustOutsideUses( const IngestResult& ing, bool crateDirAware, HashMa return; // no Rust in the tree } std::string dashed; + const HashMap cargoNames = crateDirAware ? cargoCrateNames( ing ) : HashMap{}; for( const Include& inc : ing.includes ) { const std::string_view path = inc.target; @@ -6129,6 +6203,10 @@ inline void rustOutsideUses( const IngestResult& ing, bool crateDirAware, HashMa { continue; } + if( crateDirAware && cargoNames.contains( std::string( head ) ) ) + { + continue; // a crate this tree's Cargo.toml names (its own, or a workspace member under another directory) + } if( crateDirAware && head.find( '_' ) != std::string_view::npos ) { dashed.assign( head ); @@ -6228,7 +6306,7 @@ struct BaseEvidenceTables const std::vector& fileDir; // file → interned directory id const ReceiverEvidence* recvEv; // JS/TS: a CommonJS require alias's module (moduleNames) const HashMap* rustOutsideUse; // Rust: "#X" a `use` of an outside path binds (crate-dir aware) - const HashMap* jsSelfImport; // JS/TS: "#X" an import of the tree's OWN package name binds + const HashMap* selfImport; // JS/TS, Python: "#X" an import of the tree's OWN package name binds }; // JS/TS (lane fc2-lego-extparent): "#var" for every ES import / module alias whose bare specifier names a package @@ -6239,11 +6317,11 @@ inline HashMap jsSelfPackageImports( const IngestResult& ing HashMap names; for( std::uint32_t f = 0; f < ing.files.size(); ++f ) { - const std::string_view path = ing.files[ f ]; + const std::string_view path = rootRelPath( ing, f ); if( path == "package.json" || path.ends_with( "/package.json" ) ) { tsimport::JsonNode pj; - const tsimport::JsonNode* name = tsimport::parseJsonObject( readConfigBytes( std::string( path ) ), pj ) ? pj.get( "name" ) : nullptr; + const tsimport::JsonNode* name = tsimport::parseJsonObject( readConfigBytes( diskPath( ing, f ) ), pj ) ? pj.get( "name" ) : nullptr; if( name != nullptr && name->kind == tsimport::JsonNode::Kind::Str && !name->str.empty() ) { names.try_emplace( name->str, char( 1 ) ); @@ -6266,6 +6344,66 @@ inline HashMap jsSelfPackageImports( const IngestResult& ing return out; } +// Python (lane fc2-lego-extparent, fix round 1): "#var" for every import whose first segment is the name of a package +// whose root IS this tree's root — a top-level __init__.py, so the crawl root's own directory name, or the root pyproject.toml's +// `[project] name`, is a module of the tree that no path inside it spells (`from mypkg.base import Base` in mypkg/sub.py, +// crawled as `ripwire mypkg`). The Phase-5 import table reads such a head as outside; the clause can mean the in-tree class. +inline HashMap pythonRootPackageImports( const IngestResult& ing ) +{ + HashMap out; + if( ing.crawlRoot.empty() ) + { + return out; // a multi-root run has no one package root + } + std::uint32_t initFile = kNoFile; + std::uint32_t pyproject = kNoFile; + for( std::uint32_t f = 0; f < ing.files.size(); ++f ) + { + const std::string_view rel = rootRelPath( ing, f ); + if( rel == "__init__.py" ) + { + initFile = f; + } + else if( rel == "pyproject.toml" ) + { + pyproject = f; + } + } + if( initFile == kNoFile ) + { + return out; + } + std::vector found; + std::error_code ec; + const std::filesystem::path root = std::filesystem::absolute( ing.crawlRoot, ec ).lexically_normal(); + found.push_back( ( root.has_filename() ? root : root.parent_path() ).filename().string() ); + if( pyproject != kNoFile ) + { + if( const std::optional text = docparse::detail::readWholeFile( diskPath( ing, pyproject ) ) ) + { + tomlSectionNames( *text, { "[project]" }, found ); + } + } + HashMap names; + for( std::string& name : found ) + { + std::replace( name.begin(), name.end(), '-', '_' ); + if( !name.empty() ) + { + names.try_emplace( std::move( name ), char( 1 ) ); + } + } + for( const Binding& b : ing.bindings ) + { + if( b.kind == LocalBindKind::Import && !b.var.empty() && !b.typeName.empty() && b.typeName.front() != '.' + && names.contains( std::string( std::string_view( b.typeName ).substr( 0, b.typeName.find( '.' ) ) ) ) ) + { + out.try_emplace( jsImportKey( b.fileId, b.var ), char( 1 ) ); + } + } + return out; +} + // is `path` the module `name` inside the package directory `pkgDir` (`pkg/name.py` or `pkg/name/__init__.py`)? inline bool pythonSubmoduleOf( std::string_view path, std::string_view pkgDir, std::string_view name ) noexcept { @@ -6529,6 +6667,10 @@ inline bool baseBoundOutsideTree( const BaseEvidenceTables& t, const Reference& const std::string_view qual = r.memberRoot; const std::string_view root = qual.empty() ? std::string_view( r.calleeName ) : qual.substr( 0, qual.find( '.' ) ); std::string key; + if( t.selfImport != nullptr && t.selfImport->contains( fileNameKey( key, r.fileId, root ) ) ) + { + return false; // the tree's own package name (Node self-reference, a root-is-the-package Python import): it may land on the in-tree class + } switch( langFacts( r.lang ).baseImportEvidence ) { case BaseImportEvidence::PythonImport: @@ -6538,10 +6680,6 @@ inline bool baseBoundOutsideTree( const BaseEvidenceTables& t, const Reference& } case BaseImportEvidence::JsImport: { - if( t.jsSelfImport != nullptr && t.jsSelfImport->contains( fileNameKey( key, r.fileId, root ) ) ) - { - return false; // the tree's own package name: the import may land on the in-tree class - } if( qual.empty() ) { const auto pin = t.jsImports.targets.find( jsImportKey( r.fileId, root ) ); @@ -9945,8 +10083,12 @@ inline Graph buildGraph( const IngestResult& ing, const ScipOverlay* scip = null { rustOutsideUses( ing, /*crateDirAware=*/true, rustOutsideBase ); } - const HashMap jsSelfImport = jsImports.targets.empty() ? HashMap{} : jsSelfPackageImports( ing ); - const BaseEvidenceTables baseEvidence{ ing, extVeto, jsImports, fileIncludes, includeAdj, fileDir, &recvEv, &rustOutsideBase, &jsSelfImport }; + HashMap selfImport = jsImports.targets.empty() ? HashMap{} : jsSelfPackageImports( ing ); + for( auto& [ k, v ] : pythonRootPackageImports( ing ) ) + { + selfImport.try_emplace( k, v ); + } + const BaseEvidenceTables baseEvidence{ ing, extVeto, jsImports, fileIncludes, includeAdj, fileDir, &recvEv, &rustOutsideBase, &selfImport }; { PROFILE_SCOPE_DESCRIBE( "buildGraph/5: inheritance edges" ); for( const Reference& r : ing.references ) @@ -9979,8 +10121,13 @@ inline Graph buildGraph( const IngestResult& ing, const ScipOverlay* scip = null { derived = cand; break; } // ids are ascending → lowest-id match (deterministic) } } - if( derived == kNoNode || baseBoundOutsideTree( baseEvidence, r ) ) + if( derived == kNoNode ) + { + continue; + } + if( baseBoundOutsideTree( baseEvidence, r ) ) { + g.baseOutsideClauses.push_back( std::uint32_t( &r - ing.references.data() ) ); continue; // fc2-lego-extparent: the file's own import binds the written base outside the tree — no in-tree base } if( ing.symbols[ derived ].lang == Lang::Ruby ) diff --git a/src/mcpverbs.h b/src/mcpverbs.h index 41b38e997..43004e490 100644 --- a/src/mcpverbs.h +++ b/src/mcpverbs.h @@ -2192,7 +2192,7 @@ inline McpForAssembly assembleMcpFor( const McpForAssemblyInputs& in, std::strin const std::string mcpLegoRootPrefix = flRootArg.empty() ? std::string() : rw::sarif::rootPrefixOf( flRootArg ); if( !legoStr.empty() && narrowLegoToRenderedSigs( ing, legoScoped, sigsRowsAtTarget( sigsStr, mcpSigsCut ), mcpLegoRootPrefix ) ) // forbudget-soft { - legoStr = renderToString( [ & ]( std::FILE* m2 ) { packLego( m2, ing, legoScoped, lensRank, 12, redact, &impure, kNoNode, /*withPaths=*/true, flRootArg, {}, &legoPreCapCount, &implementors ); } ); // R-R: the re-render dropped the root its first render (above) passed + legoStr = renderToString( [ & ]( std::FILE* m2 ) { packLego( m2, ing, legoScoped, lensRank, 12, redact, &impure, kNoNode, /*withPaths=*/true, flRootArg, LegoTargetedArgs().withDecidedOutside( in.g.baseOutsideClauses ), &legoPreCapCount, &implementors ); } ); // R-R: the re-render dropped the root its first render (above) passed } // R2-L2' (round-2, priced re-registration of L2/B1): the CLI twin's exact stub substitution // (verbs_for.h) — same rule, same restoring spelling (kept as the CLI flag form: this dialect's next= @@ -2778,7 +2778,7 @@ inline std::optional forTaskText( const std::string& root, const st // L2 (round-1 lever B1): each renderer's OWN pre-cap row count, captured by the SAME call that renders // the real body — see verbs_for.h's identical CLI-twin comment for the full rationale. std::size_t legoPreCapCount = 0, composePreCapCount = 0; - std::string legoStr = renderToString( [ & ]( std::FILE* m2 ) { packLego( m2, ing, legoScoped, lensRank, 12, redact, &impure, kNoNode, /*withPaths=*/true, flRootArg, {}, &legoPreCapCount, &ix.g.implementors ); } ); + std::string legoStr = renderToString( [ & ]( std::FILE* m2 ) { packLego( m2, ing, legoScoped, lensRank, 12, redact, &impure, kNoNode, /*withPaths=*/true, flRootArg, LegoTargetedArgs().withDecidedOutside( ix.g.baseOutsideClauses ), &legoPreCapCount, &ix.g.implementors ); } ); std::string composeStr, routeStr; if( !ix.g.composeEdges.empty() ) { @@ -2893,12 +2893,12 @@ inline std::string legoText( const std::string& root, const std::string& type, R // clause rides as its own comment beside the closed literal, exactly as on the CLI. rw::emitTo( mem, "{}{}{}{}{}", kLegoLegend, graphUnindexedLegendComment( rw::graphGaugeClauses( ix.g ) ).c_str(), unprovenDefsVerbComment( UnprovenDefsVerb::Lego, unprovenDefs > 0, "", anyEncFloor ? kGrepEncFloorClause : "", anyCarrierAttr ? kGrepCarrierClause : "", - pageCarriers.skippedAttrs.empty() ? "" : kGrepCarrierSkipClause, rw::kPageRaiseCapClause ); + "{}{} -->", anyEncFloor ? kGrepEncFloorClause : "", anyCarrierAttr ? kGrepCarrierClause : "", + pageCarriers.skippedAttrs.empty() ? "" : kGrepCarrierSkipClause, + grepHonestLexClause( !grepMetaAttr( cfg, pat, esc ).empty(), !found.occ.empty() ), rw::kPageRaiseCapClause ); } emitGrepHandleLegend( cfg.grepHandles ); // G3: terms=/scope=/suppressed= — only when AND/NOT was actually given, so a plain --grep answer // stays byte-identical to before G3 landed (the "purely additive" rule every ripwire flag follows). - const std::string termsAttr = grepTermsAttrs( pat, grepTerms, grepScopeVal, termsSuppressed, esc ); + const std::string termsAttr = grepTermsAttrs( pat, grepTerms, grepScopeVal, termsSuppressed, esc ) + grepMetaAttr( cfg, pat, esc ); // G4 (2026-08-15 harvest, report-ugrep §F6): corpus_excluded=/corpus_oversize= — so hits="0" can // distinguish "not in this repo" from "in a file the crawl never scanned" (an --exclude= match, or a // file past --max-file-size). Absent when zero, matching skippedOversize's own "absent means nothing @@ -1182,26 +1246,10 @@ int emitGrepReport( const MainDispatch& d ) } scanShortAttr += pageCarriers.skippedAttrs; // L4: the reads the memory guard stopped (reads_skipped=, reads_next=), or "" const char* schemaAttr = cfg.legend == "compact" ? " schema=\"ripwire.grep/v1\"" : ""; - // P3 (L7, nextverb.h): the one follow-up. A CUT answer → the next page, under the compact legend (the page - // is what the agent wants, not the prose it has already read); a hit → the enclosing-definition chain at the - // top hit (--at=FILE:LINE, the grep→at→expand chain lens 8 named); zero hits → the conceptual fallback. - std::string grepNext; - if( grepPage.end < hitCount || hitsCapped != 0 ) - { - grepNext = rw::nextFlag( cfg.grepRegex ? "--regex=" : "--grep=", pat ) + " --offset=" + std::to_string( grepPage.end ) + " --legend=compact"; - } - else if( const std::string carrierNext = rw::grepCarrierNext( ing, hits, encRows, encCarriers ); !carrierNext.empty() ) - { - grepNext = carrierNext; // L4: the top hit sits in a carrier the uses verb lists reads for — those reads are the next hop - } - else if( !hits.empty() ) - { - grepNext = rw::nextFlag( "--at=", std::string( pathFor( hits[ 0 ].fileId ) ) + ":" + std::to_string( hits[ 0 ].line ) ); - } - else - { - grepNext = rw::nextFlag( "--for=", pat ); - } + // P3 (L7) + honest-lex: the one follow-up (grepRootNext above states the order). + const std::string grepNext = grepRootNext( cfg, pat, grepPage.end < hitCount || hitsCapped != 0, grepPage.end, + [ & ] { return rw::grepCarrierNext( ing, hits, encRows, encCarriers ); }, + [ & ] { return hits.empty() ? std::string() : std::string( pathFor( hits[ 0 ].fileId ) ) + ":" + std::to_string( hits[ 0 ].line ); } ); rw::emitTo( stdout, "", ex( pat ).c_str(), schemaAttr, rootAttr.c_str(), termsAttr.c_str(), filesMatched, hitCount, pageDisclosure( grab, sizeof( grab ), grepPage.end - grepPage.begin, hitCount, grepPage.end, @@ -1237,10 +1285,7 @@ int emitGrepReport( const MainDispatch& d ) { rw::emitTo( stdout, " in=\"{}\"", ex( h.enclosing ).c_str() ); } - if( h.lineBytes != 0 ) // the matched-line cut, disclosed: absent = the whole line is here - { - rw::emitTo( stdout, " line_bytes=\"{}\"", h.lineBytes ); - } + rw::emitTo( stdout, "{}", grepRowTailAttrs( h.lineBytes, h.occ ) ); // line_bytes= (cut line), occ= (honest-lex) if( !c.more.empty() ) { rw::emitTo( stdout, " n=\"{}\"", c.more.size() + 1 ); // 1 (this row) + the folded sites — sums to shown= diff --git a/test/honestlexcheck.sh b/test/honestlexcheck.sh new file mode 100755 index 000000000..b7c230080 --- /dev/null +++ b/test/honestlexcheck.sh @@ -0,0 +1,223 @@ +#!/usr/bin/env bash +# honestlexcheck.sh — two honesty fixes in two lexical emitters (lane honest-lex, ideas-32b cards #1 and #2). +# +# (A) WHEREIS-DEF. --whereis labels HEAD rows with the parsed index when the index defines the name +# (head_labels="index"); otherwise a LEXICAL shape test labels them (head_labels="lexical"). That test took a +# declaration word ANYWHERE earlier on the line, so a comment (`// class X …`), a help string, a doc line and every +# `struct T *data` local read kind="def" (Stage 0: 241 false def rows over 30 cells). Now the marker must sit +# directly before the name, a comment line / a name after a comment opener / a name inside a string is never a +# definition, `#define NAME` is, and head_defs="0" says when the index defines no symbol of the name at all. +# (B) GREP-META. A literal --grep whose pattern reads as a regex (`a|b`, `x\.y`, `.*` inside, `^…`, `[^…]`) answered a +# confident zero (or a narrowed list) with next= --for. Now the root carries pattern_meta= and next= is the regex +# verb on the pattern. Folded in: one row per LINE with occ=N (hits= counts lines). MCP twin keys too. +# +# Fixture: a throwaway git repo under $TMP (every name is hlx-prefixed, so no other gate's zero-hit word is touched). +# The lexical path is forced with --exclude=hlxfix/ (the index then knows no def of these names), the documented way +# to reach it on a clean tree (a dirty file keeps the index labels). +# +# RED-FIRST: recorded against base 004b4c6ba — every (A) arm that asserts a ref on a marker/comment/string line, +# the #define-object arm, head_defs, and every (B) arm but the plain-literal negatives FAIL there. +# +# Usage: test/honestlexcheck.sh [BIN] | RIPWIRE_BIN=asan/ripwire test/honestlexcheck.sh +set -u +ROOT="$( cd "$( dirname "$0" )/.." && pwd )" +BIN="${1:-${RIPWIRE_BIN:-$ROOT/build/ripwire}}" +[ "${BIN#/}" = "$BIN" ] && BIN="$ROOT/$BIN" +fail=0 +ok(){ printf ' PASS %s\n' "$*"; } +no(){ printf ' FAIL %s\n' "$*"; fail=1; } +[ -x "$BIN" ] || { echo "no ripwire binary at $BIN — build first"; exit 2; } +command -v python3 >/dev/null 2>&1 || { echo "python3 required"; exit 2; } +command -v git >/dev/null 2>&1 || { echo "git required"; exit 2; } +echo "honestlexcheck: BIN=$BIN" + +TMP="$( mktemp -d )"; trap 'rm -rf "$TMP"' EXIT +R="$TMP/repo"; F="$R/hlxfix"; mkdir -p "$F" +cat > "$F/c.c" <<'EOF' +// class hlxComment is a comment, not a definition +#define HLX_OBJ 1 +#define HLX_FN(x) ((x)+1) +#ifndef HLX_OBJ +struct hlx_tag { + int n; +}; +static struct hlx_tag *hlx_local; +typedef unsigned long hlx_key; +typedef void (*hlx_cb)(hlx_key, int); +const char *hlx_s = "struct hlx_str is in a string"; +int hlx_fn2( int a ) { + return a; // struct hlx_trail is a trailing comment +} +# define HLX_SPACED 2 +EOF +cat > "$F/p.py" <<'EOF' +# def hlx_pycomment(): commented out +class HlxPy(Base): +def hlx_run( hlx_param ): + """class HlxDoc is a docstring""" +x = 1 # class HlxHashTrail trailing + yield HlxYield("a", "b") +EOF +cat > "$F/r.rs" <<'EOF' +#[inline] fn hlx_hot() {} +impl<'a> Display for HlxType<'a> {} +EOF +cat > "$F/g.go" <<'EOF' +func (r *HlxRecv) HlxMethod() int { return 1 } +EOF +cat > "$F/t.ts" <<'EOF' +export class HlxClass extends HlxBase {} +import type { HlxImported } from './x' +throw new HlxThrown() +EOF +cat > "$F/u.cpp" <<'EOF' +using std::hlx_using; +using HlxAlias = int; +using namespace hlx_ns; +class RW_API HlxExported { +template T hlx_tpl( T a ) { +EOF +cat > "$F/d.md" <<'EOF' +The `class HlxTick` spelling sits in backticks. +- (see above): hlxBullet is a markdown bullet +EOF +cat > "$F/m.m" <<'EOF' +- (void) hlxObjc { +EOF +cat > "$R/keep.c" <<'EOF' +int hlx_keep( void ) { return 0; } +int hlx_indexed( int a ) { return a; } +// hlx_indexed is mentioned in a comment +EOF +( cd "$R" && git init -q && git add -A && git -c user.name=t -c user.email=t@t commit -qm fixture ) || { echo "fixture git failed"; exit 2; } + +# kind of the HEAD row at FILE:LINE in a whereis answer (or MISSING), plus the root's head_labels/head_defs +wh() { ( cd "$R" && "$BIN" "$R" --no-cache "$@" --whereis-listing=all --limit=200 2>/dev/null ); } +kind_at() { python3 -c ' +import re,sys +s=sys.stdin.read(); p,l=sys.argv[1],sys.argv[2] +m=[k for (pp,ll,k) in re.findall(r"]*? p=\"([^\"]*)\" l=\"(\d+)\" kind=\"(\w+)\"",s) if pp==p and ll==l] +print(m[0] if m else "MISSING")' "$1" "$2"; } +arm() { # arm LABEL SYM FILE LINE WANT + local out k; out="$( wh --exclude=hlxfix/ --whereis="$2" )" + k="$( printf '%s' "$out" | kind_at "$3" "$4" )" + [ "$k" = "$5" ] && ok "$1 ($2 at $3:$4 kind=$k)" || no "$1 ($2 at $3:$4: kind=$k, want $5)" +} + +echo "── (A) lexical definition shape (head_labels=lexical forced) ──" +arm "A1 a comment line naming a declaration word is no definition" hlxComment hlxfix/c.c 1 ref +arm "A2 #define NAME (object-like) is a definition" HLX_OBJ hlxfix/c.c 2 def +arm "A3 #define NAME( (function-like) is a definition" HLX_FN hlxfix/c.c 3 def +arm "A4 #ifndef NAME is no definition (a preprocessor test)" HLX_OBJ hlxfix/c.c 4 ref +arm "A5 struct NAME { is a definition" hlx_tag hlxfix/c.c 5 def +arm "A6 struct NAME *local uses the tag, defines nothing" hlx_tag hlxfix/c.c 8 ref +arm "A7 typedef's last declarator is a definition" hlx_key hlxfix/c.c 9 def +arm "A8 a type a typedef only uses is no definition" hlx_key hlxfix/c.c 10 ref +arm "A9 a function-pointer typedef (*NAME) is a definition" hlx_cb hlxfix/c.c 10 def +arm "A10 a name inside a string literal is no definition" hlx_str hlxfix/c.c 11 ref +arm "A11 a C-family signature stays a definition" hlx_fn2 hlxfix/c.c 12 def +arm "A12 a name after a trailing // comment is no definition" hlx_trail hlxfix/c.c 13 ref +arm "A13 '# define NAME' (spaces after #) is a definition" HLX_SPACED hlxfix/c.c 15 def +arm "A14 a Python '# def …' comment is no definition" hlx_pycomment hlxfix/p.py 1 ref +arm "A15 class NAME(Base): is a definition" HlxPy hlxfix/p.py 2 def +arm "A16 def NAME( is a definition" hlx_run hlxfix/p.py 3 def +arm "A17 a parameter after def is no definition" hlx_param hlxfix/p.py 3 ref +arm "A18 a docstring line is no definition" HlxDoc hlxfix/p.py 4 ref +arm "A19 a name after a trailing # comment is no definition" HlxHashTrail hlxfix/p.py 5 ref +arm "A20 a Rust #[attr] line still opens a definition" hlx_hot hlxfix/r.rs 1 def +arm "A21 impl Trait for NAME implements, defines nothing" HlxType hlxfix/r.rs 2 ref +arm "A22 a Go method after its receiver is a definition" HlxMethod hlxfix/g.go 1 def +arm "A23 a Go receiver type is no definition" HlxRecv hlxfix/g.go 1 ref +arm "A24 class NAME extends … is a definition" HlxClass hlxfix/t.ts 1 def +arm "A25 the extended base is no definition" HlxBase hlxfix/t.ts 1 ref +arm "A26 an imported type is no definition" HlxImported hlxfix/t.ts 2 ref +arm "A26b throw new NAME() is a call, no definition" HlxThrown hlxfix/t.ts 3 ref +arm "A19b yield NAME( … ) is a call, no definition" HlxYield hlxfix/p.py 6 ref +arm "A27 a using-declaration is no definition" hlx_using hlxfix/u.cpp 1 ref +arm "A28 a using alias is a definition" HlxAlias hlxfix/u.cpp 2 def +arm "A29 using namespace NAME is no definition" hlx_ns hlxfix/u.cpp 3 ref +arm "A30 class EXPORT_MACRO NAME { is a definition" HlxExported hlxfix/u.cpp 4 def +arm "A31 template<…> RetType NAME( is a definition" hlx_tpl hlxfix/u.cpp 5 def +arm "A32 a name in markdown backticks is no definition" HlxTick hlxfix/d.md 1 ref +arm "A33 a markdown bullet '- (x) NAME' is no ObjC method" hlxBullet hlxfix/d.md 2 ref +arm "A34 an ObjC method '- (ret) NAME' is a definition" hlxObjc hlxfix/m.m 1 def + +echo "── (A) head_defs ──" +o="$( wh --whereis=hlxComment )" +printf '%s' "$o" | grep -q 'head_labels="lexical" head_defs="0"' && ok "H1 no index def of the name → head_labels=lexical head_defs=0" \ + || no "H1 head_defs=0 missing: $( printf '%s' "$o" | grep -o ']*>' )" +o="$( wh --whereis=hlx_indexed )" +if printf '%s' "$o" | grep -q 'head_labels="index"' && ! printf '%s' "$o" | grep -q 'head_defs='; then + ok "H2 an indexed def → head_labels=index, no head_defs" +else no "H2 indexed name: $( printf '%s' "$o" | grep -o ']*>' )"; fi +[ "$( printf '%s' "$o" | kind_at keep.c 3 )" = ref ] && ok "H3 the index labels a comment mention of an indexed name ref" || no "H3 keep.c:3 not ref" +o="$( wh --whereis=hlxComment )" +printf '%s' "$o" | grep -q 'head_defs=0: ' && ok "H4 the default (compact) legend defines head_defs" || no "H4 head_defs undefined in the legend" +o="$( wh --whereis=hlxComment --legend=full )" +printf '%s' "$o" | grep -q 'head_defs="0" beside it' && ok "H5 the full legend defines head_defs" || no "H5 head_defs undefined in the full legend" + +echo "── (B) grep: regex-shaped literals ──" +cat > "$R/b.c" <<'EOF' +int hlxa = 1; +int hlxb = hlxa + hlxa; +/* see hlxa.c and java.util.* || here */ +EOF +printf 'hlxq hlxq\n' > "$R/q.scm" +g() { ( cd "$R" && "$BIN" "$R" --no-cache "$@" 2>/dev/null ); } +root() { grep -o ']*>' | head -1; } +o="$( g --grep='hlxa|hlxb' | root )" +printf '%s' "$o" | grep -q 'pattern_meta="|"' && ok "B1 a|b literal → pattern_meta=|" || no "B1 $o" +printf '%s' "$o" | grep -q "next=\"--regex='hlxa|hlxb'\"" && ok "B2 its next= is the regex verb on the pattern" || no "B2 $o" +o="$( g --regex='hlxa|hlxb' | root )" +printf '%s' "$o" | grep -q 'pattern_meta=' && no "B3 a regex answer carries pattern_meta: $o" || ok "B3 a --regex answer carries no pattern_meta" +printf '%s' "$o" | grep -q 'hits="2"' && ok "B4 the named regex finds the two lines" || no "B4 $o" +o="$( g --grep='hlxa.c' | root )" +printf '%s' "$o" | grep -q 'pattern_meta=' && no "B5 a lone literal dot is meta: $o" || ok "B5 a lone '.' (hlxa.c) is a literal: no pattern_meta" +printf '%s' "$o" | grep -q 'next="--at=' && ok "B6 …and keeps the --at next=" || no "B6 $o" +o="$( g --grep='hlxa\.c' | root )" +printf '%s' "$o" | grep -q 'pattern_meta="\\"' && ok "B7 a backslash escape → pattern_meta=\\" || no "B7 $o" +o="$( g --grep='||' | root )" +printf '%s' "$o" | grep -q 'pattern_meta=' && no "B8 '||' read as alternation: $o" || ok "B8 '||' (an operator) is no alternation" +o="$( g --grep='java.util.*' | root )" +printf '%s' "$o" | grep -q 'pattern_meta=' && no "B9 a trailing .* read as meta: $o" || ok "B9 a trailing .* (java.util.*) stays literal" +o="$( g --grep='hlxa.*hlxa' | root )" +printf '%s' "$o" | grep -q 'pattern_meta=".*"' && ok "B10 .* inside the pattern → pattern_meta=.*" || no "B10 $o" +o="$( g --grep='hlxa\|hlxb' | root )" +printf '%s' "$o" | grep -q "next=\"--regex='hlxa|hlxb'\"" && ok "B11 basic-grep a\\|b → next= the ERE a|b" || no "B11 $o" +o="$( g --grep='[^h]lxa' | root )" +printf '%s' "$o" | grep -q 'pattern_meta="\[\]"' && ok "B12 a [^…] class → pattern_meta=[]" || no "B12 $o" +o="$( g --grep='a[0]' | root )" +printf '%s' "$o" | grep -q 'pattern_meta=' && no "B13 an index a[0] read as a class: $o" || ok "B13 a[0] (an index) is no bracket class" +o="$( g --grep='^hlxb' | root )" +printf '%s' "$o" | grep -q 'pattern_meta="^\$"' && ok "B14 a leading ^ → pattern_meta=^\$" || no "B14 $o" +o="$( g --grep='hlxa|hlxb' --legend=full )" +printf '%s' "$o" | grep -q 'pattern_meta= (present only' && ok "B15 the full legend defines pattern_meta= when it rides" || no "B15 legend clause missing" +o="$( g --grep='hlxa' --legend=full )" +printf '%s' "$o" | grep -q 'pattern_meta= (present only' && no "B16 the pattern_meta clause rides without the attribute" || ok "B16 the clause is present-only" + +echo "── (B) grep: one row per line, occ= ──" +o="$( g --grep='hlxa' --grep-in=any )" +printf '%s' "$o" | root | grep -q 'hits="3"' && ok "B17 hits= counts LINES (3 lines hold hlxa, 4 occurrences)" || no "B17 $( printf '%s' "$o" | root )" +printf '%s' "$o" | grep -q ']* occ="2"' && ok "B18 the line holding hlxa twice is ONE row with occ=2" || no "B18 no occ=2 row" +[ "$( printf '%s' "$o" | grep -o ']* occ=' && no "B20 a single occurrence carries occ=" || ok "B20 a single occurrence carries no occ=" +g --grep='hlxa' --grep-in=any --legend=full | grep -q 'occ= on a row' && ok "B21 the full legend defines occ= when it rides" || no "B21 occ clause missing" +o="$( g --grep='hlxq' )" +printf '%s' "$o" | grep -q '/dev/null | tail -1 | python3 -c 'import sys,json; r=json.load(sys.stdin); print(r["result"]["content"][0]["text"] if "result" in r else "__ERROR__")'; } +j="$( mcp 'hlxa|hlxb' )" +printf '%s' "$j" | python3 -c 'import sys,json; d=json.load(sys.stdin); sys.exit(0 if d.get("pattern_meta")=="|" and "--regex=" in d.get("next","") else 1)' \ + && ok "M1 MCP grep: pattern_meta | and next= the regex verb" || no "M1 MCP: $( printf '%s' "$j" | head -c 300 )" +j="$( mcp 'hlxa' )" +printf '%s' "$j" | python3 -c 'import sys,json; d=json.load(sys.stdin); h=[x for x in d["hits"] if x["line"]==2]; sys.exit(0 if len(h)==1 and h[0].get("occ")==2 and d.get("total")==3 else 1)' \ + && ok "M2 MCP grep: one row per line, occ 2, total 3 (CLI parity)" || no "M2 MCP: $( printf '%s' "$j" | head -c 400 )" +printf '%s' "$j" | python3 -c 'import sys,json; d=json.load(sys.stdin); sys.exit(1 if "pattern_meta" in d else 0)' \ + && ok "M3 MCP grep: a plain literal carries no pattern_meta" || no "M3 plain literal has pattern_meta" + +[ "$fail" -eq 0 ] && echo "honestlexcheck: ALL PASS" || echo "honestlexcheck: FAILURES" +exit "$fail" diff --git a/test/regression.sh b/test/regression.sh index 40bff09ee..a32da9ae7 100755 --- a/test/regression.sh +++ b/test/regression.sh @@ -278,7 +278,7 @@ else fi # retired: forspinecheck — the --for caller spine (Change A) was dropped by its own offline kill rule: a spine of up to 2 caller hops from the top 3 seeds named a missed rubric item on 1 of 7 in-sample rows, so the gate that pinned it went with it # retired: cacheexclkeycheck — the per-configuration auto-cache key it pinned is a registered NEGATIVE (docs/EVALS.md, "The auto-cache key ignores --exclude", RUN 2026-09-03: a 158K-file root with >= 12 gate configurations thrashed the 2 GiB sweep); the retry design keeps ONE superset blob per root and will bring its own gate -for _g in a9disclosurecheck abicheck accessshapecheck ackonlycheck adaptivecheck adaptivecutshapecheck affectedcheck agentloopclaudecheck agentloopcodexcheck agentloopeditsuitecheck agentloopfollowupcheck agentloopgradercheck agentlooplockcheck agentloopopencodecheck agentsurfacecheck agenttablecheck aiderbytescheck anchorbodycheck anchorcheck archcheck archmetricscheck argvdiffcheck arisefollowupcheck ariseshimcheck aritycheck artifactcheck astqueryregexcheck astrocheck atcheck atomscheck attribmultidefcheck attrvocabcheck baselinecheck baselinedirtycheck baselineportcheck bashsourcecheck batchcheck binoverridecheck blindspotcheck bm25boundcheck bm25check bodiesshowncheck bodydialectcheck budgetpolicycheck buildtypestampcheck builtinbindcheck bundleidcheck cachefuzzcheck cachehashcheck cacheidentitycheck cacheisolationcheck cachelintcheck cacheoffsetcheck cachereservecheck cachesplitcheck callerscheck callformcheck callsrankordercheck candheadcheck candidatescheck canoncheck capdisclosurecheck capsweepcheck ccheck ccjsoncheck ceilingverdictcheck chacheck chaconecheck chainguardcheck chainidcheck childwalkscalecheck churndecaycheck churnjoincheck churnjsonstampcheck claudeconfigdircheck clicheck clojurecheck clonebandcheck clonecachecheck clonededupcheck cloneidiomcheck clonelexcheck clsrecvcheck cochangeboostcheck cochangecliocheck cochangesurprisecheck codexinstallhonestycheck codexplugincheck codexwrapcheck collectioncapcheck columnarattrcheck columnarcheck columnarcommacheck commentcoherencecheck commenttokencheck communitydrillcheck communitylabelcheck compactlegendcheck compactroutecheck completecheck composelangcheck connectcheck connectcorecheck connectjoincheck constcheck contextratiocheck coplintcheck countfloorcheck cppbenchcheck cppoperatorcheck cppqualcheck cpptmplscopecheck crashsweepcheck crawlescapecheck crossdirincludecheck crossrefcheck crossrefdegradecheck csharpcheck csharpcondcheck cudacheck cyclecutcheck danglingwarncheck dartcheck deadcheck deadfiltercheck deadprecisioncheck deckcheck deckclaimcheck declinecheck declinedlistcheck declinedsitecheck decltodefcheck deeptailcheck defaultceilingcheck defectshapecheck defoverdeclcheck degradedhintcheck dependencypincheck deplangscheck depsprecisecheck detailcheck diagnoticecheck didyoumeancheck dispatchordercheck dmmcheck docanchorcheck docdemotecheck docdriftcheck docdriftcommentcheck docmdcachecheck docmentioncheck docscommandscheck doctorcheck donelegendcheck droppedpositivecheck duprowcheck dynmapsimdcheck editcheckanswercheck editcheckcheck editcheckdeclcheck editchecknotecheck edithandlehintcheck editpayloadbinarycheck editplancheck editplanpayloadconfinecheck editplanrecheckcheck editplanrollbackmsgcheck editpreviewcheck editroundtripcheck edittargetfileabscheck eliximportcheck elixircheck elixirnamearitycheck elixirsemanticcheck emitescapecheck emittertruthcheck emptycorpuscheck emptyvaluerefusecheck ensembleavailcheck ensemblecheck enumtablecheck essentialcxcheck estchargecheck evalcheck evictioncheck exemplarcheck exemplarconfcheck exercisescheck expandbodyfirstcheck expandcallscheck expandmodecheck expandrangecheck expandsibscheck expandtokencheck expandtopk0check extentcheck externalvetocheck falseedgecheck fcallforwardcheck fficheck fieldaffinitycheck fieldidcheck fieldnarrowcheck fieldusescheck filerootcheck fileselectorrefusecheck fillordercheck fixedbufsweep flagscheck flagsnoisecheck flagsurfacecheck flagtablecheck flipcheck floormarkcheck fnliteralcheck fnptrcheck forautobodycheck forblowupcheck forbodyfoldcheck forbodyslotcheck forbudgetmonotoncheck forcalibfactscheck forcompresscheck fordisclosurecheck forhdrshapecheck forheadrrcheck forlenscheck formatgatecheck formaxtokenscheck formembernamescheck fornotesbudgetcheck fornotesjsoncheck forrankordercheck forrootlegendcheck forsectioncollapsecheck forsigspancheck forsoftbudgetcheck forwidencheck freshclonecheck freshnesscheck g1configcheck gateabilitycheck gatecountcheck gateexitcheck gdscriptcheck genrecallcheck gitchildoptscheck gitenvhermeticcheck githardencheck gitignorecheck gitquotepathcheck gitstampcheck goinstcheck gointerfacecheck gokindcheck goscopecheck graphlegendbudgetcheck graphqueryrefusecheck grepanchorcheck grepandcheck grepbytescheck grepcheck grepcontextcheck grepcorpuscheck grepfastcheck grepfollowupcheck grepignorecheck grepscancheck grepseamcheck greptiercheck guardmsgcheck hasacheck hazardpatterncheck headbinstagecheck headsnapcachecheck helpbudgetcheck hermesinstallcheck historyoraclecheck hookcheck hostilecheck hotspotsincecheck htmlcolorcheck htmlhostcheck htmlrendercheck identitycheck impactdepthcheck impactimportcheck impactpartitioncheck impacttypecheck importcapcheck importnarrowcheck includeanglecheck includeprecisecheck indexoutcheck infraportcheck isolateprovenancecheck javamethodrefcheck javarubycheck jslangcheck jsmetricscheck jsnestedcheck jsoncheck jsonlangcheck jsonparitycheck jsonredactcheck jsonrefusallegendcheck jsonwalkcheck jsshapecheck jsverbscheck jsxcallcheck knownitemcheck kotlincheck landingcheck langcensuscheck langcheck langidentitycheck layerquerycheck layoutcheck lb3namecheck legendcostcheck legendcoveragecheck legenddriftcheck legendrefcheck legobundlecheck legocheck legotransitivecheck liftdisclosurecheck limitstablecheck lintbudgetcheck lintcatalogcheck lintcheck lintdedupcheck lintpayloadcapcheck lintprecisioncheck lintrulescheck lintscopecheck lintselectcheck listingpagingcheck localdeclcheck localitycheck localscountcheck loopconservationcheck lpincheck luacheck luarequirecheck macroedgecheck macroreparsecheck manifestcheck mapdatasectioncheck mapdiffcheck mapinventorycheck matchcapturecheck matchgrammarcheck maxfilesizecheck mcpattrparitycheck mcpaudit4hardencheck mcpclidiffcheck mcpcodexmetacheck mcpcontractcheck mcpdegradedhintcheck mcpeditcheck mcpeditkindcheck mcpeditmodecheck mcpeditpresencecheck mcpeditracecheck mcpflagshipcheck mcpforparitycheck mcpframehonestycheck mcpgrepdegradedcheck mcphandlecheck mcpincrementalcheck mcpmanifestcheck mcprangeedgecheck mcpreadloopcheck mcpredactcheck mcpreloadasancheck mcpreloadcheck mcpremotecheck mcprobustcheck mcpslicecheck mcpstalecheck mcpstdiolinecapcheck mcpstrictschemacheck mcptoolprunecheck mcptoolsubsetcheck mcptranchecheck mcptwinclaimscheck mcpverbscheck mcpw2fixcheck mcpw3fixcheck mcpwatchercheck mdembedcheck mdsectioncheck memguardcheck mentioncapcheck mentioncheck mentionsverbcheck mergechurncheck mergescoutcheck mergescoutlonglinecheck metalcheck meterdisclosurecheck metricscheck modifierguardcheck moduleconstcheck morecontractcheck mrowalkcheck multirootcheck multiswecheck namedfileinputcheck nameinfocheck namingcalibrationcheck namingconsistencycheck naminglenscheck naminglocalscheck narrowcheck narrowlangcheck neighbourcapcheck nestedimportcheck nestedqualcheck nestprofilecheck nextverbcheck noaliascheck nodekindcheck nongitqmetricscheck nonlocalstatecheck notecanoncheck notescheck notesdegradecheck nsfiltercheck nulbytecheck numericrefusecheck objcfieldcheck objcsniffcheck opencodewrapcheck optremarkscheck optremarkshotcheck ordercheck osswitchcheck oswin32logiccheck outlinecheck overbudgetcommentcheck ownerhopcheck ownerscheck packcallersharecheck packtaskcheck packtaskmonotoncheck packtaskquotacheck padscalecheck paginationcheck pagingsweepcheck panellegendcheck pargatescheck parsehealthcheck partitioncheck pathgapcheck pathownercheck patterncheck perfharnesscheck phpcheck pincensuscheck planlanescheck planlintcheck pmccheck portablebuildcheck portablecachecheck postingscheck ppaltcheck ppdeadrolescheck pranchorcheck prbudgetcheck prcheck prcontextcheck prconvergecheck precedencecheck preproccondcheck preprocdeadscalecheck prmaskanchorcheck prnestedcapcheck probecheck propcostcheck prrefsafecheck prrenamecheck pyimportprecisecheck pymodulealiascheck pyshapecheck qackconcurrencycheck qackorigincheck qbaselineproducercheck qchurncheck qchurnmemocheck qddegradecheck qddialscheck qdrefpaircheck qextractionkeycheck qoriginoraclecheck qrevtokencheck qrowlocatorcheck qschemetripcheck qsnapcachecheck qsnapprefetchcheck qsnapproducercheck qualifiedresolvecheck qualitycheck qualitycrosslangcheck qualityexcludecheck qualitykeycheck qualitykindscheck qualityorigincheck qualitypanelcheck qualityscopecheck qualitysignalcheck qualitystalecheck qualitysymcheck qualnewcheck querycheck queryfilescancheck racymtimecheck radixsimdcheck rangecomposecheck rankbycheck reachcheck readabilitycheck readmedriftcheck readmeexamplecheck recallanchorcheck recallboundarycheck recallbudgetcheck recallbufcheck recallevalcheck recallparitycheck recallpassagecheck recallrankdepthcheck recallrelcheck recallshapecheck recalltablecheck recalltotalcheck receiptpostcheck receiverevidencecheck recentscopecheck redactcheck redactfixcheck refusaltailcheck regexbombcheck regexcheck regexguardcheck regexrefusecheck registermacrocheck relevancefloorcheck relinkcheck reportcheck resolvecheck resolverhonestycheck retrievalqualitycheck reusefirstworkflowcheck ripwirepubliccheck rootrelcheck rootrelemitcheck rootspellingcheck routecheck routeedgecheck routehookcheck routeoncecheck routingreportcheck rubyargcheck rubyattrscheck rubybarecallcheck rubyclassrecvcheck rubyconstcheck rubydeclrefcheck rubydescribedclasscheck rubyinheritcheck rubymetricscheck rubyrakejbuildercheck rubyreachcheck rubyrecvcheck rubyrecvnarrowcheck rubyrequirecheck rubyrspectargetcheck rubyschemacheck rubyscopecheck rubysettercheck rubytypedrecvcheck runhintcheck runtracecheck rustanccheck rustimportprecisecheck rustqualcheck safedeletecheck sarifcheck savecachecheck scipcheck scipjoincheck scorecardcheck scoutheadconflictcheck scoutkeycheck scroundtripcheck seedboundscheck selectorchaincheck selectorhonestycheck selectorrefusecheck selectorscopecheck selfcheckcheck selfcontainedcheck shadowcheck shallowhistorycheck shapingflagcheck shellgateindexcheck showcasecapturecheck sibliftcheck sidecarsymlinkcheck sigredactcheck sincecheck sincecochangecheck sincewindowcheck singledefcheck situdiffcheck situshapecheck skilldescbudgetcheck skillevalcheck skillevalsplitcheck skillinstallcheck skillroutingjudgedcheck skillscanreadcheck skilltruthcheck skipclassifycheck skippedcheck skipreasoncheck slicecheck slicediffcheck sliceflowcheck sliceflowsenscheck slotjoincheck spectimingcheck staleackcheck statgatecheck stdmembergatecheck stdqualcheck strkerncheck structlayoutcheck sublistcountcheck subscriptopcheck substrfiltercheck subtokencheck svectorcheck swiftcheck swiftmemberscheck swiftshapecheck taskechocheck tempfilesymlinkcheck termmargincheck testedreachcheck testgatecheck testgatelegendbudgetcheck testgatepagecheck testgaterefusecheck testmacrocheck testrowruncheck testscopecheck textdocscheck timsortcheck tokenbudgetcheck tomllangcheck toolcallroutecheck tornreadcheck traceasanlinearcheck tracecheck tracehandoffcapcheck tracehopcheck traceminecheck treecheck truncvocabcheck tsimportprecisecheck tsshapecheck type3check type3clonecheck typerefcheck unreachablecheck unresolvedcheck usescheck usesliteralcheck usesselectorcheck usingdeclcheck utf8scrubcheck vendoredassetcheck vendoredbundlecheck vendorpatchcheck verifycheck versioncheck w2verbscheck w3fixbudgetcheck w3fixlegendcheck weaksignalcheck withgraphcheck withprofilecheck worktreeleakcheck wrapverbscheck writetargetcheck xmlwellformed yamllangcheck zonecheck zoneconsistencycheck zoomcheck; do +for _g in a9disclosurecheck abicheck accessshapecheck ackonlycheck adaptivecheck adaptivecutshapecheck affectedcheck agentloopclaudecheck agentloopcodexcheck agentloopeditsuitecheck agentloopfollowupcheck agentloopgradercheck agentlooplockcheck agentloopopencodecheck agentsurfacecheck agenttablecheck aiderbytescheck anchorbodycheck anchorcheck archcheck archmetricscheck argvdiffcheck arisefollowupcheck ariseshimcheck aritycheck artifactcheck astqueryregexcheck astrocheck atcheck atomscheck attribmultidefcheck attrvocabcheck baselinecheck baselinedirtycheck baselineportcheck bashsourcecheck batchcheck binoverridecheck blindspotcheck bm25boundcheck bm25check bodiesshowncheck bodydialectcheck budgetpolicycheck buildtypestampcheck builtinbindcheck bundleidcheck cachefuzzcheck cachehashcheck cacheidentitycheck cacheisolationcheck cachelintcheck cacheoffsetcheck cachereservecheck cachesplitcheck callerscheck callformcheck callsrankordercheck candheadcheck candidatescheck canoncheck capdisclosurecheck capsweepcheck ccheck ccjsoncheck ceilingverdictcheck chacheck chaconecheck chainguardcheck chainidcheck childwalkscalecheck churndecaycheck churnjoincheck churnjsonstampcheck claudeconfigdircheck clicheck clojurecheck clonebandcheck clonecachecheck clonededupcheck cloneidiomcheck clonelexcheck clsrecvcheck cochangeboostcheck cochangecliocheck cochangesurprisecheck codexinstallhonestycheck codexplugincheck codexwrapcheck collectioncapcheck columnarattrcheck columnarcheck columnarcommacheck commentcoherencecheck commenttokencheck communitydrillcheck communitylabelcheck compactlegendcheck compactroutecheck completecheck composelangcheck connectcheck connectcorecheck connectjoincheck constcheck contextratiocheck coplintcheck countfloorcheck cppbenchcheck cppoperatorcheck cppqualcheck cpptmplscopecheck crashsweepcheck crawlescapecheck crossdirincludecheck crossrefcheck crossrefdegradecheck csharpcheck csharpcondcheck cudacheck cyclecutcheck danglingwarncheck dartcheck deadcheck deadfiltercheck deadprecisioncheck deckcheck deckclaimcheck declinecheck declinedlistcheck declinedsitecheck decltodefcheck deeptailcheck defaultceilingcheck defectshapecheck defoverdeclcheck degradedhintcheck dependencypincheck deplangscheck depsprecisecheck detailcheck diagnoticecheck didyoumeancheck dispatchordercheck dmmcheck docanchorcheck docdemotecheck docdriftcheck docdriftcommentcheck docmdcachecheck docmentioncheck docscommandscheck doctorcheck donelegendcheck droppedpositivecheck duprowcheck dynmapsimdcheck editcheckanswercheck editcheckcheck editcheckdeclcheck editchecknotecheck edithandlehintcheck editpayloadbinarycheck editplancheck editplanpayloadconfinecheck editplanrecheckcheck editplanrollbackmsgcheck editpreviewcheck editroundtripcheck edittargetfileabscheck eliximportcheck elixircheck elixirnamearitycheck elixirsemanticcheck emitescapecheck emittertruthcheck emptycorpuscheck emptyvaluerefusecheck ensembleavailcheck ensemblecheck enumtablecheck essentialcxcheck estchargecheck evalcheck evictioncheck exemplarcheck exemplarconfcheck exercisescheck expandbodyfirstcheck expandcallscheck expandmodecheck expandrangecheck expandsibscheck expandtokencheck expandtopk0check extentcheck externalvetocheck falseedgecheck fcallforwardcheck fficheck fieldaffinitycheck fieldidcheck fieldnarrowcheck fieldusescheck filerootcheck fileselectorrefusecheck fillordercheck fixedbufsweep flagscheck flagsnoisecheck flagsurfacecheck flagtablecheck flipcheck floormarkcheck fnliteralcheck fnptrcheck forautobodycheck forblowupcheck forbodyfoldcheck forbodyslotcheck forbudgetmonotoncheck forcalibfactscheck forcompresscheck fordisclosurecheck forhdrshapecheck forheadrrcheck forlenscheck formatgatecheck formaxtokenscheck formembernamescheck fornotesbudgetcheck fornotesjsoncheck forrankordercheck forrootlegendcheck forsectioncollapsecheck forsigspancheck forsoftbudgetcheck forwidencheck freshclonecheck freshnesscheck g1configcheck gateabilitycheck gatecountcheck gateexitcheck gdscriptcheck genrecallcheck gitchildoptscheck gitenvhermeticcheck githardencheck gitignorecheck gitquotepathcheck gitstampcheck goinstcheck gointerfacecheck gokindcheck goscopecheck graphlegendbudgetcheck graphqueryrefusecheck grepanchorcheck grepandcheck grepbytescheck grepcheck grepcontextcheck grepcorpuscheck grepfastcheck grepfollowupcheck grepignorecheck grepscancheck grepseamcheck greptiercheck guardmsgcheck hasacheck hazardpatterncheck headbinstagecheck headsnapcachecheck helpbudgetcheck hermesinstallcheck historyoraclecheck honestlexcheck hookcheck hostilecheck hotspotsincecheck htmlcolorcheck htmlhostcheck htmlrendercheck identitycheck impactdepthcheck impactimportcheck impactpartitioncheck impacttypecheck importcapcheck importnarrowcheck includeanglecheck includeprecisecheck indexoutcheck infraportcheck isolateprovenancecheck javamethodrefcheck javarubycheck jslangcheck jsmetricscheck jsnestedcheck jsoncheck jsonlangcheck jsonparitycheck jsonredactcheck jsonrefusallegendcheck jsonwalkcheck jsshapecheck jsverbscheck jsxcallcheck knownitemcheck kotlincheck landingcheck langcensuscheck langcheck langidentitycheck layerquerycheck layoutcheck lb3namecheck legendcostcheck legendcoveragecheck legenddriftcheck legendrefcheck legobundlecheck legocheck legotransitivecheck liftdisclosurecheck limitstablecheck lintbudgetcheck lintcatalogcheck lintcheck lintdedupcheck lintpayloadcapcheck lintprecisioncheck lintrulescheck lintscopecheck lintselectcheck listingpagingcheck localdeclcheck localitycheck localscountcheck loopconservationcheck lpincheck luacheck luarequirecheck macroedgecheck macroreparsecheck manifestcheck mapdatasectioncheck mapdiffcheck mapinventorycheck matchcapturecheck matchgrammarcheck maxfilesizecheck mcpattrparitycheck mcpaudit4hardencheck mcpclidiffcheck mcpcodexmetacheck mcpcontractcheck mcpdegradedhintcheck mcpeditcheck mcpeditkindcheck mcpeditmodecheck mcpeditpresencecheck mcpeditracecheck mcpflagshipcheck mcpforparitycheck mcpframehonestycheck mcpgrepdegradedcheck mcphandlecheck mcpincrementalcheck mcpmanifestcheck mcprangeedgecheck mcpreadloopcheck mcpredactcheck mcpreloadasancheck mcpreloadcheck mcpremotecheck mcprobustcheck mcpslicecheck mcpstalecheck mcpstdiolinecapcheck mcpstrictschemacheck mcptoolprunecheck mcptoolsubsetcheck mcptranchecheck mcptwinclaimscheck mcpverbscheck mcpw2fixcheck mcpw3fixcheck mcpwatchercheck mdembedcheck mdsectioncheck memguardcheck mentioncapcheck mentioncheck mentionsverbcheck mergechurncheck mergescoutcheck mergescoutlonglinecheck metalcheck meterdisclosurecheck metricscheck modifierguardcheck moduleconstcheck morecontractcheck mrowalkcheck multirootcheck multiswecheck namedfileinputcheck nameinfocheck namingcalibrationcheck namingconsistencycheck naminglenscheck naminglocalscheck narrowcheck narrowlangcheck neighbourcapcheck nestedimportcheck nestedqualcheck nestprofilecheck nextverbcheck noaliascheck nodekindcheck nongitqmetricscheck nonlocalstatecheck notecanoncheck notescheck notesdegradecheck nsfiltercheck nulbytecheck numericrefusecheck objcfieldcheck objcsniffcheck opencodewrapcheck optremarkscheck optremarkshotcheck ordercheck osswitchcheck oswin32logiccheck outlinecheck overbudgetcommentcheck ownerhopcheck ownerscheck packcallersharecheck packtaskcheck packtaskmonotoncheck packtaskquotacheck padscalecheck paginationcheck pagingsweepcheck panellegendcheck pargatescheck parsehealthcheck partitioncheck pathgapcheck pathownercheck patterncheck perfharnesscheck phpcheck pincensuscheck planlanescheck planlintcheck pmccheck portablebuildcheck portablecachecheck postingscheck ppaltcheck ppdeadrolescheck pranchorcheck prbudgetcheck prcheck prcontextcheck prconvergecheck precedencecheck preproccondcheck preprocdeadscalecheck prmaskanchorcheck prnestedcapcheck probecheck propcostcheck prrefsafecheck prrenamecheck pyimportprecisecheck pymodulealiascheck pyshapecheck qackconcurrencycheck qackorigincheck qbaselineproducercheck qchurncheck qchurnmemocheck qddegradecheck qddialscheck qdrefpaircheck qextractionkeycheck qoriginoraclecheck qrevtokencheck qrowlocatorcheck qschemetripcheck qsnapcachecheck qsnapprefetchcheck qsnapproducercheck qualifiedresolvecheck qualitycheck qualitycrosslangcheck qualityexcludecheck qualitykeycheck qualitykindscheck qualityorigincheck qualitypanelcheck qualityscopecheck qualitysignalcheck qualitystalecheck qualitysymcheck qualnewcheck querycheck queryfilescancheck racymtimecheck radixsimdcheck rangecomposecheck rankbycheck reachcheck readabilitycheck readmedriftcheck readmeexamplecheck recallanchorcheck recallboundarycheck recallbudgetcheck recallbufcheck recallevalcheck recallparitycheck recallpassagecheck recallrankdepthcheck recallrelcheck recallshapecheck recalltablecheck recalltotalcheck receiptpostcheck receiverevidencecheck recentscopecheck redactcheck redactfixcheck refusaltailcheck regexbombcheck regexcheck regexguardcheck regexrefusecheck registermacrocheck relevancefloorcheck relinkcheck reportcheck resolvecheck resolverhonestycheck retrievalqualitycheck reusefirstworkflowcheck ripwirepubliccheck rootrelcheck rootrelemitcheck rootspellingcheck routecheck routeedgecheck routehookcheck routeoncecheck routingreportcheck rubyargcheck rubyattrscheck rubybarecallcheck rubyclassrecvcheck rubyconstcheck rubydeclrefcheck rubydescribedclasscheck rubyinheritcheck rubymetricscheck rubyrakejbuildercheck rubyreachcheck rubyrecvcheck rubyrecvnarrowcheck rubyrequirecheck rubyrspectargetcheck rubyschemacheck rubyscopecheck rubysettercheck rubytypedrecvcheck runhintcheck runtracecheck rustanccheck rustimportprecisecheck rustqualcheck safedeletecheck sarifcheck savecachecheck scipcheck scipjoincheck scorecardcheck scoutheadconflictcheck scoutkeycheck scroundtripcheck seedboundscheck selectorchaincheck selectorhonestycheck selectorrefusecheck selectorscopecheck selfcheckcheck selfcontainedcheck shadowcheck shallowhistorycheck shapingflagcheck shellgateindexcheck showcasecapturecheck sibliftcheck sidecarsymlinkcheck sigredactcheck sincecheck sincecochangecheck sincewindowcheck singledefcheck situdiffcheck situshapecheck skilldescbudgetcheck skillevalcheck skillevalsplitcheck skillinstallcheck skillroutingjudgedcheck skillscanreadcheck skilltruthcheck skipclassifycheck skippedcheck skipreasoncheck slicecheck slicediffcheck sliceflowcheck sliceflowsenscheck slotjoincheck spectimingcheck staleackcheck statgatecheck stdmembergatecheck stdqualcheck strkerncheck structlayoutcheck sublistcountcheck subscriptopcheck substrfiltercheck subtokencheck svectorcheck swiftcheck swiftmemberscheck swiftshapecheck taskechocheck tempfilesymlinkcheck termmargincheck testedreachcheck testgatecheck testgatelegendbudgetcheck testgatepagecheck testgaterefusecheck testmacrocheck testrowruncheck testscopecheck textdocscheck timsortcheck tokenbudgetcheck tomllangcheck toolcallroutecheck tornreadcheck traceasanlinearcheck tracecheck tracehandoffcapcheck tracehopcheck traceminecheck treecheck truncvocabcheck tsimportprecisecheck tsshapecheck type3check type3clonecheck typerefcheck unreachablecheck unresolvedcheck usescheck usesliteralcheck usesselectorcheck usingdeclcheck utf8scrubcheck vendoredassetcheck vendoredbundlecheck vendorpatchcheck verifycheck versioncheck w2verbscheck w3fixbudgetcheck w3fixlegendcheck weaksignalcheck withgraphcheck withprofilecheck worktreeleakcheck wrapverbscheck writetargetcheck xmlwellformed yamllangcheck zonecheck zoneconsistencycheck zoomcheck; do [ -f "$ROOT/test/$_g.sh" ] || continue if RIPWIRE_BIN="$BIN" bash "$ROOT/test/$_g.sh" >/dev/null 2>&1; then ok "absorb gate ($_g.sh)" From c26318920bbf97bfb8b53662c16fa9337d6037c0 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I <210578314+joyful-ii-V-I@users.noreply.github.com> Date: Fri, 23 Oct 2026 08:32:18 -0400 Subject: [PATCH 36/77] refactor(grep): spell occ= and pattern_meta= through the shared absent-when-empty field helpers (qd duplication) Co-Authored-By: Claude Opus 5.5 --- src/graphlegend.h | 10 ++++++++++ src/mcpverbs.h | 11 ++--------- src/verbs_grep.h | 14 ++------------ 3 files changed, 14 insertions(+), 21 deletions(-) diff --git a/src/graphlegend.h b/src/graphlegend.h index b372ae171..a33444004 100644 --- a/src/graphlegend.h +++ b/src/graphlegend.h @@ -811,6 +811,16 @@ inline std::string countAttrXmlOrEmpty( std::string_view name, std::size_t count { return countFieldOrEmpty( name, count, /*json=*/false ); } +// The same absent-when-empty shape for a TEXT value the caller already escaped for its dialect: ` name="v"` or `,"name":"v"` +// (grep's pattern_meta= and its MCP twin spell through it). +inline std::string textFieldOrEmpty( std::string_view name, std::string_view escaped, bool json ) +{ + if( escaped.empty() ) + { + return {}; + } + return json ? ",\"" + std::string( name ) + "\":\"" + std::string( escaped ) + "\"" : " " + std::string( name ) + "=\"" + std::string( escaped ) + "\""; +} // The attribute and the key, one spelling each, absent at zero like bodyless_defs= and graph_unindexed=. inline std::string declinedCallsAttrXml( std::size_t declinedCalls ) diff --git a/src/mcpverbs.h b/src/mcpverbs.h index 0e917c5f5..6b54d93d1 100644 --- a/src/mcpverbs.h +++ b/src/mcpverbs.h @@ -1221,14 +1221,7 @@ inline std::string grepDegradedNoteJson( const IngestResult& ing, std::span 1 ? ",\"occ\":" + std::to_string( occ ) : std::string(); -} - -// honest-lex GREP-META: the CLI root's pattern_meta= twin (search.h grepPatternMeta) — this verb is literal-only. -inline std::string grepPatternMetaJson( std::string_view pattern ) -{ - const std::string meta = grepPatternMeta( pattern ); - return meta.empty() ? std::string() : ",\"pattern_meta\":\"" + mcpdetail::jsonEscape( meta ) + "\""; + return countFieldOrEmpty( "occ", occ > 1 ? occ : 0, /*json=*/true ); } // The root's next for this literal-only verb: the CLI grepRootNext twin. A regex-shaped pattern → the regex run first @@ -1390,7 +1383,7 @@ inline std::string grepHitsJson( const std::string& root, const std::string& pat // G1: mirrors the CLI — same condition (singleRootJ), same fact, right // after pattern like the CLI's attribute order (mcpclidiffcheck.sh's LENS2 fact-parity). + ( singleRootJ ? ( "\",\"root\":\"" + mcpdetail::jsonEscape( root ) + "\"" ) : "\"" ) - + grepPatternMetaJson( pattern ) + + textFieldOrEmpty( "pattern_meta", mcpdetail::jsonEscape( grepPatternMeta( pattern ) ), /*json=*/true ) // honest-lex + ",\"files\":" + std::to_string( filesMatched ) + ( isPaging ? std::string{} : ( ",\"total\":" + std::to_string( collected.raw.size() ) ) ) + pageDisclosure( pagebuf, sizeof( pagebuf ), rowCount, collected.raw.size(), grepPage.end, diff --git a/src/verbs_grep.h b/src/verbs_grep.h index 048e1bc37..921cd7f68 100644 --- a/src/verbs_grep.h +++ b/src/verbs_grep.h @@ -402,24 +402,14 @@ std::string grepUnindexedAttrs( const rw::GrepAuxCollection& aux ) // honest-lex, occ= (the pattern occurs N times on this one line; absent = once). std::string grepRowTailAttrs( std::uint32_t lineBytes, std::uint32_t occ ) { - std::string attrs; - if( lineBytes != 0 ) - { - attrs += " line_bytes=\"" + std::to_string( lineBytes ) + "\""; - } - if( occ > 1 ) - { - attrs += " occ=\"" + std::to_string( occ ) + "\""; - } - return attrs; + return rw::countAttrXmlOrEmpty( "line_bytes", lineBytes ) + rw::countAttrXmlOrEmpty( "occ", occ > 1 ? occ : 0 ); } // honest-lex GREP-META: pattern_meta= on a LITERAL answer whose pattern reads as regex syntax (search.h grepPatternMeta // states the rule); "" on a regex answer or a plain literal. std::string grepMetaAttr( const rw::Config& cfg, std::string_view pat, std::vector& esc ) { - const std::string meta = cfg.grepRegex ? std::string() : rw::grepPatternMeta( pat ); - return meta.empty() ? std::string() : " pattern_meta=\"" + std::string( rw::escapeXml( meta, esc ) ) + "\""; + return rw::textFieldOrEmpty( "pattern_meta", cfg.grepRegex ? std::string_view() : rw::escapeXml( rw::grepPatternMeta( pat ), esc ), /*json=*/false ); } // The present-only legend clauses for honest-lex's two attributes (full legend; the compact one leans on the dict). From 5f898263c2f43d2516bf5347ed8287e6d8b6eb76 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I <210578314+joyful-ii-V-I@users.noreply.github.com> Date: Fri, 23 Oct 2026 09:15:52 -0400 Subject: [PATCH 37/77] test(hop1): hop1check gate; #11 rule requires every same-named class to type the field in-tree hop1check.sh: 45 arms over the slot bridge (3-slot chain, cap boundary, mixed chains, width 16/17, slot-name, position-breaker, cast, same field name in another struct), the #11 declined rule (this/param positives, string/local/field negatives, same-named classes, 17 candidates, near_sink count), the callers next=, the impact ring (XML, JSON, columnar, row cap, proven exclusion) and both MCP twins and legends. DeclinedCrossRule now requires EVERY class the receiver may be an instance of to state the field as an in-tree class (a class stating nothing no longer lets another same-named class decide). Registers the gate (regression.sh, shard weights, gate counts 704) and adds the CHANGELOG entries. Co-Authored-By: Claude Opus 5.5 --- .github/pargates-shard-weights.json | 1 + CHANGELOG.md | 28 ++ README.md | 4 +- docs/EVALS.md | 6 +- present/deck5_ripwire_build.js | 6 +- src/backhops.h | 25 +- test/hop1check.sh | 402 ++++++++++++++++++++++++++++ test/regression.sh | 2 +- 8 files changed, 449 insertions(+), 25 deletions(-) create mode 100755 test/hop1check.sh diff --git a/.github/pargates-shard-weights.json b/.github/pargates-shard-weights.json index 13638b79f..9e24a797b 100644 --- a/.github/pargates-shard-weights.json +++ b/.github/pargates-shard-weights.json @@ -242,6 +242,7 @@ "headsnapcachecheck.sh": 19.9, "historyoraclecheck.sh": 6.9, "hookcheck.sh": 78.0, +"hop1check.sh": 30.0, "hostilecheck.sh": 6.2, "hotspotsincecheck.sh": 8.6, "htmlcolorcheck.sh": 12.4, diff --git a/CHANGELOG.md b/CHANGELOG.md index 3b244c95e..3dcf61db0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,34 @@ not published here — see `docs/EVALS.md` for the instruments behind the headli ## [Unreleased] +### Added — one backward hop step for `--path` and `--impact` (slot and declined callers) + +`--callers=X` already names, one hop per call, who MAY call X without a graph edge: a `` row's +`called_by=` (a call through a struct field X is stored in) and a `` row (a declined call whose candidate list holds +X). The answers that join hops now read the same rows, from one provider (`src/backhops.h`): + +- `--path`'s hedged `` crosses a slot hop (`via="slot"`, `via="slot-name"` for a field-name-only join) while the + join names at most 16 calling functions (`slot_callers=`); a chain whose every crossing is a slot may take three + crossings, any other chain keeps two. On tmux, `--path=cmdq_fire_command,window_copy_cmd_cancel` now prints the + three-field-call chain (`cmd_entry.exec`, `window_mode.command`, the command table's `.f`); before, no chain. +- A `reachable="0"` path answer whose search met gaps and whose `to=` has such a hop ends with `next="--callers="` + (CLI and MCP `path_between`) instead of the gap rows' bodies. +- `--impact` (XML, `--json`, columnar count, MCP `impact`) adds `reaches_hedged=N` beside `reaches=`: the functions that + may reach the symbol through one or two calls through struct fields, with the two-hop ones listed as + `` (16 shown, `next=` the rest). Never counted in + `reaches=`, `by_depth=` or the radius counts. + +Every crossing stays a hedge: `proven="0"`, `via=` named, its width beside it, and never in `reachable=`/`hops=`/`reaches=`. + +### Fixed — a declined call on a typed in-tree receiver is no longer read as the builtin method its name spells + +The bridge and the `` frontier skipped every declined call spelled like a builtin type's method, so hono's +`this.router.match( method, path )` (`router!: Router`, an in-tree interface with eight implementations) was dropped as +`String.prototype.match`. The skip now lifts for one field of `this`/`self` or of a parameter whose class the source +states, when every same-named class types that field as an in-tree class and the call's candidate list is complete (2 +to 16). A `.match` on a string parameter, an un-annotated local, a field typed `string`, or a field another same-named +class types outside the tree stays skipped. + ### Security — the pattern screen refuses overlapping alternation, one group deeper A pattern whose group carries a top-level `|` under an unbounded outer quantifier could backtrack catastrophically diff --git a/README.md b/README.md index df8542c7c..f07180754 100644 --- a/README.md +++ b/README.md @@ -2120,9 +2120,9 @@ wrong, and it has. These are the results that say so, all in-tree, all published ### In the tests
-703 gate scripts, five contracts no unit test can hold, and the house rule: write the gate before the code it measures +704 gate scripts, five contracts no unit test can hold, and the house rule: write the gate before the code it measures -`test/regression.sh` names **703 gate scripts** and is the authoritative list; +`test/regression.sh` names **704 gate scripts** and is the authoritative list; `python3 test/pargates.py . ./build/ripwire -j 6` runs the same set in parallel. On top of them sit the contracts that do not fit a unit test: two runs byte-identical, warm output identical to cold, output that pipes clean through `xmllint --noout`, a sanitizer build with `-fno-sanitize-recover=all`, and a diff --git a/docs/EVALS.md b/docs/EVALS.md index a77007342..ab0849671 100644 --- a/docs/EVALS.md +++ b/docs/EVALS.md @@ -21,7 +21,7 @@ section, and it is not an afterthought. | **Co-change / known-item evals** | `--eval`, `--eval-retrieval` (see `bench/ANSWERQUALITY.md`) | Whether the tool surfaces the other files a real historical commit touched; and known-item retrieval across four rankers. | | **Ensemble calibration harness** | `bench/ensemblecal/` | Whether `--ensemble`'s four evidence families are actually orthogonal, how often each fires, how stable each is across commits — and the preset ladder derived from that (§9). | | **Differential argv harness** | `test/argvdiffcheck.sh` | That a refactor changed *nothing observable*: two binaries, every argv vector, stdout + stderr + exit code byte-identical. | -| **The gate suite** | `test/regression.sh`, `test/pargates.py` | 703 gate scripts plus the determinism, cache-transparency and golden contracts. | +| **The gate suite** | `test/regression.sh`, `test/pargates.py` | 704 gate scripts plus the determinism, cache-transparency and golden contracts. | | **`--quality-delta`** | `src/quality.h` | Ten measured code-quality failure modes, reported only where a change made them worse. | ### The labeling protocol (why the held-out eval is allowed to disagree with the ranker) @@ -5888,7 +5888,7 @@ copy here would be exactly the dialect divergence that gate exists to catch. Com tags, wrap, stable-order defaults), seven individually invoked standalone gates (`g1freshcheck`, `skillscan`, `htmlexport`, `compresscheck`, `handoffcheck`, `releaseinstallcheck`, `taskroutecheck`), and a single loop -naming **703 gate scripts**, all of which exist on disk. +naming **704 gate scripts**, all of which exist on disk. `python3 test/pargates.py . ./build/ripwire -j 6` runs the same scripts in parallel so a full verification fits in one sitting. It does not modify `regression.sh`. @@ -7005,7 +7005,7 @@ Listed because the reason is more useful than the silence. shipped**. See `bench/locbench/anchorhop_calib.json`. The mention anchor's reproducible numbers are the ablations in §4. - **A single round gate-count.** Two in-tree numbers disagree (`test/pargates.py`'s docstring says - ~210; `test/argvdiffcheck.sh` says 200+), while the loop in `test/regression.sh` names 703. The + ~210; `test/argvdiffcheck.sh` says 200+), while the loop in `test/regression.sh` names 704. The loop is the authority; the stale docstrings are a known drift. Since 2026-09-10 the number is not written by hand anywhere: `docs/gatecount_build.py` derives it from the loop and rewrites every published site, `test/gatecountcheck.sh` fails if any of them drifts, and `test/manifestcheck.sh` diff --git a/present/deck5_ripwire_build.js b/present/deck5_ripwire_build.js index ac507279a..04c345674 100644 --- a/present/deck5_ripwire_build.js +++ b/present/deck5_ripwire_build.js @@ -1214,7 +1214,7 @@ function storyCards(s, { kick, head, stories, footText }){ kicker(s, "// how it stays true", AMBER); title(s, "Proven, not promised"); const cards = [ - ["703 gate scripts", "the suite runs on every push — plus determinism, cache-transparency and golden contracts; the gate count itself is gated against the runner's own loop"], // gatecount + ["704 gate scripts", "the suite runs on every push — plus determinism, cache-transparency and golden contracts; the gate count itself is gated against the runner's own loop"], // gatecount ["byte-identical, always", "two runs over the same tree produce the same bytes; warm equals cold. Enforced in CI, twice — Release AND a plain flavour, because NDEBUG once blinded a whole class of checks"], ["differential refactoring", "a refactor must prove it changed nothing observable: two binaries, hundreds of argv vectors, stdout + stderr + exit codes byte-identical"], ["held-out labels, authored blind", "eval labels were written by reading source before the ranker ever ran on them — so the eval is allowed to say the ranker is wrong. It has."], @@ -1238,7 +1238,7 @@ function storyCards(s, { kick, head, stories, footText }){ title(s, "Claims you can trust, because we publish what failed", { size: 32 }); card(s, MX, 1.72, 3.86, 1.72); - stat(s, "703", "gate scripts named by test/regression.sh — and the COUNT itself is gated against the runner's own loop, so it cannot go stale quietly", // gatecount + stat(s, "704", "gate scripts named by test/regression.sh — and the COUNT itself is gated against the runner's own loop, so it cannot go stale quietly", // gatecount MX+0.15, 1.86, 3.56, CYAN, { bsize: 42, bh: 0.66, lsize: 9.5 }); card(s, 4.68, 1.72, 3.86, 1.72, CARD2); stat(s, "8", "registered NEGATIVES — changes built, gated green, measured against a band written before the code, and reverted rather than tuned", @@ -1540,7 +1540,7 @@ function storyCards(s, { kick, head, stories, footText }){ ["192 long flags · 36 slides", "bash test/deckclaimcheck.sh"], ["every --flag named here exists", "bash test/deckcheck.sh"], ["74.7% fewer element bytes", "bash test/showcasecapturecheck.sh"], - ["703 gate scripts", "bash test/manifestcheck.sh"], // gatecount + ["704 gate scripts", "bash test/manifestcheck.sh"], // gatecount ["49 repos · 71 papers · 237 surveyed","bash test/readmedriftcheck.sh"], ["the ten moments, any row", "ripwire . --callers=SYM | wc -c"], ["the head-to-head table", "bench/headtohead/r4-2026-08-06/"], diff --git a/src/backhops.h b/src/backhops.h index f5ae1a294..fed98dcd2 100644 --- a/src/backhops.h +++ b/src/backhops.h @@ -64,11 +64,11 @@ inline bool builtinSpelledCall( const Reference& call ) noexcept // (JS/TS) or `self.f` (Python, Ruby), the language's keyword only (checklist 37: a local named `self` in JS is an ordinary // name) — or a parameter or local of the calling function whose class the source states (`app: Hono`, a // LocalBindKind::RecvType record, unqualified, one spelling), standing for EVERY in-tree class definition of that name; -// (4) each such class that states f's class (a LocalBindKind::MemberType record, `router!: Router<…>`, `this.f = new Foo()`; -// one spelling per class, two decide nothing) states an in-tree class-like definition's name, and at least one does. +// (4) EVERY such class states f's class (a LocalBindKind::MemberType record, `router!: Router<…>`, `this.f = new Foo()`; one +// spelling per class, two decide nothing) as the name of an in-tree class-like definition. // What it does NOT mean: the call is not proven to reach any candidate; it is only no longer read as the builtin. A `.match` on a // receiver with no stated class (`path.match( re )`), on a field typed `string`, on a field one same-named class types outside -// the tree, or on a field only a base class states (bases are not walked) stays skipped. +// the tree or not at all, or on a field only a base class states (bases are not walked) stays skipped. class DeclinedCrossRule { public: @@ -173,21 +173,14 @@ class DeclinedCrossRule owners.push_back( it->id ); } } - std::size_t typed = 0; - for( const NodeId owner : owners ) + return !owners.empty() && std::ranges::all_of( owners, [ & ]( NodeId owner ) { + // (4) this class states one in-tree class for the field; stating none (a primitive, a base's field), two spellings, or + // a class the tree does not define leaves the builtin reading standing const auto it = std::ranges::lower_bound( m_fields, std::pair( owner, ch.path ), fieldKeyLess, [ ]( const FieldType& f ) { return std::pair( f.owner, f.field ); } ); - if( it == m_fields.end() || it->owner != owner || it->field != ch.path ) - { - continue; // this class states nothing for the field - } - if( it->type.empty() || !std::ranges::binary_search( m_classNames, it->type, sortutil::svLess ) ) - { - return false; // (4) two spellings, or a class the tree does not define: the builtin reading stands - } - ++typed; - } - return typed > 0; + return it != m_fields.end() && it->owner == owner && it->field == ch.path && !it->type.empty() + && std::ranges::binary_search( m_classNames, it->type, sortutil::svLess ); + } ); } // The class the source states for parameter or local `var` of function `fn` (one unqualified spelling), "" when none or two. diff --git a/test/hop1check.sh b/test/hop1check.sh new file mode 100755 index 000000000..13c9fcdcc --- /dev/null +++ b/test/hop1check.sh @@ -0,0 +1,402 @@ +#!/usr/bin/env bash +# hop1check.sh — ONE backward hop provider (src/backhops.h), three consumers: --path's hedged bridge, --path's root next=, +# --impact's hedged slot ring; and #11, the builtin-name skip of declined crossings narrowed to a receiver the index types. +# +# THE DEFECT. `--callers=X` named, one hop per call, who MAY call X without a graph edge ( called_by=, ), +# but the answers that join hops ignored it: a three-field-call chain (tmux send-keys: cmd_entry.exec, window_mode.command, +# the command table's .f) printed no bridge, its root next= pointed at gap bodies, and `--impact` said reaches="0" beside its +# own slot row. hono's `this.router.match( … )` was never crossed: `match` is spelled like String.prototype.match. +# +# THE CONTRACT. Every crossing is a hedge: proven="0" on the bridge, via= named (slot | slot-name | declined), its width +# beside it (slot_callers= / cands=, at most 16), and never counted in reachable= / hops= / reaches=. A chain whose every +# crossing is a slot may take 3 crossings; any other keeps 2. The ring is one or two slot hops back, reaches_hedged= beside +# reaches=, rows after the rows. #11 lifts the builtin skip only for one field of this/self or of a +# parameter whose class the source states, every same-named class typing it as an in-tree class, on a Ladder decline with a +# complete list (2..16). +# +# ARMS +# (B1) 3-slot chain: bridge crossings=3, every row via="slot" slot_callers="1"; root reachable="0" hops="0". +# (B2) one step past the slot bound (4 slot crossings): no bridge; the 3-step chain in the same tree keeps it (checklist 38). +# (B3) a mixed chain (value + slot + slot = 3 crossings, not all slot): no bridge; value + slot (2) keeps one. +# (B4) width: a field called through by 17 functions is not crossed; by 16 it is (slot_callers="16"). +# (B5) slot-name (one side's struct type unknown): via="slot-name". +# (B6) position-breaker: a positional entry after a preprocessor conditional is not joined, so not crossed; the twin is. +# (B7) escape-and-cast: a call through a cast receiver is not crossed; the same call through a typed local is. +# (B8) same field name in another struct: not crossed; the same struct's caller is. +# (D1) #11: `this.router.match( m )` (router!: Router, an in-tree interface; a Ladder decline over 2 routers) is crossed. +# (D2) the same field read off a parameter whose class is stated (`app: App`) is crossed. +# (D3) `path.match( /x/ )` (path: string), (D4) an un-annotated local, (D5) `this.name.match( re )` (name: string): not crossed. +# (D6) entity identity: a second class named App typing router as a Map makes the parameter shape undecided (not crossed), +# while `this.router` inside the first App stays crossed (owner by definition, not by name). +# (D7) a typed receiver over 17 candidates is no way in (near_sink stays external). +# (D8) near_sink counts exactly the crossed sites (why="declined:2"). +# (N1) to= with a slot hop: the root next= is --callers on to=; (N2) a to= with none keeps --expand; (N3) both legend forms. +# (I1) --impact ring: reaches="0" reaches_hedged="2", one row; (I2) a ring member already in reaches= is not counted; +# (I3) no slot row: no reaches_hedged=, no ; (I4) JSON twin, same names; (I5) columnar count; (I6) row cap 16 + next=. +# (M1) MCP path_between: the same and next= as the CLI; (M2) MCP impact: the same reaches_hedged= and . +# (L1) full legends carry the slot / ring / callers-next sentences and parse as XML; (L2) compact legends define the terms. +# +# Usage: test/hop1check.sh [BIN] (BIN defaults to RIPWIRE_BIN, then build/ripwire) +set -u +ROOT="$( cd "$( dirname "$0" )/.." && pwd )" +BIN="${1:-${RIPWIRE_BIN:-$ROOT/build/ripwire}}" +[ "${BIN#/}" = "$BIN" ] && BIN="$ROOT/$BIN" +echo "hop1check: BIN=$BIN" +[ -x "$BIN" ] || { echo " FAIL no ripwire binary at $BIN — build first"; exit 2; } +command -v python3 >/dev/null 2>&1 || { echo " FAIL python3 required"; exit 2; } +TMP="$( mktemp -d )"; trap 'rm -rf "$TMP"' EXIT + +# ── fixtures ──────────────────────────────────────────────────────────────────────────────────────────────────────── +mkdir -p "$TMP/c3" "$TMP/mix" "$TMP/w16" "$TMP/w17" "$TMP/neg" "$TMP/known" "$TMP/cap" "$TMP/ts/routers" "$TMP/ts/app" \ + "$TMP/ts2/routers" "$TMP/ts2/app" "$TMP/ts2/other" "$TMP/wide/routers" "$TMP/wide/app" +cat > "$TMP/c3/chain.c" <<'EOF' +struct entry { const char *name; void (*exec)( void ); }; +struct mode { const char *name; void (*command)( int ); }; +struct cmdt { const char *name; void (*f)( void ); }; +struct wme { const struct mode *mode; }; +struct q { void (*run)( struct entry * ); }; + +struct wme *current_wme( void ); + +static void cancel_cmd( void ) { } +static void other_cmd( void ) { } +static const struct cmdt cmd_table[] = { { "cancel", cancel_cmd }, { "other", other_cmd } }; + +void copy_command( int i ) +{ + cmd_table[i].f(); +} +const struct mode copy_mode = { .name = "copy", .command = copy_command }; + +void send_keys_exec( void ) +{ + struct wme *w = current_wme(); + w->mode->command( 0 ); +} +const struct entry send_keys_entry = { .name = "send-keys", .exec = send_keys_exec }; + +void fire_command( struct entry *e ) +{ + e->exec(); +} +const struct q top_q = { .run = fire_command }; + +void top_level( struct q *x ) +{ + x->run( 0 ); +} + +static void lonely( void ) { } +EOF +# mix: start hands send_keys_exec to an external API (a value crossing), then two slot crossings to cancel_cmd +cp "$TMP/c3/chain.c" "$TMP/mix/chain.c" +cat >> "$TMP/mix/chain.c" <<'EOF' +void register_callback( void ( *cb )( void ) ); +void start( void ) +{ + register_callback( send_keys_exec ); +} +EOF +# known: send_keys_exec ALSO calls copy_command directly — a proven caller the ring must not count again +cat > "$TMP/known/k.c" <<'EOF' +struct entry { void (*exec)( void ); }; +struct mode { void (*command)( int ); }; +struct wme { const struct mode *mode; }; +struct wme *current_wme( void ); +void copy_command( int i ) { } +const struct mode copy_mode = { .command = copy_command }; +void send_keys_exec( void ) +{ + struct wme *w = current_wme(); + w->mode->command( 0 ); + copy_command( 1 ); +} +const struct entry send_keys_entry = { .exec = send_keys_exec }; +void fire_command( struct entry *e ) { e->exec(); } +EOF +# width: a field the sink is stored in, called through by 16 (w16) or 17 (w17) functions +for n in 16 17; do + { + echo 'struct hooks { void (*cb)( void ); };' + echo 'static void sink( void ) { }' + echo 'const struct hooks h = { .cb = sink };' + i=1; while [ "$i" -le "$n" ]; do printf 'void c%02d( struct hooks *x ) { x->cb(); }\n' "$i"; i=$(( i + 1 )); done + } > "$TMP/w$n/w.c" +done +# cap: 17 functions call through the field copy_command is stored in — the ring's d=2 rows cut at 16 +{ + echo 'struct mode { void (*command)( int ); };' + echo 'struct cmdt { void (*f)( void ); };' + echo 'struct wme { const struct mode *mode; };' + echo 'struct wme *current_wme( void );' + echo 'static void cancel_cmd( void ) { }' + echo 'static const struct cmdt cmd_table[] = { { cancel_cmd } };' + echo 'void copy_command( int i ) { cmd_table[i].f(); }' + echo 'const struct mode copy_mode = { .command = copy_command };' + i=1; while [ "$i" -le 17 ]; do printf 'void k%02d( void ) { struct wme *w = current_wme(); w->mode->command( 0 ); }\n' "$i"; i=$(( i + 1 )); done +} > "$TMP/cap/cap.c" +cat > "$TMP/neg/neg.c" <<'EOF' +struct A { void (*cb)( void ); }; +struct B { void (*cb)( void ); }; +struct ops { int a; int b; void (*run)( void ); }; + +static void sink_a( void ) { } +const struct A a_hooks = { .cb = sink_a }; +void call_a( struct A *a ) { a->cb(); } +void call_b( struct B *b ) { b->cb(); } + +static void sink_pos( void ) { } +const struct ops pos_ok = { 1, 2, sink_pos }; +void call_pos( struct ops *p ) { p->run(); } + +static void sink_ifdef( void ) { } +const struct ops pos_bad = { 1, +#ifdef EXTRA + 3, +#endif + 2, sink_ifdef }; +void call_ifdef( struct ops *p ) { p->run(); } + +void call_cast( void *p ) { ( (struct ops *)p )->run(); } +void call_typed( void *p ) { struct ops *q = p; q->run(); } +EOF +# ts: the hono shape (#11) +cat > "$TMP/ts/routers/router.ts" <<'EOF' +export interface Router { + match(method: string): string +} +EOF +for x in a b; do +cat > "$TMP/ts/routers/$x.ts" < "$TMP/ts/app/app.ts" <<'EOF' +import type { Router } from '../routers/router' +import { aRouter } from '../routers/a' +import { bRouter } from '../routers/b' + +export class App { + router!: Router + name: string = 'app' + constructor() { + this.router = Math.random() > 0.5 ? new aRouter() : new bRouter() + } + dispatch(method: string): string { + return this.router.match(method) + } + label(re: RegExp): boolean { + return this.name.match(re) !== null + } +} + +export function viaParam(app: App): string { + return app.router.match('GET') +} + +export function onString(path: string): boolean { + return path.match(/x/) !== null +} + +export function onLocal(): boolean { + const p = 'abc' + return p.match(/b/) !== null +} +EOF +cp "$TMP/ts/routers/"*.ts "$TMP/ts2/routers/" +cp "$TMP/ts/app/app.ts" "$TMP/ts2/app/app.ts" +cat > "$TMP/ts2/other/app.ts" <<'EOF' +export class App { + router: Map = new Map() +} +EOF +# wide: 17 routers, every one imported by the app — a typed receiver over 17 candidates +{ + echo "export interface Router {"; echo " match(method: string): string"; echo "}" +} > "$TMP/wide/routers/router.ts" +imports="" +i=1; while [ "$i" -le 17 ]; do + printf "import type { Router } from './router'\nexport class R%02d implements Router {\n match(method: string): string {\n return method\n }\n}\n" "$i" > "$TMP/wide/routers/r$i.ts" + imports="${imports}import { R$( printf '%02d' "$i" ) } from '../routers/r$i' +" + i=$(( i + 1 )) +done +{ + echo "import type { Router } from '../routers/router'" + printf '%s' "$imports" + echo "export class App {" + echo " router!: Router" + echo " dispatch(method: string): string {" + echo " return this.router.match(method)" + echo " }" + echo "}" +} > "$TMP/wide/app/app.ts" + +python3 - "$BIN" "$TMP" "$ROOT" <<'PY' +import json, re, shutil, subprocess, sys +binp, tmp, repo = sys.argv[1], sys.argv[2], sys.argv[3] +fail = 0 +def ok( m ): print( " PASS " + m ) +def no( m ): + global fail + fail = 1 + print( " FAIL " + m ) +def run( d, *args ): + """One answer, read only off a run that succeeded and printed a root (checklist 14).""" + p = subprocess.run( [ binp, d, "--no-cache", *args ], cwd = d, capture_output = True, text = True, timeout = 300 ) + if p.returncode != 0 or not re.search( r'<(path|impact|callers) |^\{"of"', p.stdout ): + return None + return p.stdout +def body( t ): + return re.sub( r'', '', t, flags = re.S ) +def bridge( t ): + m = re.search( r'', body( t ) ) + return m.group( 0 ) if m else "" +def root( t, tag ): + m = re.search( r'<' + tag + r' [^>]*>', body( t ) ) + return m.group( 0 ) if m else "" +def attr( el, name ): + m = re.search( r'\s' + name + r'="([^"]*)"', el ) + return m.group( 1 ) if m else None +def check( label, cond, detail = "" ): + ( ok if cond else no )( label + ( "" if cond else " — " + detail ) ) + +def path( d, a, b, *extra ): + t = run( d, "--path=%s,%s" % ( a, b ), *extra ) + return t if t is not None else "" + +# ── B: the bridge's slot crossings ────────────────────────────────────────────────────────────────────────────────── +c3 = tmp + "/c3" +t = path( c3, "fire_command", "cancel_cmd" ); br = bridge( t ); r = root( t, "path" ) +rows = re.findall( r']*/>', br ) +check( "(B1) a 3-slot chain is bridged: crossings=3, every crossing via=slot slot_callers=1, proven=0", + attr( br, "crossings" ) == "3" and attr( br, "proven" ) == "0" and len( rows ) == 4 + and all( 'via="slot" slot_callers="1"' in x for x in rows[1:] ) + and [ attr( x, "n" ) for x in rows ] == [ "fire_command", "send_keys_exec", "copy_command", "cancel_cmd" ], repr( br ) ) +check( "(B1) the root stays reachable=0 hops=0 (a bridge is in no count)", attr( r, "reachable" ) == "0" and attr( r, "hops" ) == "0", r ) +t4 = path( c3, "top_level", "cancel_cmd" ); t3 = path( c3, "top_level", "copy_command" ) +check( "(B2) one step past the slot bound (4 slot crossings): no bridge", t4 != "" and bridge( t4 ) == "" and attr( root( t4, "path" ), "reachable" ) == "0", repr( bridge( t4 ) ) ) +check( "(B2) one step short (3 slot crossings) in the same tree: the bridge stays", attr( bridge( t3 ), "crossings" ) == "3", repr( bridge( t3 ) ) ) +mix = tmp + "/mix" +tm3 = path( mix, "start", "cancel_cmd" ); tm2 = path( mix, "start", "copy_command" ) +check( "(B3) a mixed chain of 3 crossings (value + slot + slot) is not bridged", tm3 != "" and bridge( tm3 ) == "", repr( bridge( tm3 ) ) ) +check( "(B3) value + slot (2 crossings) is", re.search( r'n="send_keys_exec"[^>]*via="value" cands="1".*n="copy_command"[^>]*via="slot" slot_callers="1"', bridge( tm2 ) ) is not None, repr( bridge( tm2 ) ) ) +t17 = path( tmp + "/w17", "c01", "sink" ); t16 = path( tmp + "/w16", "c01", "sink" ) +check( "(B4) a field 17 functions call through is not crossed", t17 != "" and bridge( t17 ) == "", repr( bridge( t17 ) ) ) +check( "(B4) a field 16 functions call through is (slot_callers=16)", 'via="slot" slot_callers="16"' in bridge( t16 ), repr( bridge( t16 ) ) ) +sj = repo + "/test/slotjoinfix" +ts5 = path( sj, "run_outer", "on_cb" ) +check( "(B5) a slot joined by field name only is crossed as via=slot-name", 'n="on_cb" p="mode.c:7" via="slot-name" slot_callers="1"' in bridge( ts5 ), repr( bridge( ts5 ) ) ) +neg = tmp + "/neg" +def crossed( a, b ): + return bridge( path( neg, a, b ) ) != "" +check( "(B6) a positional entry after a preprocessor conditional is not crossed", not crossed( "call_ifdef", "sink_ifdef" ) ) +check( "(B6) its twin without the conditional is", crossed( "call_pos", "sink_pos" ) ) +check( "(B7) a call through a cast receiver is not crossed", not crossed( "call_cast", "sink_pos" ) ) +check( "(B7) the same call through a typed local is", crossed( "call_typed", "sink_pos" ) ) +check( "(B8) a same-named field of ANOTHER struct is not crossed", not crossed( "call_b", "sink_a" ) ) +check( "(B8) the same struct's caller is", crossed( "call_a", "sink_a" ) ) + +# ── D: #11, the builtin-name skip narrowed ────────────────────────────────────────────────────────────────────────── +ts = tmp + "/ts" +def tsbridge( d, a ): + return bridge( path( d, a, "routers/a.ts:match" ) ) +check( "(D1) this.router.match (router!: Router, in-tree) is crossed via=declined cands=2", + 'n="match" p="routers/a.ts:3" via="declined" cands="2"' in tsbridge( ts, "dispatch" ), repr( tsbridge( ts, "dispatch" ) ) ) +check( "(D2) app.router.match on a parameter typed App is crossed", 'via="declined" cands="2"' in tsbridge( ts, "viaParam" ), repr( tsbridge( ts, "viaParam" ) ) ) +check( "(D3) path.match( /x/ ) on a parameter typed string is not crossed", tsbridge( ts, "onString" ) == "" and path( ts, "onString", "routers/a.ts:match" ) != "" ) +check( "(D4) p.match( /b/ ) on an un-annotated local is not crossed", tsbridge( ts, "onLocal" ) == "" and path( ts, "onLocal", "routers/a.ts:match" ) != "" ) +check( "(D5) this.name.match( re ) (name: string) is not crossed", tsbridge( ts, "label" ) == "" and path( ts, "label", "routers/a.ts:match" ) != "" ) +ts2 = tmp + "/ts2" +check( "(D6) a second class App typing router as a Map (not an in-tree class) leaves the parameter shape undecided (not crossed)", + tsbridge( ts2, "viaParam" ) == "" and path( ts2, "viaParam", "routers/a.ts:match" ) != "" ) +check( "(D6) this.router inside the first App stays crossed (owner by definition, not by name)", 'via="declined" cands="2"' in tsbridge( ts2, "dispatch" ), repr( tsbridge( ts2, "dispatch" ) ) ) +tw = path( tmp + "/wide", "dispatch", "routers/r1.ts:match" ) +check( "(D7) a typed receiver over 17 candidates is no way in (near_sink why=external, no bridge)", + tw != "" and bridge( tw ) == "" and 'why="external"' in body( tw ) and "declined" not in ( re.search( r'', body( tw ) ) or re.match( "", "" ) ).group( 0 ), repr( body( tw )[-400:] ) ) +td = path( ts, "onString", "routers/a.ts:match" ) +check( "(D8) near_sink counts exactly the two crossed sites (why=declined:2)", 'why="declined:2"' in body( td ), repr( re.findall( r']*>', body( td ) ) ) ) + +# ── N: the root next= ─────────────────────────────────────────────────────────────────────────────────────────────── +t = path( c3, "fire_command", "cancel_cmd" ); r = root( t, "path" ) +check( "(N1) to= with a slot hop: the root next= is --callers on to=", attr( r, "next" ) == "--callers=chain.c:cancel_cmd", repr( attr( r, "next" ) ) ) +tl = path( c3, "fire_command", "lonely" ); rl = root( tl, "path" ) +check( "(N2) to= with no backward hop keeps the gap bodies next=", ( attr( rl, "next" ) or "" ).startswith( "--expand=" ) and attr( rl, "gaps" ) is not None, repr( rl ) ) +tf = path( c3, "fire_command", "cancel_cmd", "--legend=full" ); tlf = path( c3, "fire_command", "lonely", "--legend=full" ) +check( "(N3) the full legend says next= lists to='s callers exactly when it does, and reads the bodies otherwise", + "next= lists to='s callers" in tf and "next= reads the rows' bodies" not in tf + and "next= reads the rows' bodies" in tlf and "next= lists to='s callers" not in tlf ) + +# ── I: the --impact hedged slot ring ──────────────────────────────────────────────────────────────────────────────── +ti = run( c3, "--impact=cancel_cmd" ) or ""; ri = root( ti, "impact" ) +check( "(I1) reaches stays 0, reaches_hedged=2 beside it", attr( ri, "reaches" ) == "0" and attr( ri, "reaches_hedged" ) == "2", ri ) +check( "(I1) one d=2 row: send_keys_exec may call copy_command through a field at chain.c:22", + '' in body( ti ), repr( body( ti )[-500:] ) ) +tk = run( tmp + "/known", "--impact=copy_command" ) or ""; rk = root( tk, "impact" ) +check( "(I2) a ring member already in reaches= is not counted again (reaches=1, reaches_hedged=1)", + attr( rk, "reaches" ) == "1" and attr( rk, "reaches_hedged" ) == "1" and '", + tn != "" and "reaches_hedged" not in tn and "]*>', body( tcap ) ) +check( "(I6) 17 two-hop rows: shown=16 of total=17, next= the callers of the first unshown row's calls=", + ring is not None and attr( ring.group( 0 ), "total" ) == "17" and attr( ring.group( 0 ), "shown" ) == "16" + and attr( ring.group( 0 ), "next" ) == "--callers=copy_command" and len( re.findall( r' 0: + resp[ o[ "id" ] ] = "".join( c.get( "text", "" ) for c in o.get( "result", {} ).get( "content", [] ) ) +m1, m2, m3 = resp.get( 1, "" ), resp.get( 2, "" ), resp.get( 3, "" ) +cli1 = path( c3, "fire_command", "cancel_cmd" ); cli2 = path( ts, "dispatch", "routers/a.ts:match" ) +check( "(M1) MCP path_between: the same and root next= as the CLI (slot chain and #11 crossing)", + bridge( m1 ) != "" and bridge( m1 ) == bridge( cli1 ) and attr( root( m1, "path" ), "next" ) == attr( root( cli1, "path" ), "next" ) + and bridge( m2 ) != "" and bridge( m2 ) == bridge( cli2 ), repr( ( bridge( m1 )[:200], bridge( m2 )[:200] ) ) ) +cli3 = run( c3, "--impact=cancel_cmd" ) or "" +ringm = re.search( r'', body( m3 ) ); ringc = re.search( r'', body( cli3 ) ) +check( "(M2) MCP impact: the same reaches_hedged= and as the CLI", + attr( root( m3, "impact" ), "reaches_hedged" ) == "2" and ringm is not None and ringc is not None and ringm.group( 0 ) == ringc.group( 0 ), repr( m3[-400:] ) ) + +# ── L: legends ────────────────────────────────────────────────────────────────────────────────────────────────────── +pf = path( c3, "fire_command", "cancel_cmd", "--legend=full" ) +imf = run( c3, "--impact=cancel_cmd", "--legend=full" ) or "" +check( "(L1) the full path legend names the slot crossing and slot_callers=", "slot_callers=N the functions that field join names" in pf and "3 when every crossing is a slot" in pf ) +check( "(L1) the full impact legend names reaches_hedged= and the rows", "reaches_hedged=N (absent when 0)" in imf and "" in imf ) +if shutil.which( "xmllint" ): + for label, doc in ( ( "path", pf ), ( "impact", imf ), ( "impact compact", ti ), ( "path compact", t ) ): + x = subprocess.run( [ "xmllint", "--noout", "-" ], input = doc, capture_output = True, text = True ) + check( "(L1) the %s answer with the new clauses parses as XML (checklist 25)" % label, x.returncode == 0 and doc != "", x.stderr[:300] ) +else: + print( " SKIP (L1) xmllint not installed: the well-formedness arm needs it" ) +check( "(L2) the compact path legend defines slot_callers=", "s via=slot|slot-name slot_callers=N:" in t ) +check( "(L2) the compact impact legend defines reaches_hedged= and the ring row", "reaches_hedged=N:" in ti and "rh called_by= called_at= via= calls=:" in ti ) +sys.exit( fail ) +PY +rc=$? +if [ "$rc" -eq 0 ]; then echo "ALL PASS"; else echo "FAIL"; fi +exit "$rc" diff --git a/test/regression.sh b/test/regression.sh index 40bff09ee..eb17b4246 100755 --- a/test/regression.sh +++ b/test/regression.sh @@ -278,7 +278,7 @@ else fi # retired: forspinecheck — the --for caller spine (Change A) was dropped by its own offline kill rule: a spine of up to 2 caller hops from the top 3 seeds named a missed rubric item on 1 of 7 in-sample rows, so the gate that pinned it went with it # retired: cacheexclkeycheck — the per-configuration auto-cache key it pinned is a registered NEGATIVE (docs/EVALS.md, "The auto-cache key ignores --exclude", RUN 2026-09-03: a 158K-file root with >= 12 gate configurations thrashed the 2 GiB sweep); the retry design keeps ONE superset blob per root and will bring its own gate -for _g in a9disclosurecheck abicheck accessshapecheck ackonlycheck adaptivecheck adaptivecutshapecheck affectedcheck agentloopclaudecheck agentloopcodexcheck agentloopeditsuitecheck agentloopfollowupcheck agentloopgradercheck agentlooplockcheck agentloopopencodecheck agentsurfacecheck agenttablecheck aiderbytescheck anchorbodycheck anchorcheck archcheck archmetricscheck argvdiffcheck arisefollowupcheck ariseshimcheck aritycheck artifactcheck astqueryregexcheck astrocheck atcheck atomscheck attribmultidefcheck attrvocabcheck baselinecheck baselinedirtycheck baselineportcheck bashsourcecheck batchcheck binoverridecheck blindspotcheck bm25boundcheck bm25check bodiesshowncheck bodydialectcheck budgetpolicycheck buildtypestampcheck builtinbindcheck bundleidcheck cachefuzzcheck cachehashcheck cacheidentitycheck cacheisolationcheck cachelintcheck cacheoffsetcheck cachereservecheck cachesplitcheck callerscheck callformcheck callsrankordercheck candheadcheck candidatescheck canoncheck capdisclosurecheck capsweepcheck ccheck ccjsoncheck ceilingverdictcheck chacheck chaconecheck chainguardcheck chainidcheck childwalkscalecheck churndecaycheck churnjoincheck churnjsonstampcheck claudeconfigdircheck clicheck clojurecheck clonebandcheck clonecachecheck clonededupcheck cloneidiomcheck clonelexcheck clsrecvcheck cochangeboostcheck cochangecliocheck cochangesurprisecheck codexinstallhonestycheck codexplugincheck codexwrapcheck collectioncapcheck columnarattrcheck columnarcheck columnarcommacheck commentcoherencecheck commenttokencheck communitydrillcheck communitylabelcheck compactlegendcheck compactroutecheck completecheck composelangcheck connectcheck connectcorecheck connectjoincheck constcheck contextratiocheck coplintcheck countfloorcheck cppbenchcheck cppoperatorcheck cppqualcheck cpptmplscopecheck crashsweepcheck crawlescapecheck crossdirincludecheck crossrefcheck crossrefdegradecheck csharpcheck csharpcondcheck cudacheck cyclecutcheck danglingwarncheck dartcheck deadcheck deadfiltercheck deadprecisioncheck deckcheck deckclaimcheck declinecheck declinedlistcheck declinedsitecheck decltodefcheck deeptailcheck defaultceilingcheck defectshapecheck defoverdeclcheck degradedhintcheck dependencypincheck deplangscheck depsprecisecheck detailcheck diagnoticecheck didyoumeancheck dispatchordercheck dmmcheck docanchorcheck docdemotecheck docdriftcheck docdriftcommentcheck docmdcachecheck docmentioncheck docscommandscheck doctorcheck donelegendcheck droppedpositivecheck duprowcheck dynmapsimdcheck editcheckanswercheck editcheckcheck editcheckdeclcheck editchecknotecheck edithandlehintcheck editpayloadbinarycheck editplancheck editplanpayloadconfinecheck editplanrecheckcheck editplanrollbackmsgcheck editpreviewcheck editroundtripcheck edittargetfileabscheck eliximportcheck elixircheck elixirnamearitycheck elixirsemanticcheck emitescapecheck emittertruthcheck emptycorpuscheck emptyvaluerefusecheck ensembleavailcheck ensemblecheck enumtablecheck essentialcxcheck estchargecheck evalcheck evictioncheck exemplarcheck exemplarconfcheck exercisescheck expandbodyfirstcheck expandcallscheck expandmodecheck expandrangecheck expandsibscheck expandtokencheck expandtopk0check extentcheck externalvetocheck falseedgecheck fcallforwardcheck fficheck fieldaffinitycheck fieldidcheck fieldnarrowcheck fieldusescheck filerootcheck fileselectorrefusecheck fillordercheck fixedbufsweep flagscheck flagsnoisecheck flagsurfacecheck flagtablecheck flipcheck floormarkcheck fnliteralcheck fnptrcheck forautobodycheck forblowupcheck forbodyfoldcheck forbodyslotcheck forbudgetmonotoncheck forcalibfactscheck forcompresscheck fordisclosurecheck forhdrshapecheck forheadrrcheck forlenscheck formatgatecheck formaxtokenscheck formembernamescheck fornotesbudgetcheck fornotesjsoncheck forrankordercheck forrootlegendcheck forsectioncollapsecheck forsigspancheck forsoftbudgetcheck forwidencheck freshclonecheck freshnesscheck g1configcheck gateabilitycheck gatecountcheck gateexitcheck gdscriptcheck genrecallcheck gitchildoptscheck gitenvhermeticcheck githardencheck gitignorecheck gitquotepathcheck gitstampcheck goinstcheck gointerfacecheck gokindcheck goscopecheck graphlegendbudgetcheck graphqueryrefusecheck grepanchorcheck grepandcheck grepbytescheck grepcheck grepcontextcheck grepcorpuscheck grepfastcheck grepfollowupcheck grepignorecheck grepscancheck grepseamcheck greptiercheck guardmsgcheck hasacheck hazardpatterncheck headbinstagecheck headsnapcachecheck helpbudgetcheck hermesinstallcheck historyoraclecheck hookcheck hostilecheck hotspotsincecheck htmlcolorcheck htmlhostcheck htmlrendercheck identitycheck impactdepthcheck impactimportcheck impactpartitioncheck impacttypecheck importcapcheck importnarrowcheck includeanglecheck includeprecisecheck indexoutcheck infraportcheck isolateprovenancecheck javamethodrefcheck javarubycheck jslangcheck jsmetricscheck jsnestedcheck jsoncheck jsonlangcheck jsonparitycheck jsonredactcheck jsonrefusallegendcheck jsonwalkcheck jsshapecheck jsverbscheck jsxcallcheck knownitemcheck kotlincheck landingcheck langcensuscheck langcheck langidentitycheck layerquerycheck layoutcheck lb3namecheck legendcostcheck legendcoveragecheck legenddriftcheck legendrefcheck legobundlecheck legocheck legotransitivecheck liftdisclosurecheck limitstablecheck lintbudgetcheck lintcatalogcheck lintcheck lintdedupcheck lintpayloadcapcheck lintprecisioncheck lintrulescheck lintscopecheck lintselectcheck listingpagingcheck localdeclcheck localitycheck localscountcheck loopconservationcheck lpincheck luacheck luarequirecheck macroedgecheck macroreparsecheck manifestcheck mapdatasectioncheck mapdiffcheck mapinventorycheck matchcapturecheck matchgrammarcheck maxfilesizecheck mcpattrparitycheck mcpaudit4hardencheck mcpclidiffcheck mcpcodexmetacheck mcpcontractcheck mcpdegradedhintcheck mcpeditcheck mcpeditkindcheck mcpeditmodecheck mcpeditpresencecheck mcpeditracecheck mcpflagshipcheck mcpforparitycheck mcpframehonestycheck mcpgrepdegradedcheck mcphandlecheck mcpincrementalcheck mcpmanifestcheck mcprangeedgecheck mcpreadloopcheck mcpredactcheck mcpreloadasancheck mcpreloadcheck mcpremotecheck mcprobustcheck mcpslicecheck mcpstalecheck mcpstdiolinecapcheck mcpstrictschemacheck mcptoolprunecheck mcptoolsubsetcheck mcptranchecheck mcptwinclaimscheck mcpverbscheck mcpw2fixcheck mcpw3fixcheck mcpwatchercheck mdembedcheck mdsectioncheck memguardcheck mentioncapcheck mentioncheck mentionsverbcheck mergechurncheck mergescoutcheck mergescoutlonglinecheck metalcheck meterdisclosurecheck metricscheck modifierguardcheck moduleconstcheck morecontractcheck mrowalkcheck multirootcheck multiswecheck namedfileinputcheck nameinfocheck namingcalibrationcheck namingconsistencycheck naminglenscheck naminglocalscheck narrowcheck narrowlangcheck neighbourcapcheck nestedimportcheck nestedqualcheck nestprofilecheck nextverbcheck noaliascheck nodekindcheck nongitqmetricscheck nonlocalstatecheck notecanoncheck notescheck notesdegradecheck nsfiltercheck nulbytecheck numericrefusecheck objcfieldcheck objcsniffcheck opencodewrapcheck optremarkscheck optremarkshotcheck ordercheck osswitchcheck oswin32logiccheck outlinecheck overbudgetcommentcheck ownerhopcheck ownerscheck packcallersharecheck packtaskcheck packtaskmonotoncheck packtaskquotacheck padscalecheck paginationcheck pagingsweepcheck panellegendcheck pargatescheck parsehealthcheck partitioncheck pathgapcheck pathownercheck patterncheck perfharnesscheck phpcheck pincensuscheck planlanescheck planlintcheck pmccheck portablebuildcheck portablecachecheck postingscheck ppaltcheck ppdeadrolescheck pranchorcheck prbudgetcheck prcheck prcontextcheck prconvergecheck precedencecheck preproccondcheck preprocdeadscalecheck prmaskanchorcheck prnestedcapcheck probecheck propcostcheck prrefsafecheck prrenamecheck pyimportprecisecheck pymodulealiascheck pyshapecheck qackconcurrencycheck qackorigincheck qbaselineproducercheck qchurncheck qchurnmemocheck qddegradecheck qddialscheck qdrefpaircheck qextractionkeycheck qoriginoraclecheck qrevtokencheck qrowlocatorcheck qschemetripcheck qsnapcachecheck qsnapprefetchcheck qsnapproducercheck qualifiedresolvecheck qualitycheck qualitycrosslangcheck qualityexcludecheck qualitykeycheck qualitykindscheck qualityorigincheck qualitypanelcheck qualityscopecheck qualitysignalcheck qualitystalecheck qualitysymcheck qualnewcheck querycheck queryfilescancheck racymtimecheck radixsimdcheck rangecomposecheck rankbycheck reachcheck readabilitycheck readmedriftcheck readmeexamplecheck recallanchorcheck recallboundarycheck recallbudgetcheck recallbufcheck recallevalcheck recallparitycheck recallpassagecheck recallrankdepthcheck recallrelcheck recallshapecheck recalltablecheck recalltotalcheck receiptpostcheck receiverevidencecheck recentscopecheck redactcheck redactfixcheck refusaltailcheck regexbombcheck regexcheck regexguardcheck regexrefusecheck registermacrocheck relevancefloorcheck relinkcheck reportcheck resolvecheck resolverhonestycheck retrievalqualitycheck reusefirstworkflowcheck ripwirepubliccheck rootrelcheck rootrelemitcheck rootspellingcheck routecheck routeedgecheck routehookcheck routeoncecheck routingreportcheck rubyargcheck rubyattrscheck rubybarecallcheck rubyclassrecvcheck rubyconstcheck rubydeclrefcheck rubydescribedclasscheck rubyinheritcheck rubymetricscheck rubyrakejbuildercheck rubyreachcheck rubyrecvcheck rubyrecvnarrowcheck rubyrequirecheck rubyrspectargetcheck rubyschemacheck rubyscopecheck rubysettercheck rubytypedrecvcheck runhintcheck runtracecheck rustanccheck rustimportprecisecheck rustqualcheck safedeletecheck sarifcheck savecachecheck scipcheck scipjoincheck scorecardcheck scoutheadconflictcheck scoutkeycheck scroundtripcheck seedboundscheck selectorchaincheck selectorhonestycheck selectorrefusecheck selectorscopecheck selfcheckcheck selfcontainedcheck shadowcheck shallowhistorycheck shapingflagcheck shellgateindexcheck showcasecapturecheck sibliftcheck sidecarsymlinkcheck sigredactcheck sincecheck sincecochangecheck sincewindowcheck singledefcheck situdiffcheck situshapecheck skilldescbudgetcheck skillevalcheck skillevalsplitcheck skillinstallcheck skillroutingjudgedcheck skillscanreadcheck skilltruthcheck skipclassifycheck skippedcheck skipreasoncheck slicecheck slicediffcheck sliceflowcheck sliceflowsenscheck slotjoincheck spectimingcheck staleackcheck statgatecheck stdmembergatecheck stdqualcheck strkerncheck structlayoutcheck sublistcountcheck subscriptopcheck substrfiltercheck subtokencheck svectorcheck swiftcheck swiftmemberscheck swiftshapecheck taskechocheck tempfilesymlinkcheck termmargincheck testedreachcheck testgatecheck testgatelegendbudgetcheck testgatepagecheck testgaterefusecheck testmacrocheck testrowruncheck testscopecheck textdocscheck timsortcheck tokenbudgetcheck tomllangcheck toolcallroutecheck tornreadcheck traceasanlinearcheck tracecheck tracehandoffcapcheck tracehopcheck traceminecheck treecheck truncvocabcheck tsimportprecisecheck tsshapecheck type3check type3clonecheck typerefcheck unreachablecheck unresolvedcheck usescheck usesliteralcheck usesselectorcheck usingdeclcheck utf8scrubcheck vendoredassetcheck vendoredbundlecheck vendorpatchcheck verifycheck versioncheck w2verbscheck w3fixbudgetcheck w3fixlegendcheck weaksignalcheck withgraphcheck withprofilecheck worktreeleakcheck wrapverbscheck writetargetcheck xmlwellformed yamllangcheck zonecheck zoneconsistencycheck zoomcheck; do +for _g in a9disclosurecheck abicheck accessshapecheck ackonlycheck adaptivecheck adaptivecutshapecheck affectedcheck agentloopclaudecheck agentloopcodexcheck agentloopeditsuitecheck agentloopfollowupcheck agentloopgradercheck agentlooplockcheck agentloopopencodecheck agentsurfacecheck agenttablecheck aiderbytescheck anchorbodycheck anchorcheck archcheck archmetricscheck argvdiffcheck arisefollowupcheck ariseshimcheck aritycheck artifactcheck astqueryregexcheck astrocheck atcheck atomscheck attribmultidefcheck attrvocabcheck baselinecheck baselinedirtycheck baselineportcheck bashsourcecheck batchcheck binoverridecheck blindspotcheck bm25boundcheck bm25check bodiesshowncheck bodydialectcheck budgetpolicycheck buildtypestampcheck builtinbindcheck bundleidcheck cachefuzzcheck cachehashcheck cacheidentitycheck cacheisolationcheck cachelintcheck cacheoffsetcheck cachereservecheck cachesplitcheck callerscheck callformcheck callsrankordercheck candheadcheck candidatescheck canoncheck capdisclosurecheck capsweepcheck ccheck ccjsoncheck ceilingverdictcheck chacheck chaconecheck chainguardcheck chainidcheck childwalkscalecheck churndecaycheck churnjoincheck churnjsonstampcheck claudeconfigdircheck clicheck clojurecheck clonebandcheck clonecachecheck clonededupcheck cloneidiomcheck clonelexcheck clsrecvcheck cochangeboostcheck cochangecliocheck cochangesurprisecheck codexinstallhonestycheck codexplugincheck codexwrapcheck collectioncapcheck columnarattrcheck columnarcheck columnarcommacheck commentcoherencecheck commenttokencheck communitydrillcheck communitylabelcheck compactlegendcheck compactroutecheck completecheck composelangcheck connectcheck connectcorecheck connectjoincheck constcheck contextratiocheck coplintcheck countfloorcheck cppbenchcheck cppoperatorcheck cppqualcheck cpptmplscopecheck crashsweepcheck crawlescapecheck crossdirincludecheck crossrefcheck crossrefdegradecheck csharpcheck csharpcondcheck cudacheck cyclecutcheck danglingwarncheck dartcheck deadcheck deadfiltercheck deadprecisioncheck deckcheck deckclaimcheck declinecheck declinedlistcheck declinedsitecheck decltodefcheck deeptailcheck defaultceilingcheck defectshapecheck defoverdeclcheck degradedhintcheck dependencypincheck deplangscheck depsprecisecheck detailcheck diagnoticecheck didyoumeancheck dispatchordercheck dmmcheck docanchorcheck docdemotecheck docdriftcheck docdriftcommentcheck docmdcachecheck docmentioncheck docscommandscheck doctorcheck donelegendcheck droppedpositivecheck duprowcheck dynmapsimdcheck editcheckanswercheck editcheckcheck editcheckdeclcheck editchecknotecheck edithandlehintcheck editpayloadbinarycheck editplancheck editplanpayloadconfinecheck editplanrecheckcheck editplanrollbackmsgcheck editpreviewcheck editroundtripcheck edittargetfileabscheck eliximportcheck elixircheck elixirnamearitycheck elixirsemanticcheck emitescapecheck emittertruthcheck emptycorpuscheck emptyvaluerefusecheck ensembleavailcheck ensemblecheck enumtablecheck essentialcxcheck estchargecheck evalcheck evictioncheck exemplarcheck exemplarconfcheck exercisescheck expandbodyfirstcheck expandcallscheck expandmodecheck expandrangecheck expandsibscheck expandtokencheck expandtopk0check extentcheck externalvetocheck falseedgecheck fcallforwardcheck fficheck fieldaffinitycheck fieldidcheck fieldnarrowcheck fieldusescheck filerootcheck fileselectorrefusecheck fillordercheck fixedbufsweep flagscheck flagsnoisecheck flagsurfacecheck flagtablecheck flipcheck floormarkcheck fnliteralcheck fnptrcheck forautobodycheck forblowupcheck forbodyfoldcheck forbodyslotcheck forbudgetmonotoncheck forcalibfactscheck forcompresscheck fordisclosurecheck forhdrshapecheck forheadrrcheck forlenscheck formatgatecheck formaxtokenscheck formembernamescheck fornotesbudgetcheck fornotesjsoncheck forrankordercheck forrootlegendcheck forsectioncollapsecheck forsigspancheck forsoftbudgetcheck forwidencheck freshclonecheck freshnesscheck g1configcheck gateabilitycheck gatecountcheck gateexitcheck gdscriptcheck genrecallcheck gitchildoptscheck gitenvhermeticcheck githardencheck gitignorecheck gitquotepathcheck gitstampcheck goinstcheck gointerfacecheck gokindcheck goscopecheck graphlegendbudgetcheck graphqueryrefusecheck grepanchorcheck grepandcheck grepbytescheck grepcheck grepcontextcheck grepcorpuscheck grepfastcheck grepfollowupcheck grepignorecheck grepscancheck grepseamcheck greptiercheck guardmsgcheck hasacheck hazardpatterncheck headbinstagecheck headsnapcachecheck helpbudgetcheck hermesinstallcheck historyoraclecheck hookcheck hop1check hostilecheck hotspotsincecheck htmlcolorcheck htmlhostcheck htmlrendercheck identitycheck impactdepthcheck impactimportcheck impactpartitioncheck impacttypecheck importcapcheck importnarrowcheck includeanglecheck includeprecisecheck indexoutcheck infraportcheck isolateprovenancecheck javamethodrefcheck javarubycheck jslangcheck jsmetricscheck jsnestedcheck jsoncheck jsonlangcheck jsonparitycheck jsonredactcheck jsonrefusallegendcheck jsonwalkcheck jsshapecheck jsverbscheck jsxcallcheck knownitemcheck kotlincheck landingcheck langcensuscheck langcheck langidentitycheck layerquerycheck layoutcheck lb3namecheck legendcostcheck legendcoveragecheck legenddriftcheck legendrefcheck legobundlecheck legocheck legotransitivecheck liftdisclosurecheck limitstablecheck lintbudgetcheck lintcatalogcheck lintcheck lintdedupcheck lintpayloadcapcheck lintprecisioncheck lintrulescheck lintscopecheck lintselectcheck listingpagingcheck localdeclcheck localitycheck localscountcheck loopconservationcheck lpincheck luacheck luarequirecheck macroedgecheck macroreparsecheck manifestcheck mapdatasectioncheck mapdiffcheck mapinventorycheck matchcapturecheck matchgrammarcheck maxfilesizecheck mcpattrparitycheck mcpaudit4hardencheck mcpclidiffcheck mcpcodexmetacheck mcpcontractcheck mcpdegradedhintcheck mcpeditcheck mcpeditkindcheck mcpeditmodecheck mcpeditpresencecheck mcpeditracecheck mcpflagshipcheck mcpforparitycheck mcpframehonestycheck mcpgrepdegradedcheck mcphandlecheck mcpincrementalcheck mcpmanifestcheck mcprangeedgecheck mcpreadloopcheck mcpredactcheck mcpreloadasancheck mcpreloadcheck mcpremotecheck mcprobustcheck mcpslicecheck mcpstalecheck mcpstdiolinecapcheck mcpstrictschemacheck mcptoolprunecheck mcptoolsubsetcheck mcptranchecheck mcptwinclaimscheck mcpverbscheck mcpw2fixcheck mcpw3fixcheck mcpwatchercheck mdembedcheck mdsectioncheck memguardcheck mentioncapcheck mentioncheck mentionsverbcheck mergechurncheck mergescoutcheck mergescoutlonglinecheck metalcheck meterdisclosurecheck metricscheck modifierguardcheck moduleconstcheck morecontractcheck mrowalkcheck multirootcheck multiswecheck namedfileinputcheck nameinfocheck namingcalibrationcheck namingconsistencycheck naminglenscheck naminglocalscheck narrowcheck narrowlangcheck neighbourcapcheck nestedimportcheck nestedqualcheck nestprofilecheck nextverbcheck noaliascheck nodekindcheck nongitqmetricscheck nonlocalstatecheck notecanoncheck notescheck notesdegradecheck nsfiltercheck nulbytecheck numericrefusecheck objcfieldcheck objcsniffcheck opencodewrapcheck optremarkscheck optremarkshotcheck ordercheck osswitchcheck oswin32logiccheck outlinecheck overbudgetcommentcheck ownerhopcheck ownerscheck packcallersharecheck packtaskcheck packtaskmonotoncheck packtaskquotacheck padscalecheck paginationcheck pagingsweepcheck panellegendcheck pargatescheck parsehealthcheck partitioncheck pathgapcheck pathownercheck patterncheck perfharnesscheck phpcheck pincensuscheck planlanescheck planlintcheck pmccheck portablebuildcheck portablecachecheck postingscheck ppaltcheck ppdeadrolescheck pranchorcheck prbudgetcheck prcheck prcontextcheck prconvergecheck precedencecheck preproccondcheck preprocdeadscalecheck prmaskanchorcheck prnestedcapcheck probecheck propcostcheck prrefsafecheck prrenamecheck pyimportprecisecheck pymodulealiascheck pyshapecheck qackconcurrencycheck qackorigincheck qbaselineproducercheck qchurncheck qchurnmemocheck qddegradecheck qddialscheck qdrefpaircheck qextractionkeycheck qoriginoraclecheck qrevtokencheck qrowlocatorcheck qschemetripcheck qsnapcachecheck qsnapprefetchcheck qsnapproducercheck qualifiedresolvecheck qualitycheck qualitycrosslangcheck qualityexcludecheck qualitykeycheck qualitykindscheck qualityorigincheck qualitypanelcheck qualityscopecheck qualitysignalcheck qualitystalecheck qualitysymcheck qualnewcheck querycheck queryfilescancheck racymtimecheck radixsimdcheck rangecomposecheck rankbycheck reachcheck readabilitycheck readmedriftcheck readmeexamplecheck recallanchorcheck recallboundarycheck recallbudgetcheck recallbufcheck recallevalcheck recallparitycheck recallpassagecheck recallrankdepthcheck recallrelcheck recallshapecheck recalltablecheck recalltotalcheck receiptpostcheck receiverevidencecheck recentscopecheck redactcheck redactfixcheck refusaltailcheck regexbombcheck regexcheck regexguardcheck regexrefusecheck registermacrocheck relevancefloorcheck relinkcheck reportcheck resolvecheck resolverhonestycheck retrievalqualitycheck reusefirstworkflowcheck ripwirepubliccheck rootrelcheck rootrelemitcheck rootspellingcheck routecheck routeedgecheck routehookcheck routeoncecheck routingreportcheck rubyargcheck rubyattrscheck rubybarecallcheck rubyclassrecvcheck rubyconstcheck rubydeclrefcheck rubydescribedclasscheck rubyinheritcheck rubymetricscheck rubyrakejbuildercheck rubyreachcheck rubyrecvcheck rubyrecvnarrowcheck rubyrequirecheck rubyrspectargetcheck rubyschemacheck rubyscopecheck rubysettercheck rubytypedrecvcheck runhintcheck runtracecheck rustanccheck rustimportprecisecheck rustqualcheck safedeletecheck sarifcheck savecachecheck scipcheck scipjoincheck scorecardcheck scoutheadconflictcheck scoutkeycheck scroundtripcheck seedboundscheck selectorchaincheck selectorhonestycheck selectorrefusecheck selectorscopecheck selfcheckcheck selfcontainedcheck shadowcheck shallowhistorycheck shapingflagcheck shellgateindexcheck showcasecapturecheck sibliftcheck sidecarsymlinkcheck sigredactcheck sincecheck sincecochangecheck sincewindowcheck singledefcheck situdiffcheck situshapecheck skilldescbudgetcheck skillevalcheck skillevalsplitcheck skillinstallcheck skillroutingjudgedcheck skillscanreadcheck skilltruthcheck skipclassifycheck skippedcheck skipreasoncheck slicecheck slicediffcheck sliceflowcheck sliceflowsenscheck slotjoincheck spectimingcheck staleackcheck statgatecheck stdmembergatecheck stdqualcheck strkerncheck structlayoutcheck sublistcountcheck subscriptopcheck substrfiltercheck subtokencheck svectorcheck swiftcheck swiftmemberscheck swiftshapecheck taskechocheck tempfilesymlinkcheck termmargincheck testedreachcheck testgatecheck testgatelegendbudgetcheck testgatepagecheck testgaterefusecheck testmacrocheck testrowruncheck testscopecheck textdocscheck timsortcheck tokenbudgetcheck tomllangcheck toolcallroutecheck tornreadcheck traceasanlinearcheck tracecheck tracehandoffcapcheck tracehopcheck traceminecheck treecheck truncvocabcheck tsimportprecisecheck tsshapecheck type3check type3clonecheck typerefcheck unreachablecheck unresolvedcheck usescheck usesliteralcheck usesselectorcheck usingdeclcheck utf8scrubcheck vendoredassetcheck vendoredbundlecheck vendorpatchcheck verifycheck versioncheck w2verbscheck w3fixbudgetcheck w3fixlegendcheck weaksignalcheck withgraphcheck withprofilecheck worktreeleakcheck wrapverbscheck writetargetcheck xmlwellformed yamllangcheck zonecheck zoneconsistencycheck zoomcheck; do [ -f "$ROOT/test/$_g.sh" ] || continue if RIPWIRE_BIN="$BIN" bash "$ROOT/test/$_g.sh" >/dev/null 2>&1; then ok "absorb gate ($_g.sh)" From fddfb32a03b67499256d3d6f021cc57e6b2e7986 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I <210578314+joyful-ii-V-I@users.noreply.github.com> Date: Fri, 23 Oct 2026 09:33:29 -0400 Subject: [PATCH 38/77] test(hop1): hop1check reports through the house accumulator (gateexitcheck C) Co-Authored-By: Claude Opus 5.5 --- test/hop1check.sh | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/test/hop1check.sh b/test/hop1check.sh index 13c9fcdcc..b4e0519fc 100755 --- a/test/hop1check.sh +++ b/test/hop1check.sh @@ -397,6 +397,11 @@ check( "(L2) the compact path legend defines slot_callers=", "s via=slot|slot-na check( "(L2) the compact impact legend defines reaches_hedged= and the ring row", "reaches_hedged=N:" in ti and "rh called_by= called_at= via= calls=:" in ti ) sys.exit( fail ) PY -rc=$? -if [ "$rc" -eq 0 ]; then echo "ALL PASS"; else echo "FAIL"; fi -exit "$rc" +rc=$? # the harness's own status, read immediately (checklist 14): 0 every arm passed, 1 an arm failed, anything else a crash +fail=0 +if [ "$rc" -ne 0 ]; then + printf ' FAIL the arm harness exited %s (an arm failed above, or the harness crashed)\n' "$rc" + fail=1 +fi +[ "$fail" = 0 ] && echo "ALL PASS" || echo "FAILURES ABOVE" +exit "$fail" From 5cd062d7056b6e9ddb23c2477dcfb527184588b2 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I <210578314+joyful-ii-V-I@users.noreply.github.com> Date: Fri, 23 Oct 2026 10:29:01 -0400 Subject: [PATCH 39/77] pathgaps fix 5: aliased member imports, dotted import root, PEP 695 type parameters (rule 2c base binding) An aliased from-import records its member name (importedName from:x): a bare clause naming the alias is withheld, a qualified one reads the member as the submodule. An unaliased dotted import binds the root package, so it files no module. A class or def type parameter is a VarDecl. qschemetrip re-pinned. Co-Authored-By: Claude Sonnet 5.5 --- CHANGELOG.md | 8 +++-- src/graph.h | 36 +++++++++++++++++------ src/ingest_binds.h | 29 ++++++++++++++++++ src/ingest_cache.h | 2 ++ src/ingest_importcap.h | 3 +- src/model.h | 3 ++ test/pathgapcheck.sh | 63 ++++++++++++++++++++++++++++++++++++++++ test/qschemetrip.hash | 2 +- test/qschemetripcheck.sh | 4 +++ 9 files changed, 137 insertions(+), 13 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d61a7302..3c19e8b66 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -38,12 +38,16 @@ not published here — see `docs/EVALS.md` for the instruments behind the headli the class statement's own scope chain (its class body or function, the enclosing functions, then the module; an enclosing class body is skipped, as Python skips it): an assignment of any form (`Base = Other`, `Base: type = Other`), a def, class, loop, `with` or `except` target, a parameter, a `global` or `nonlocal` (also in a def that starts the - file), a `match` capture (`case Base:`, `case P as Base`, `*Base`), a PEP 695 `type Base = …`, a second import, a `try: + file), a `match` capture (`case Base:`, `case P as Base`, `*Base`), a PEP 695 `type Base = …` or a class or def type + parameter (`class Sub[Base](Base)`), a second import, an import under another name (`from m import Other as Base`, which + binds m's `Other`, never `Base`), a `try: from a import Base / except ImportError: Base = None` fallback, or a star import that could bind it (one from a module outside the tree, or from a module that defines, imports or star-imports the name), wherever in the scope it sits and in either order. A qualified base (`q.Base`) is also withheld when another function or class imports `q` as a module (`import a.b as q`), since the import tables are per file; this over-hedges the same import repeated in two scopes, - and a star import from an in-tree module whose `__all__` omits the name. The import must be the name's one binding; + and a star import from an in-tree module whose `__all__` omits the name. `from pkg import other as a` with `a.Base` now + reads `other` (the member's own name), not `a`; a dotted `import pkg.x` binds the root `pkg`, which no table names, so + `pkg.Base` is withheld (this also over-hedges `import pkg.x as pkg`). The import must be the name's one binding; otherwise the site is withheld, never shown. Not read, and not detectable statically: a rebinding made from outside the file (`a.Base = Other` on an imported module). diff --git a/src/graph.h b/src/graph.h index 72abd80cb..24dcd7931 100644 --- a/src/graph.h +++ b/src/graph.h @@ -2025,12 +2025,14 @@ inline CallDisposition javaCandidateRefused( Graph& g, const Reference& r, bool // the veto fires only when the name's in-repo definitions are ALL members. // one binding of a Python name: the scope it sits in (the enclosing Function/Method/Class symbol; kNoNode = module scope), whether // it is an import (a rebinding of any other form is not) and, for an import, whether it binds a MODULE (`import a.b as q`: the one -// form importBindFile records) rather than a member (`from a import q`: memberImportFile) +// form importBindFile records) rather than a member (`from a import q`: memberImportFile); `member` is the member's own name in m when +// the import renames it (`from m import x as q`: "x") struct PythonNameBind { NodeId scope = kNoNode; bool isImport = false; bool moduleForm = false; + std::string member; // `from m import x as y` (y != x): "x", the member's own name in m; empty for every other form }; // the scope facts a Python class statement's base name is judged by (pythonBaseNameProven) @@ -2091,6 +2093,12 @@ inline void recordImportBindFile( const Binding& b, std::uint32_t resolved, cons // absolute spec only (never relative); root-scoped to b.fileId's own root in a multi-root workspace moduleFile = resolvePythonModuleSuffix( b.typeName, fileIndex, fileRoot, b.fileId ); } + // ind4b fix 5 (E3): `import pkg.x` binds the ROOT `pkg` (pkg/__init__.py), not pkg.x, but `import pkg.x as pkg` binds pkg.x and the + // record cannot tell them apart (same var, same typeName): a dotted spec whose first segment is the bound name names no ONE file. + if( b.typeName.starts_with( b.var ) && b.typeName.size() > b.var.size() && b.typeName[ b.var.size() ] == '.' ) + { + moduleFile = kNoFile; + } // issue #287 round 3 (review rv-p6.md HIGH): try_emplace runs even when THIS import is unresolved // (moduleFile==kNoFile) — an import whose target is outside the indexed tree (stdlib/third-party/ // unknown) is STILL a rebinding of `b.var`, not an absence of one. Skipping the emplace here (the @@ -2201,7 +2209,9 @@ inline HashMap> buildPythonNameBinds( c && wanted.contains( b.var ) && isPythonSourcePath( rootRelPath( ing, b.fileId ) ) ) { key.clear(); Narrower::appendUint( key, b.fileId ); key.push_back( '#' ); key.append( b.var ); - binds[ key ].push_back( PythonNameBind{ pythonBindScope( ing, b ), b.kind == LocalBindKind::Import, b.importedName == "module" } ); + const bool member = b.importedName.starts_with( kPythonFromMemberMark ); + binds[ key ].push_back( PythonNameBind{ pythonBindScope( ing, b ), b.kind == LocalBindKind::Import, b.importedName == "module", + member ? b.importedName.substr( kPythonFromMemberMark.size() ) : std::string{} } ); } } return binds; @@ -6444,8 +6454,8 @@ inline bool pythonStarMayBind( const ExternalVetoTables& v, std::uint32_t f, std // The import TABLES that resolve the name are file-wide, though, so an import outside the chain can still be the one they hold: // a qualified clause (`q.Base`) reads importBindFile first, which only a module-form import (`import a.b as q`) fills, so any such // import of `q` outside the chain leaves it undecided; a bare clause reads memberImportFile alone, so its import must be the member form -// (`from m import Base`), where two modules already degrade to kNoFile. -inline bool pythonBaseNameProven( const BaseEvidenceTables& t, const Reference& r ) +// (`from m import Base`), where two modules already degrade to kNoFile. Returns that one binding (the caller reads its renamed member), else null. +inline const PythonNameBind* pythonBaseNameProven( const BaseEvidenceTables& t, const Reference& r ) { const bool qualified = !r.memberRoot.empty(); // `q.Base`: the clause starts with the qualifier, else with the bare base const std::string_view name = qualified ? std::string_view( r.memberRoot ) : std::string_view( r.calleeName ); @@ -6453,11 +6463,12 @@ inline bool pythonBaseNameProven( const BaseEvidenceTables& t, const Reference& const auto it = t.extVeto.pythonScopes.nameBinds.find( fileNameKey( key, r.fileId, name ) ); if( it == t.extVeto.pythonScopes.nameBinds.end() || r.fromSymbol == kNoNode ) { - return false; + return nullptr; } const bool scoped = std::ranges::any_of( it->second, []( const PythonNameBind& b ) { return b.scope != kNoNode; } ); const std::vector chain = scoped ? pythonClassScopeChain( t.ing, r.fromSymbol ) : std::vector{}; int here = 0; + const PythonNameBind* sole = nullptr; bool importOnly = false; bool moduleForm = false; bool outsideForm = false; // an import of the name outside the chain that binds a module @@ -6466,6 +6477,7 @@ inline bool pythonBaseNameProven( const BaseEvidenceTables& t, const Reference& if( b.scope == kNoNode || std::ranges::find( chain, b.scope ) != chain.end() ) { ++here; + sole = &b; importOnly = b.isImport; moduleForm = b.moduleForm; } @@ -6474,7 +6486,7 @@ inline bool pythonBaseNameProven( const BaseEvidenceTables& t, const Reference& outsideForm = outsideForm || ( b.isImport && b.moduleForm ); } } - return here == 1 && importOnly && ( qualified ? !outsideForm : !moduleForm ) && !pythonStarMayBind( t.extVeto, r.fileId, name ); + return here == 1 && importOnly && ( qualified ? !outsideForm : !moduleForm ) && !pythonStarMayBind( t.extVeto, r.fileId, name ) ? sole : nullptr; } // tier 2, Python: `from m import X` (X bare), `import pkg.mod as q` (q.X), `from pkg import q` (q.X: q is pkg's submodule, @@ -6486,12 +6498,17 @@ inline void pythonImportedBases( const BaseEvidenceTables& t, const Reference& r const std::string_view qual = r.memberRoot; // the base's written qualifier on an inherit ref ("" bare) std::string key; const std::string_view first = qual.empty() ? std::string_view( r.calleeName ) : qual; - if( t.extVeto.pythonModuleRebind.contains( fileNameKey( key, r.fileId, first ) ) || !pythonBaseNameProven( t, r ) ) + const PythonNameBind* bind = pythonBaseNameProven( t, r ); + if( t.extVeto.pythonModuleRebind.contains( fileNameKey( key, r.fileId, first ) ) || bind == nullptr ) { return; } if( qual.empty() ) { + if( !bind->member.empty() ) + { + return; // `from m import Other as Base`: the candidates are named Base, the class the clause means is m's Other (E1) + } pythonReexportChain( t, cand, pythonImportFileOf( t.extVeto.memberImportFile, r.fileId, r.calleeName, key ), r.calleeName, out ); return; } @@ -6510,16 +6527,17 @@ inline void pythonImportedBases( const BaseEvidenceTables& t, const Reference& r return; } const std::string_view pkgDir = includerDir( rootRelPath( t.ing, pkg ) ); + const std::string_view link = bind->member.empty() ? qual : std::string_view( bind->member ); // `from pkg import other as a`: pkg's name is `other` (E2) for( NodeId c : cand ) { - if( pythonSubmoduleOf( rootRelPath( t.ing, t.ing.symbols[ c ].fileId ), pkgDir, qual ) && std::find( out.begin(), out.end(), c ) == out.end() ) + if( pythonSubmoduleOf( rootRelPath( t.ing, t.ing.symbols[ c ].fileId ), pkgDir, link ) && std::find( out.begin(), out.end(), c ) == out.end() ) { out.push_back( c ); } } if( out.empty() ) { - pythonReexportChain( t, cand, pkg, qual, out ); + pythonReexportChain( t, cand, pkg, link, out ); } } diff --git a/src/ingest_binds.h b/src/ingest_binds.h index e7e039868..12a684711 100644 --- a/src/ingest_binds.h +++ b/src/ingest_binds.h @@ -3339,6 +3339,34 @@ inline void capturePythonPatternDecls( TSNode n, std::uint32_t fileId, Lang lang pushRawBind( fileId, lang, nodeTextOf( id, src ), std::string{}, BindSite{ at, 0u, 0u }, LocalBindKind::VarDecl, binds ); } +// PEP 695 `class C[Base](Base)` / `def f[Base]()`: each type parameter is a name the base clause can resolve to (a TypeVar, so CPython +// raises TypeError and no derivation exists). One `type` per name; a bound `Base: int` wraps its name in a constrained_type, `*Ts` / `**P` +// in a splat_type. A def's parameters are visible inside it (a VarDecl at the list); a class's are visible to its own base clause, which +// the scope chain reads from OUTSIDE the class, so it is filed one byte before the class like the class's own name. +inline void capturePythonTypeParamDecls( TSNode n, const char* t, std::uint32_t fileId, Lang lang, std::string_view src, std::vector& binds ) +{ + const bool isClass = kindIs( t, "class_definition" ); + const TSNode list = isClass || kindIs( t, "function_definition" ) ? fieldChild( n, NodeField::TypeParameters ) : TSNode{}; + if( ts_node_is_null( list ) ) + { + return; + } + const std::uint32_t own = ts_node_start_byte( n ); + const std::uint32_t at = isClass ? ( own > 0 ? own - 1 : 0 ) : ts_node_start_byte( list ); + for( std::uint32_t i = 0, k = ts_node_named_child_count( list ); i < k; ++i ) + { + TSNode c = ts_node_named_child( list, i ); // `type`, then (constrained_type > type | splat_type) > ... > identifier + for( int depth = 0; depth < 4 && !ts_node_is_null( c ) && !kindIs( ts_node_type( c ), "identifier" ); ++depth ) + { + c = ts_node_named_child_count( c ) > 0 ? ts_node_named_child( c, 0 ) : TSNode{}; + } + if( !ts_node_is_null( c ) && kindIs( ts_node_type( c ), "identifier" ) ) + { + pushRawBind( fileId, lang, nodeTextOf( c, src ), std::string{}, BindSite{ at, 0u, 0u }, LocalBindKind::VarDecl, binds ); + } + } +} + inline void capturePythonRebindShadowDecls( TSNode n, const char* t, std::uint32_t fileId, Lang lang, std::string_view src, std::vector& binds ) { std::vector targets; // reused scratch, cleared before each shape below @@ -3473,6 +3501,7 @@ inline void captureShadowScopeDecls( TSNode n, const char* t, std::uint32_t file } capturePythonRebindShadowDecls( n, t, fileId, lang, src, binds ); // issue #287 round 2: rebind veto evidence capturePythonPatternDecls( n, fileId, lang, src, binds ); // ind4b fix 4: match captures and `type` statements + capturePythonTypeParamDecls( n, t, fileId, lang, src, binds ); // ind4b fix 5: PEP 695 type parameters return; } if( lang != Lang::Cpp && lang != Lang::ObjC ) diff --git a/src/ingest_cache.h b/src/ingest_cache.h index c83e55853..eb8f7abd3 100644 --- a/src/ingest_cache.h +++ b/src/ingest_cache.h @@ -351,6 +351,8 @@ constexpr std::uint32_t kParserVer = 224; // bump on any grammar/.sc // emits a LocalBindKind::VarDecl for a match-statement capture (`case Base:`, `case P as Base`, `Cls(x=Base)`, // `*Base`, `**Base`) and a PEP 695 `type Base = ...`, so a Python base name rebound that way is no binding the // rule 2c proof can read. Same record layout: kCacheVersion unchanged. + // Fix round 5 (still 224, the placeholder is unreleased): an aliased `from m import x as y` records "from:x" in the Import + // binding's importedName (kPythonFromMemberMark), and a PEP 695 class/def type parameter is a VarDecl. // 212 = 2026-10-21 (train 32a: member-names + rust-receivers): strictly above every live number — main 206 (rich 207), // lane rust-receivers 202 (203), lane ix1-slot 204 (205), lane l6-iface 210 (211); #420 reserves 214 (215), so train // 32b can go above. Rich file tag 213. kCacheVersion stays 36: neither lane changed a record format (rust-receivers diff --git a/src/ingest_importcap.h b/src/ingest_importcap.h index 31a5dcdbb..628a86010 100644 --- a/src/ingest_importcap.h +++ b/src/ingest_importcap.h @@ -307,6 +307,7 @@ void emitCapturedPythonImportBind( TSNode namesNode, TSNode aliasNode, std::uint bound = bound.substr( 0, bound.find( '.' ) ); // `import a.b` binds `a`, not `a.b` } std::string target = importSpecifierText( isFrom ? first : namesNode, src ); + const bool renamed = isFrom && hasAlias && nodeTextOf( namesNode, src ) != bound; // `from m import x as y`: y alone names no member of m if( bound.empty() || target.empty() ) { return; @@ -318,7 +319,7 @@ void emitCapturedPythonImportBind( TSNode namesNode, TSNode aliasNode, std::uint b.kind = LocalBindKind::Import; b.var.assign( bound ); b.typeName = std::move( target ); - b.importedName = isFrom ? std::string{} : "module"; + b.importedName = !isFrom ? "module" : renamed ? std::string( kPythonFromMemberMark ).append( nodeTextOf( namesNode, src ) ) : std::string{}; binds.push_back( std::move( b ) ); } diff --git a/src/model.h b/src/model.h index 31a38c3da..38c0704d5 100644 --- a/src/model.h +++ b/src/model.h @@ -1127,6 +1127,8 @@ enum class LocalBindKind : std::uint8_t // import x [as y]` (`var` names a MEMBER of m, not m). The module-alias receiver narrow // (graph.h buildGraph) reads this to refuse `x.attr()` when `x` was bound the second way // — `x` there is a value from inside m, and `.attr` is not a lookup in m's own namespace. + // ind4b fix 5: `from m import x as y` with y != x records "from:x" (kPythonFromMemberMark + x) — the + // member's ORIGINAL name, which `var` (the local alias) loses; a plain `from m import x` stays empty. JsImport, // named ES import: var=local name, typeName=module, importedName=export (empty for type-only). JsExport, // ES export: var=EXPORTED name; importedName=the LOCAL name it binds (empty on the declaration // form, where the two are the same word). spanStart/spanEnd is the region a definition must @@ -1338,6 +1340,7 @@ inline constexpr std::uint8_t kWholeResult = 0xFF; // (LocalBindKind::RubySingletonDef, RubyClassMixin; parser version 145): written in a concern's `included do`, or a def // in its `class_methods do`. inline constexpr std::string_view kRubyIncluderMark = "included"; +inline constexpr std::string_view kPythonFromMemberMark = "from:"; // Python Import importedName prefix: `from m import x as y` records "from:x" (never equals "module") inline constexpr std::string_view kRubyClassMethodsMark = "class_methods"; static_assert( enumCountIsExact(), "kLocalBindKindCount must name the LAST LocalBindKind enumerator — move it with the append" ); diff --git a/test/pathgapcheck.sh b/test/pathgapcheck.sh index 81db372ce..c63d6b72a 100755 --- a/test/pathgapcheck.sh +++ b/test/pathgapcheck.sh @@ -83,6 +83,9 @@ # (K17-K18) fix 4: the file-wide import tables never decide for an import outside the class's chain (qualmod, qualfn), a `global` # rebinding in a def at byte 0 is module scope (glob0), a match-statement capture and a PEP 695 `type` statement are bindings # (matchas/matchbare/matchsplat/matchkw/matchdict/matchfn, typestmt); K18 qualctl and matchctl stay a way in. +# (K19-K20) fix 5: `from m import Other as Base` (memberalias), `from pkg.x import other as a` (qualalias) and `import pkg.x` (dottedroot, +# binds the root) name something else than the written name says, and a PEP 695 type parameter shadows the base (tpclass, +# tpdef): all withheld; K19 qualok now PROVES `from pkg.x import a as q` (q.Base) through the recorded member name; K20 stays. # # Usage: test/pathgapcheck.sh [BIN] (BIN defaults to RIPWIRE_BIN, then build/ripwire) set -u @@ -568,6 +571,40 @@ for v in qualmod qualfn qualctl glob0 glob1 matchas matchbare matchsplat matchkw printf "$out" > "$d/other/b.py" done # K17 end +# K19 fixtures (lane ind4b fix 5): the import forms whose local name is not the name the module holds, and PEP 695 type parameters. The +# tail of every fixture is `class Sub(): def render_line` calling target(); CPython (3.13) truth noted per variant. +# memberalias: `from pkg.x.a import Other as Base` - Sub's base is pkg.x.a.Other, not the Base this file also happens to hold (E1) +# qualalias: `from pkg.x import other as a` + Sub(a.Base) - a is pkg/x/other.py, not pkg/x/a.py (E2) +# dottedroot: `import pkg.x` binds the ROOT package pkg: pkg.Base is pkg/__init__.py's class, not pkg/x/__init__.py's re-export (E3) +# tpclass/tpdef: `class Sub[Base](Base)` / `def make[Base](): class Sub(Base)` - the type parameter is the name (TypeError, no base) (N8) +# Controls (stay a way in): sameas (`import Base as Base`), qualok (`from pkg.x import a as q` + q.Base is pkg/x/a.py), outform (an +# unrelated function's `import pkg.y.base2 as Base` does not touch the bare member import), dotted2 (`import pkg.x.a as q` + q.Base) +# K19 begin +for v in memberalias qualalias dottedroot tpclass tpdef sameas qualok outform dotted2; do d="$TMP/c5_$v"; mkdir -p "$d/pkg/x" "$d/pkg/y" "$d/other" + printf 'class Base:\n def render_line(self):\n return 1\n' > "$d/pkg/x/a.py" + printf 'from pkg.x.a import Base\ndef run(obj):\n return obj.render_line()\n' > "$d/pkg/x/c.py" + printf 'class Base:\n def other(self):\n return 3\n' > "$d/pkg/y/base2.py" + : > "$d/pkg/__init__.py"; : > "$d/pkg/x/__init__.py"; : > "$d/pkg/y/__init__.py" + tgt='def target():\n return 2\n\n' + sub() { printf 'class Sub(%s):\n def render_line(self):\n return target()\n' "$1"; } + case "$v" in + memberalias) printf 'class Other:\n pass\n' >> "$d/pkg/x/a.py" + out="from pkg.x.a import Other as Base\n\n${tgt}$(sub Base)\n" ;; + qualalias) printf 'class Base:\n pass\n' > "$d/pkg/x/other.py" + out="from pkg.x import other as a\n\n${tgt}$(sub a.Base)\n" ;; + dottedroot) printf 'class Base:\n def other(self):\n return 4\n' > "$d/pkg/__init__.py" + printf 'from pkg.x.a import Base\n' > "$d/pkg/x/__init__.py" + out="import pkg.x\n\n${tgt}$(sub pkg.Base)\n" ;; + tpclass) out="from pkg.x.a import Base\n\n${tgt}class Sub[Base](Base):\n def render_line(self):\n return target()\n" ;; + tpdef) out="from pkg.x.a import Base\n\n${tgt}def make[Base]():\n class Sub(Base):\n def render_line(self):\n return target()\n return Sub\n" ;; + sameas) out="from pkg.x.a import Base as Base\n\n${tgt}$(sub Base)\n" ;; + qualok) out="from pkg.x import a as q\n\n${tgt}$(sub q.Base)\n" ;; + outform) out="from pkg.x.a import Base\ndef g():\n import pkg.y.base2 as Base\n return Base\n\n${tgt}$(sub Base)\n" ;; + dotted2) out="import pkg.x.a as q\n\n${tgt}$(sub q.Base)\n" ;; + esac + printf "$out" > "$d/other/b.py" +done +# K19 end nsblock(){ grep -o ']*>.*' "$1"; } nshead(){ grep -o ']*>' "$1"; } nsrows(){ grep -o ']*/>' "$1"; } @@ -1090,6 +1127,32 @@ for fx in qualctl:10:6 matchctl:13:9; do v=${fx%%:*}; rest=${fx#*:}; ln=${rest%% "" done +# K19 (lane ind4b fix 5): the names an import does NOT bind, and PEP 695 type parameters (each withheld arm is shown + bridged at fix 4's head). +# `from m import Other as Base` binds the local Base to m's Other (memberalias); `from pkg.x import other as a` binds a to pkg/x/other.py +# (qualalias); `import pkg.x` binds the root pkg, not pkg.x (dottedroot); a class/def type parameter is a TypeVar, never a base (tpclass, tpdef). +# qualok (`from pkg.x import a as q`, q is pkg/x/a.py) is the one that GAINS: the alias is resolved through the recorded member name. +# sameas, outform and dotted2 stay a way in (K20): an alias equal to the name, an unrelated function's import, a dotted import with an alias. +for fx in memberalias:7 qualalias:7 dottedroot:7 tpclass:7 tpdef:8; do v=${fx%%:*}; ln=${fx##*:} + case "$v" in + memberalias) why="from pkg.x.a import Other as Base: Sub's base is pkg.x.a.Other, not the Base that file also holds" ;; + qualalias) why="from pkg.x import other as a; Sub(a.Base): a is pkg/x/other.py, not pkg/x/a.py" ;; + dottedroot) why="import pkg.x binds the root package pkg; pkg.Base is pkg/__init__.py's class" ;; + tpclass) why="class Sub[Base](Base): the type parameter is the name (CPython: TypeError, no base)" ;; + tpdef) why="def make[Base](): class Sub(Base): the function's type parameter is the name" ;; + esac + nsArm "(K19) 2c import/type-parameter binding ($v): $why — withheld" "$TMP/c5_$v" "run,target" \ + '' \ + "" + brArm "(K19b) 2c import/type-parameter binding ($v): no namesake bridge to it either" "$TMP/c5_$v" "run,target" '' +done +for fx in sameas:7:3 qualok:7:3 outform:10:6 dotted2:7:3; do v=${fx%%:*}; rest=${fx#*:}; ln=${rest%%:*}; tl=${rest##*:} + nsArm "(K20) 2c import binding ($v): Base is still pkg/x/a.py's class where Sub is defined — a way in" "$TMP/c5_$v" "run,target" \ + '' \ + "" + brArm "(K20b) 2c import binding ($v): the bridge crosses to the override" "$TMP/c5_$v" "run,target" \ + "" +done + echo "(M) MCP path_between carries the same clause" # Two MCP legend postures (lean-answers): the unchanged assertions run on --mcp-legend=inline, the pre-posture spelling # where every answer carries its root facts on ; a TWIN runs the default session posture, whose answers after the diff --git a/test/qschemetrip.hash b/test/qschemetrip.hash index e60bed005..bb7d2fe11 100644 --- a/test/qschemetrip.hash +++ b/test/qschemetrip.hash @@ -1 +1 @@ -c4422a58e4e5431372e8b06423ab8791a0c73496499b838c3dd548825962d607 +c48ab251f48f26c04e91054bfef4c1446cbab7f25b4074be1283d1a51d8b0d07 diff --git a/test/qschemetripcheck.sh b/test/qschemetripcheck.sh index e6ec064df..ed8c62a38 100755 --- a/test/qschemetripcheck.sh +++ b/test/qschemetripcheck.sh @@ -34,6 +34,10 @@ 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-23, lane ind4b-k5-tighten-k5c fix round 5: RE-PINNED with UPDATE_GOLDEN=1. Fix round 4 moved kParserVer 212 -> 224 (rich file tag +# 225; quality.h's kIngestParserVerMirror with it) and left the pin; the manifest hashes that line and the comment under it, which this +# round also grew. kParserVer 224 is a PLACEHOLDER the train renumbers, and the train re-pins again then. kCacheVersion stays 36 and +# kQSnapCacheScheme stays 22: no record format and no watched quality.h function moved (importedName gains a value, same field). # 2026-10-21, train 32a (member-names 781a22fb, rust-receivers dd378171 merged onto main f156be85): RE-DERIVED ONCE with # UPDATE_GOLDEN=1 on the merged tree (a667ffc55e…8a05 -> c4422a58e4…2d607). Watched text: kParserVer 206 -> 212 (rich file tag # 213; above main 206 / 207, lane rust-receivers 202 / 203, ix1-slot 204 / 205, l6-iface 210 / 211; #420 reserves 214 / 215) and From 7f190c6d4ed3554c3b84986a9e14433f27d07cb7 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I <210578314+joyful-ii-V-I@users.noreply.github.com> Date: Fri, 23 Oct 2026 10:32:20 -0400 Subject: [PATCH 40/77] test(honestlexcheck): if/else verdicts and a write-failure-safe ok() (gateexitcheck G1/G2) Co-Authored-By: Claude Opus 5.5 --- test/honestlexcheck.sh | 76 ++++++++++++++++++++++-------------------- 1 file changed, 40 insertions(+), 36 deletions(-) diff --git a/test/honestlexcheck.sh b/test/honestlexcheck.sh index b7c230080..2134dbf24 100755 --- a/test/honestlexcheck.sh +++ b/test/honestlexcheck.sh @@ -24,7 +24,7 @@ ROOT="$( cd "$( dirname "$0" )/.." && pwd )" BIN="${1:-${RIPWIRE_BIN:-$ROOT/build/ripwire}}" [ "${BIN#/}" = "$BIN" ] && BIN="$ROOT/$BIN" fail=0 -ok(){ printf ' PASS %s\n' "$*"; } +ok(){ printf ' PASS %s\n' "$*" || { fail=1; printf ' FAIL could not write the PASS line for: %s\n' "$*"; }; return 0; } no(){ printf ' FAIL %s\n' "$*"; fail=1; } [ -x "$BIN" ] || { echo "no ripwire binary at $BIN — build first"; exit 2; } command -v python3 >/dev/null 2>&1 || { echo "python3 required"; exit 2; } @@ -101,7 +101,7 @@ print(m[0] if m else "MISSING")' "$1" "$2"; } arm() { # arm LABEL SYM FILE LINE WANT local out k; out="$( wh --exclude=hlxfix/ --whereis="$2" )" k="$( printf '%s' "$out" | kind_at "$3" "$4" )" - [ "$k" = "$5" ] && ok "$1 ($2 at $3:$4 kind=$k)" || no "$1 ($2 at $3:$4: kind=$k, want $5)" + if [ "$k" = "$5" ]; then ok "$1 ($2 at $3:$4 kind=$k)"; else no "$1 ($2 at $3:$4: kind=$k, want $5)"; fi } echo "── (A) lexical definition shape (head_labels=lexical forced) ──" @@ -144,17 +144,17 @@ arm "A34 an ObjC method '- (ret) NAME' is a definition" hlxObjc hlxfix echo "── (A) head_defs ──" o="$( wh --whereis=hlxComment )" -printf '%s' "$o" | grep -q 'head_labels="lexical" head_defs="0"' && ok "H1 no index def of the name → head_labels=lexical head_defs=0" \ - || no "H1 head_defs=0 missing: $( printf '%s' "$o" | grep -o ']*>' )" +if printf '%s' "$o" | grep -q 'head_labels="lexical" head_defs="0"'; then ok "H1 no index def of the name → head_labels=lexical head_defs=0" +else no "H1 head_defs=0 missing: $( printf '%s' "$o" | grep -o ']*>' )"; fi o="$( wh --whereis=hlx_indexed )" if printf '%s' "$o" | grep -q 'head_labels="index"' && ! printf '%s' "$o" | grep -q 'head_defs='; then ok "H2 an indexed def → head_labels=index, no head_defs" else no "H2 indexed name: $( printf '%s' "$o" | grep -o ']*>' )"; fi -[ "$( printf '%s' "$o" | kind_at keep.c 3 )" = ref ] && ok "H3 the index labels a comment mention of an indexed name ref" || no "H3 keep.c:3 not ref" +if [ "$( printf '%s' "$o" | kind_at keep.c 3 )" = ref ]; then ok "H3 the index labels a comment mention of an indexed name ref"; else no "H3 keep.c:3 not ref"; fi o="$( wh --whereis=hlxComment )" -printf '%s' "$o" | grep -q 'head_defs=0: ' && ok "H4 the default (compact) legend defines head_defs" || no "H4 head_defs undefined in the legend" +if printf '%s' "$o" | grep -q 'head_defs=0: '; then ok "H4 the default (compact) legend defines head_defs"; else no "H4 head_defs undefined in the legend"; fi o="$( wh --whereis=hlxComment --legend=full )" -printf '%s' "$o" | grep -q 'head_defs="0" beside it' && ok "H5 the full legend defines head_defs" || no "H5 head_defs undefined in the full legend" +if printf '%s' "$o" | grep -q 'head_defs="0" beside it'; then ok "H5 the full legend defines head_defs"; else no "H5 head_defs undefined in the full legend"; fi echo "── (B) grep: regex-shaped literals ──" cat > "$R/b.c" <<'EOF' @@ -166,58 +166,62 @@ printf 'hlxq hlxq\n' > "$R/q.scm" g() { ( cd "$R" && "$BIN" "$R" --no-cache "$@" 2>/dev/null ); } root() { grep -o ']*>' | head -1; } o="$( g --grep='hlxa|hlxb' | root )" -printf '%s' "$o" | grep -q 'pattern_meta="|"' && ok "B1 a|b literal → pattern_meta=|" || no "B1 $o" -printf '%s' "$o" | grep -q "next=\"--regex='hlxa|hlxb'\"" && ok "B2 its next= is the regex verb on the pattern" || no "B2 $o" +if printf '%s' "$o" | grep -q 'pattern_meta="|"'; then ok "B1 a|b literal → pattern_meta=|"; else no "B1 $o"; fi +if printf '%s' "$o" | grep -q "next=\"--regex='hlxa|hlxb'\""; then ok "B2 its next= is the regex verb on the pattern"; else no "B2 $o"; fi o="$( g --regex='hlxa|hlxb' | root )" -printf '%s' "$o" | grep -q 'pattern_meta=' && no "B3 a regex answer carries pattern_meta: $o" || ok "B3 a --regex answer carries no pattern_meta" -printf '%s' "$o" | grep -q 'hits="2"' && ok "B4 the named regex finds the two lines" || no "B4 $o" +if printf '%s' "$o" | grep -q 'pattern_meta='; then no "B3 a regex answer carries pattern_meta: $o"; else ok "B3 a --regex answer carries no pattern_meta"; fi +if printf '%s' "$o" | grep -q 'hits="2"'; then ok "B4 the named regex finds the two lines"; else no "B4 $o"; fi o="$( g --grep='hlxa.c' | root )" -printf '%s' "$o" | grep -q 'pattern_meta=' && no "B5 a lone literal dot is meta: $o" || ok "B5 a lone '.' (hlxa.c) is a literal: no pattern_meta" -printf '%s' "$o" | grep -q 'next="--at=' && ok "B6 …and keeps the --at next=" || no "B6 $o" +if printf '%s' "$o" | grep -q 'pattern_meta='; then no "B5 a lone literal dot is meta: $o"; else ok "B5 a lone '.' (hlxa.c) is a literal: no pattern_meta"; fi +if printf '%s' "$o" | grep -q 'next="--at='; then ok "B6 …and keeps the --at next="; else no "B6 $o"; fi o="$( g --grep='hlxa\.c' | root )" -printf '%s' "$o" | grep -q 'pattern_meta="\\"' && ok "B7 a backslash escape → pattern_meta=\\" || no "B7 $o" +if printf '%s' "$o" | grep -q 'pattern_meta="\\"'; then ok "B7 a backslash escape → pattern_meta=\\"; else no "B7 $o"; fi o="$( g --grep='||' | root )" -printf '%s' "$o" | grep -q 'pattern_meta=' && no "B8 '||' read as alternation: $o" || ok "B8 '||' (an operator) is no alternation" +if printf '%s' "$o" | grep -q 'pattern_meta='; then no "B8 '||' read as alternation: $o"; else ok "B8 '||' (an operator) is no alternation"; fi o="$( g --grep='java.util.*' | root )" -printf '%s' "$o" | grep -q 'pattern_meta=' && no "B9 a trailing .* read as meta: $o" || ok "B9 a trailing .* (java.util.*) stays literal" +if printf '%s' "$o" | grep -q 'pattern_meta='; then no "B9 a trailing .* read as meta: $o"; else ok "B9 a trailing .* (java.util.*) stays literal"; fi o="$( g --grep='hlxa.*hlxa' | root )" -printf '%s' "$o" | grep -q 'pattern_meta=".*"' && ok "B10 .* inside the pattern → pattern_meta=.*" || no "B10 $o" +if printf '%s' "$o" | grep -q 'pattern_meta=".*"'; then ok "B10 .* inside the pattern → pattern_meta=.*"; else no "B10 $o"; fi o="$( g --grep='hlxa\|hlxb' | root )" -printf '%s' "$o" | grep -q "next=\"--regex='hlxa|hlxb'\"" && ok "B11 basic-grep a\\|b → next= the ERE a|b" || no "B11 $o" +if printf '%s' "$o" | grep -q "next=\"--regex='hlxa|hlxb'\""; then ok "B11 basic-grep a\\|b → next= the ERE a|b"; else no "B11 $o"; fi o="$( g --grep='[^h]lxa' | root )" -printf '%s' "$o" | grep -q 'pattern_meta="\[\]"' && ok "B12 a [^…] class → pattern_meta=[]" || no "B12 $o" +if printf '%s' "$o" | grep -q 'pattern_meta="\[\]"'; then ok "B12 a [^…] class → pattern_meta=[]"; else no "B12 $o"; fi o="$( g --grep='a[0]' | root )" -printf '%s' "$o" | grep -q 'pattern_meta=' && no "B13 an index a[0] read as a class: $o" || ok "B13 a[0] (an index) is no bracket class" +if printf '%s' "$o" | grep -q 'pattern_meta='; then no "B13 an index a[0] read as a class: $o"; else ok "B13 a[0] (an index) is no bracket class"; fi o="$( g --grep='^hlxb' | root )" -printf '%s' "$o" | grep -q 'pattern_meta="^\$"' && ok "B14 a leading ^ → pattern_meta=^\$" || no "B14 $o" +if printf '%s' "$o" | grep -q 'pattern_meta="^\$"'; then ok "B14 a leading ^ → pattern_meta=^\$"; else no "B14 $o"; fi o="$( g --grep='hlxa|hlxb' --legend=full )" -printf '%s' "$o" | grep -q 'pattern_meta= (present only' && ok "B15 the full legend defines pattern_meta= when it rides" || no "B15 legend clause missing" +if printf '%s' "$o" | grep -q 'pattern_meta= (present only'; then ok "B15 the full legend defines pattern_meta= when it rides"; else no "B15 legend clause missing"; fi o="$( g --grep='hlxa' --legend=full )" -printf '%s' "$o" | grep -q 'pattern_meta= (present only' && no "B16 the pattern_meta clause rides without the attribute" || ok "B16 the clause is present-only" +if printf '%s' "$o" | grep -q 'pattern_meta= (present only'; then no "B16 the pattern_meta clause rides without the attribute"; else ok "B16 the clause is present-only"; fi echo "── (B) grep: one row per line, occ= ──" o="$( g --grep='hlxa' --grep-in=any )" -printf '%s' "$o" | root | grep -q 'hits="3"' && ok "B17 hits= counts LINES (3 lines hold hlxa, 4 occurrences)" || no "B17 $( printf '%s' "$o" | root )" -printf '%s' "$o" | grep -q ']* occ="2"' && ok "B18 the line holding hlxa twice is ONE row with occ=2" || no "B18 no occ=2 row" -[ "$( printf '%s' "$o" | grep -o ']* occ=' && no "B20 a single occurrence carries occ=" || ok "B20 a single occurrence carries no occ=" -g --grep='hlxa' --grep-in=any --legend=full | grep -q 'occ= on a row' && ok "B21 the full legend defines occ= when it rides" || no "B21 occ clause missing" +if printf '%s' "$o" | root | grep -q 'hits="3"'; then ok "B17 hits= counts LINES (3 lines hold hlxa, 4 occurrences)"; else no "B17 $( printf '%s' "$o" | root )"; fi +if printf '%s' "$o" | grep -q ']* occ="2"'; then ok "B18 the line holding hlxa twice is ONE row with occ=2"; else no "B18 no occ=2 row"; fi +if [ "$( printf '%s' "$o" | grep -o ']* occ='; then no "B20 a single occurrence carries occ="; else ok "B20 a single occurrence carries no occ="; fi +if g --grep='hlxa' --grep-in=any --legend=full | grep -q 'occ= on a row'; then ok "B21 the full legend defines occ= when it rides"; else no "B21 occ clause missing"; fi o="$( g --grep='hlxq' )" -printf '%s' "$o" | grep -q '/dev/null | tail -1 | python3 -c 'import sys,json; r=json.load(sys.stdin); print(r["result"]["content"][0]["text"] if "result" in r else "__ERROR__")'; } j="$( mcp 'hlxa|hlxb' )" -printf '%s' "$j" | python3 -c 'import sys,json; d=json.load(sys.stdin); sys.exit(0 if d.get("pattern_meta")=="|" and "--regex=" in d.get("next","") else 1)' \ - && ok "M1 MCP grep: pattern_meta | and next= the regex verb" || no "M1 MCP: $( printf '%s' "$j" | head -c 300 )" +if printf '%s' "$j" | python3 -c 'import sys,json; d=json.load(sys.stdin); sys.exit(0 if d.get("pattern_meta")=="|" and "--regex=" in d.get("next","") else 1)'; then + ok "M1 MCP grep: pattern_meta | and next= the regex verb" +else no "M1 MCP: $( printf '%s' "$j" | head -c 300 )"; fi j="$( mcp 'hlxa' )" -printf '%s' "$j" | python3 -c 'import sys,json; d=json.load(sys.stdin); h=[x for x in d["hits"] if x["line"]==2]; sys.exit(0 if len(h)==1 and h[0].get("occ")==2 and d.get("total")==3 else 1)' \ - && ok "M2 MCP grep: one row per line, occ 2, total 3 (CLI parity)" || no "M2 MCP: $( printf '%s' "$j" | head -c 400 )" -printf '%s' "$j" | python3 -c 'import sys,json; d=json.load(sys.stdin); sys.exit(1 if "pattern_meta" in d else 0)' \ - && ok "M3 MCP grep: a plain literal carries no pattern_meta" || no "M3 plain literal has pattern_meta" +if printf '%s' "$j" | python3 -c 'import sys,json; d=json.load(sys.stdin); h=[x for x in d["hits"] if x["line"]==2]; sys.exit(0 if len(h)==1 and h[0].get("occ")==2 and d.get("total")==3 else 1)'; then + ok "M2 MCP grep: one row per line, occ 2, total 3 (CLI parity)" +else no "M2 MCP: $( printf '%s' "$j" | head -c 400 )"; fi +if printf '%s' "$j" | python3 -c 'import sys,json; d=json.load(sys.stdin); sys.exit(1 if "pattern_meta" in d else 0)'; then + ok "M3 MCP grep: a plain literal carries no pattern_meta" +else no "M3 plain literal has pattern_meta"; fi [ "$fail" -eq 0 ] && echo "honestlexcheck: ALL PASS" || echo "honestlexcheck: FAILURES" exit "$fail" From 4994f5aef4c48e3bc87a25d2e79bcd5b2e77bc11 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I <210578314+joyful-ii-V-I@users.noreply.github.com> Date: Fri, 23 Oct 2026 10:37:19 -0400 Subject: [PATCH 41/77] pathgaps fix 5: capturePythonTypeParamDecls split (quality-delta complexity, params) Co-Authored-By: Claude Sonnet 5.5 --- src/ingest_binds.h | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/src/ingest_binds.h b/src/ingest_binds.h index 12a684711..5a4b8c88a 100644 --- a/src/ingest_binds.h +++ b/src/ingest_binds.h @@ -3343,10 +3343,20 @@ inline void capturePythonPatternDecls( TSNode n, std::uint32_t fileId, Lang lang // raises TypeError and no derivation exists). One `type` per name; a bound `Base: int` wraps its name in a constrained_type, `*Ts` / `**P` // in a splat_type. A def's parameters are visible inside it (a VarDecl at the list); a class's are visible to its own base clause, which // the scope chain reads from OUTSIDE the class, so it is filed one byte before the class like the class's own name. -inline void capturePythonTypeParamDecls( TSNode n, const char* t, std::uint32_t fileId, Lang lang, std::string_view src, std::vector& binds ) +inline TSNode pythonTypeParamName( TSNode param ) // `type` > identifier | constrained_type > type > identifier | splat_type > identifier { - const bool isClass = kindIs( t, "class_definition" ); - const TSNode list = isClass || kindIs( t, "function_definition" ) ? fieldChild( n, NodeField::TypeParameters ) : TSNode{}; + TSNode c = param; + for( int depth = 0; depth < 4 && !ts_node_is_null( c ) && !kindIs( ts_node_type( c ), "identifier" ); ++depth ) + { + c = ts_node_named_child_count( c ) > 0 ? ts_node_named_child( c, 0 ) : TSNode{}; + } + return !ts_node_is_null( c ) && kindIs( ts_node_type( c ), "identifier" ) ? c : TSNode{}; +} + +inline void capturePythonTypeParamDecls( TSNode n, std::uint32_t fileId, Lang lang, std::string_view src, std::vector& binds ) +{ + const bool isClass = kindIs( ts_node_type( n ), "class_definition" ); + const TSNode list = isClass || kindIs( ts_node_type( n ), "function_definition" ) ? fieldChild( n, NodeField::TypeParameters ) : TSNode{}; if( ts_node_is_null( list ) ) { return; @@ -3355,14 +3365,9 @@ inline void capturePythonTypeParamDecls( TSNode n, const char* t, std::uint32_t const std::uint32_t at = isClass ? ( own > 0 ? own - 1 : 0 ) : ts_node_start_byte( list ); for( std::uint32_t i = 0, k = ts_node_named_child_count( list ); i < k; ++i ) { - TSNode c = ts_node_named_child( list, i ); // `type`, then (constrained_type > type | splat_type) > ... > identifier - for( int depth = 0; depth < 4 && !ts_node_is_null( c ) && !kindIs( ts_node_type( c ), "identifier" ); ++depth ) + if( const TSNode id = pythonTypeParamName( ts_node_named_child( list, i ) ); !ts_node_is_null( id ) ) { - c = ts_node_named_child_count( c ) > 0 ? ts_node_named_child( c, 0 ) : TSNode{}; - } - if( !ts_node_is_null( c ) && kindIs( ts_node_type( c ), "identifier" ) ) - { - pushRawBind( fileId, lang, nodeTextOf( c, src ), std::string{}, BindSite{ at, 0u, 0u }, LocalBindKind::VarDecl, binds ); + pushRawBind( fileId, lang, nodeTextOf( id, src ), std::string{}, BindSite{ at, 0u, 0u }, LocalBindKind::VarDecl, binds ); } } } @@ -3501,7 +3506,7 @@ inline void captureShadowScopeDecls( TSNode n, const char* t, std::uint32_t file } capturePythonRebindShadowDecls( n, t, fileId, lang, src, binds ); // issue #287 round 2: rebind veto evidence capturePythonPatternDecls( n, fileId, lang, src, binds ); // ind4b fix 4: match captures and `type` statements - capturePythonTypeParamDecls( n, t, fileId, lang, src, binds ); // ind4b fix 5: PEP 695 type parameters + capturePythonTypeParamDecls( n, fileId, lang, src, binds ); // ind4b fix 5: PEP 695 type parameters return; } if( lang != Lang::Cpp && lang != Lang::ObjC ) From f82aed6330c67af73963bea2c98cadb20347993a Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I <210578314+joyful-ii-V-I@users.noreply.github.com> Date: Fri, 23 Oct 2026 11:46:08 -0400 Subject: [PATCH 42/77] refactor(hop1): bridge crossings as helpers; backhops.h index keyed by entity, small clones gone (qd) bridgeSlotCrossings / bridgeDeclinedCrossings carry the per-symbol crossing loops out of pathBridge; DeclinedCrossRule keys its stated classes in two HashMaps (classId#field, functionId#name) instead of sorted vectors with a comparator; reaches_hedged= goes through countFieldOrEmpty; BackHopVia names are a table. Co-Authored-By: Claude Opus 5.5 --- src/backhops.h | 126 +++++++++++++++++-------------------------------- src/pathgaps.h | 83 +++++++++++++++++--------------- 2 files changed, 88 insertions(+), 121 deletions(-) diff --git a/src/backhops.h b/src/backhops.h index fed98dcd2..2e97cff36 100644 --- a/src/backhops.h +++ b/src/backhops.h @@ -19,6 +19,7 @@ // What it does NOT mean: a hop is NOT a call edge and NOT proven. Every consumer prints it as a hedge (proven="0", via= // named, the width beside it) and never counts it in reachable= / hops= / reaches=. +#include "graphlegend.h" // countFieldOrEmpty — reaches_hedged= absent at zero #include "graph.h" // Graph — the declined records and their stored candidate lists #include "model.h" #include "nextverb.h" // nextAttrXml / nextFlag — the ring's continuation @@ -30,6 +31,7 @@ #include "infra/sortutil.h" // svLess #include +#include #include #include #include @@ -83,14 +85,6 @@ class DeclinedCrossRule } private: - struct FieldType - { - NodeId owner = kNoNode; // the class definition that states the field (checklist 16: the entity, not its name — - // hono's hono-base.ts and hono.ts both define a class `Hono`, each typing `router` its own - // way); in m_recvTypes, the function stating its parameter's or local's class - std::string_view field; - std::string_view type; // "" = spelled two ways: decides nothing - }; struct ClassName { std::string_view name; @@ -125,15 +119,32 @@ class DeclinedCrossRule return best != nullptr ? best->id : kNoNode; } + // The receiver keyword of the four languages builtinSpelledCall's tables cover; any other language, or another spelling, is + // an ordinary name (checklist 37). static bool selfKeyword( Lang lang, std::string_view root ) noexcept { - switch( lang ) + static constexpr std::pair kSelf[] = { + { Lang::JavaScript, "this" }, { Lang::TypeScript, "this" }, { Lang::Python, "self" }, { Lang::Ruby, "self" } }; + return std::ranges::find( kSelf, std::pair( lang, root ) ) != std::end( kSelf ); + } + static std::string keyOf( NodeId id, std::string_view name ) + { + return std::to_string( id ) + "#" + std::string( name ); + } + // One stated class per key: a second, different spelling leaves "" (decides nothing), as ReceiverEvidence's tombstones do. + static void noteOnce( HashMap& m, std::string key, std::string_view type ) + { + const auto [ it, fresh ] = m.try_emplace( std::move( key ), type ); + if( !fresh && it->second != type ) { - case Lang::JavaScript: case Lang::TypeScript: return root == "this"; - case Lang::Python: case Lang::Ruby: return root == "self"; - default: return false; // builtinSpelledCall's tables hold these four only + it->second = {}; } } + static std::string_view lookup( const HashMap& m, NodeId id, std::string_view name ) + { + const auto it = m.find( keyOf( id, name ) ); + return it == m.end() ? std::string_view() : it->second; + } bool receiverTypedInTree( std::uint32_t site, const Reference& r ) const { @@ -166,7 +177,7 @@ class DeclinedCrossRule owners.push_back( own ); } } - else if( const std::string_view cls = statedClassOf( caller, ch.root ); !cls.empty() ) + else if( const std::string_view cls = lookup( m_recvTypes, caller, ch.root ); !cls.empty() ) { for( auto it = std::ranges::lower_bound( m_classIds, cls, sortutil::svLess, &ClassName::name ); it != m_classIds.end() && it->name == cls; ++it ) { @@ -177,24 +188,11 @@ class DeclinedCrossRule { // (4) this class states one in-tree class for the field; stating none (a primitive, a base's field), two spellings, or // a class the tree does not define leaves the builtin reading standing - const auto it = std::ranges::lower_bound( m_fields, std::pair( owner, ch.path ), fieldKeyLess, [ ]( const FieldType& f ) { return std::pair( f.owner, f.field ); } ); - return it != m_fields.end() && it->owner == owner && it->field == ch.path && !it->type.empty() - && std::ranges::binary_search( m_classNames, it->type, sortutil::svLess ); + const std::string_view type = lookup( m_fields, owner, ch.path ); + return !type.empty() && std::ranges::binary_search( m_classNames, type, sortutil::svLess ); } ); } - // The class the source states for parameter or local `var` of function `fn` (one unqualified spelling), "" when none or two. - std::string_view statedClassOf( NodeId fn, std::string_view var ) const - { - const auto it = std::ranges::lower_bound( m_recvTypes, std::pair( fn, var ), fieldKeyLess, [ ]( const FieldType& f ) { return std::pair( f.owner, f.field ); } ); - return it != m_recvTypes.end() && it->owner == fn && it->field == var ? it->type : std::string_view(); - } - - static bool fieldKeyLess( const std::pair& a, const std::pair& b ) noexcept - { - return a.first != b.first ? a.first < b.first : sortutil::svLess( a.second, b.second ); - } - // Built on the first receiver shape that needs it: a tree with no such call pays nothing. void buildOnce() const { @@ -225,52 +223,23 @@ class DeclinedCrossRule } if( b.kind == LocalBindKind::RecvType ) { - m_recvTypes.push_back( FieldType{ b.fromSymbol, b.var, b.typeName } ); // keyed by the function that states it + noteOnce( m_recvTypes, keyOf( b.fromSymbol, b.var ), b.typeName ); // keyed by the function that states it } else if( const NodeId owner = ownerOf( b.fromSymbol ); owner != kNoNode ) { - m_fields.push_back( FieldType{ owner, b.var, b.typeName } ); - } - } - oneSpellingPerKey( m_fields ); - oneSpellingPerKey( m_recvTypes ); - } - - // Sorted by key, one row per (owner, name): two different spellings decide nothing (type ""). - static void oneSpellingPerKey( std::vector& v ) - { - std::ranges::sort( v, [ ]( const FieldType& a, const FieldType& b ) - { - if( a.owner != b.owner || a.field != b.field ) - { - return fieldKeyLess( { a.owner, a.field }, { b.owner, b.field } ); - } - return sortutil::svLess( a.type, b.type ); - } ); - std::vector merged; - for( const FieldType& f : v ) - { - if( !merged.empty() && merged.back().owner == f.owner && merged.back().field == f.field ) - { - if( merged.back().type != f.type ) - { - merged.back().type = {}; - } - continue; + noteOnce( m_fields, keyOf( owner, b.var ), b.typeName ); } - merged.push_back( f ); } - v = std::move( merged ); } const IngestResult& m_ing; const Graph& m_g; mutable bool m_built = false; - mutable std::vector m_fields; // (owner, field) → the one stated class, sorted + mutable HashMap m_fields; // "classId#field" → the one stated class ("" two spellings) mutable std::vector m_classNames; // every in-tree class-like definition name, sorted, distinct mutable std::vector m_classes; // every class-like definition's span, by file (ownerOf) mutable std::vector m_classIds; // every class-like definition by name (a stated class's definitions) - mutable std::vector m_recvTypes; // (function, parameter or local) → the one stated class, sorted + mutable HashMap m_recvTypes; // "functionId#name" → its parameter's or local's stated class }; // ── the provider ───────────────────────────────────────────────────────────────────────────────────────────────────── @@ -372,14 +341,7 @@ class BackwardHops mutable HashMap> m_memo; }; -inline bool isSlotHop( BackHopVia v ) noexcept -{ - return v == BackHopVia::Slot || v == BackHopVia::SlotName; -} -inline std::string_view backHopViaName( BackHopVia v ) noexcept -{ - return v == BackHopVia::Slot ? "slot" : v == BackHopVia::SlotName ? "slot-name" : "declined"; -} +inline constexpr std::array kBackHopViaNames = { "slot", "slot-name", "declined" }; // indexed by BackHopVia // Whether any of `sinks` has a backward hop a consumer follows (width within kBackHopWidth): --path's root next= swap. inline bool anyFollowedBackHop( const BackwardHops& hops, std::span sinks ) @@ -431,7 +393,7 @@ inline ImpactRing impactSlotRing( const IngestResult& ing, const BackwardHops& h { for( const BackHop& h : hops.backwardHops( s ) ) { - if( isSlotHop( h.via ) && std::ranges::find( d1, h.from ) == d1.end() ) + if( h.via != BackHopVia::Declined && std::ranges::find( d1, h.from ) == d1.end() ) { d1.push_back( h.from ); } @@ -453,7 +415,7 @@ inline ImpactRing impactSlotRing( const IngestResult& ing, const BackwardHops& h { for( const BackHop& h : hops.backwardHops( f ) ) { - if( !isSlotHop( h.via ) || h.from == f || ( h.from < S && known[ h.from ] != 0 ) || std::ranges::find( d1, h.from ) != d1.end() ) + if( h.via == BackHopVia::Declined || h.from == f || ( h.from < S && known[ h.from ] != 0 ) || std::ranges::find( d1, h.from ) != d1.end() ) { continue; // a proven member, an answered definition or a d=1 function already speaks for itself } @@ -464,14 +426,8 @@ inline ImpactRing impactSlotRing( const IngestResult& ing, const BackwardHops& h return out; } -inline std::string impactRingAttrXml( const ImpactRing& ring ) -{ - return ring.hedged == 0 ? std::string() : " reaches_hedged=\"" + std::to_string( ring.hedged ) + "\""; -} -inline std::string impactRingKeyJson( const ImpactRing& ring ) -{ - return ring.hedged == 0 ? std::string() : ",\"reaches_hedged\":" + std::to_string( ring.hedged ); -} +inline std::string impactRingAttrXml( const ImpactRing& ring ) { return countFieldOrEmpty( "reaches_hedged", ring.hedged, /*json=*/false ); } +inline std::string impactRingKeyJson( const ImpactRing& ring ) { return countFieldOrEmpty( "reaches_hedged", ring.hedged, /*json=*/true ); } inline std::string impactRingNext( const IngestResult& ing, const ImpactRing& ring ) { @@ -495,7 +451,7 @@ inline std::string impactRingXml( const IngestResult& ing, const ImpactRing& rin const ImpactRingRow& row = ring.rows[ i ]; const Reference& c = ing.references[ row.ref ]; out += ""; + + "\" via=\"" + std::string( kBackHopViaNames[ std::size_t( row.via ) ] ) + "\" calls=\"" + ex( ing.symbols[ row.calls ].name ) + "\"/>"; } return out + ""; } @@ -518,7 +474,7 @@ inline std::string impactRingJson( const IngestResult& ing, const ImpactRing& ri const ImpactRingRow& row = ring.rows[ i ]; const Reference& c = ing.references[ row.ref ]; out += std::string( i == 0 ? "{" : ",{" ) + "\"called_by\":\"" + jsonStr( ing.symbols[ row.fn ].name ) + "\",\"called_at\":\"" - + jsonStr( vrPath( ing, c.fileId, rr ) + ":" + std::to_string( c.line ) ) + "\",\"via\":\"" + std::string( backHopViaName( row.via ) ) + + jsonStr( vrPath( ing, c.fileId, rr ) + ":" + std::to_string( c.line ) ) + "\",\"via\":\"" + std::string( kBackHopViaNames[ std::size_t( row.via ) ] ) + "\",\"calls\":\"" + jsonStr( ing.symbols[ row.calls ].name ) + "\"}"; } return out + "]}"; @@ -534,7 +490,13 @@ inline constexpr std::string_view kImpactRingRowsLegend = // `on`: reaches_hedged= rides; `rows`: the element rides too (XML/JSON; the columnar form carries the count alone). inline std::string impactRingLegend( bool on, bool rows ) { - return on ? std::string( kImpactRingLegend ) + ( rows ? std::string( kImpactRingRowsLegend ) : std::string() ) : std::string(); + std::string out; + if( on ) + { + out.append( kImpactRingLegend ); + out.append( rows ? kImpactRingRowsLegend : std::string_view() ); + } + return out; } } // namespace rw diff --git a/src/pathgaps.h b/src/pathgaps.h index 1b8f479f6..760fa3153 100644 --- a/src/pathgaps.h +++ b/src/pathgaps.h @@ -951,9 +951,10 @@ enum class BridgeVia : std::uint8_t { Edge, Declined, Value, Closure, Namesake, inline constexpr std::size_t kBridgeViaCount = 7; static_assert( enumCountIsExact(), "kBridgeViaCount must equal the BridgeVia count — raise it with the append" ); inline constexpr std::array kBridgeViaNames = { "", "declined", "value", "closure", "namesake", "slot", "slot-name" }; +static_assert( std::size_t( BridgeVia::SlotName ) + 1 == kBridgeViaCount && std::size_t( BridgeVia::Slot ) + 2 == kBridgeViaCount, "the two slot crossings close the enum" ); inline bool bridgeViaIsSlot( BridgeVia v ) noexcept { - return v == BridgeVia::Slot || v == BridgeVia::SlotName; + return v >= BridgeVia::Slot; // Slot or SlotName, the enum's last two (static_assert above) } struct PathBridge @@ -966,6 +967,46 @@ struct PathBridge bool found() const noexcept { return !hops.empty(); } }; +// SLOT crossings out of `u` (HOP1): each function a struct-field call in u may call, kept only when that function's own backward +// hops name u (the --callers join and its tier rule, read from the provider: one copy) within the width bound. `cross( to, via, +// width )` decides whether the target is new. +template +inline void bridgeSlotCrossings( const ValueRefIndex& values, const BackwardHops& backHops, NodeId u, Cross&& cross ) +{ + for( const std::uint32_t call : values.slots().callsIn( u ) ) + { + for( const SlotIndex::Hit& stored : values.slots().valuesFor( call ) ) + { + for( const NodeId f : values.targetsOf( stored.ref ) ) + { + const std::span hops = backHops.backwardHops( f ); + const auto it = std::ranges::find_if( hops, [ & ]( const BackHop& h ) { return h.from == u && h.via != BackHopVia::Declined && h.width <= kBridgeFanOut; } ); + if( it != hops.end() ) + { + cross( f, it->via == BackHopVia::Slot ? BridgeVia::Slot : BridgeVia::SlotName, it->width ); + } + } + } + } +} + +// DECLINED crossings of record `site`: each candidate of its stored list, unless the list is missing or wider than kBridgeFanOut, +// or the call reads as the builtin method it spells (DeclinedCrossRule, HOP1 #11). +template +inline void bridgeDeclinedCrossings( const Graph& g, const DeclinedCrossRule& rule, std::uint32_t site, Cross&& cross ) +{ + const std::uint32_t list = g.declinedSiteList[ site ]; + if( list == kNoDeclinedList || list + 1 >= g.declinedListOff.size() || rule.builtinSpelled( site ) ) + { + return; // a call with no stored candidate list names nothing to cross to; a builtin-spelled one is no hand-off + } + const std::size_t fan = g.declinedListOff[ list + 1 ] - g.declinedListOff[ list ]; + for( std::uint32_t slot = g.declinedListOff[ list ]; fan <= kBridgeFanOut && slot < g.declinedListOff[ list + 1 ]; ++slot ) + { + cross( g.declinedListCand[ slot ], BridgeVia::Declined, fan ); + } +} + inline PathBridge pathBridge( const IngestResult& ing, const Graph& g, std::span srcDefs, std::span dstDefs, const ValueRefIndex& values, const BackwardHops& backHops ) { @@ -1057,7 +1098,6 @@ inline PathBridge pathBridge( const IngestResult& ing, const Graph& g, std::span frontier.push_back( s ); } } - const DeclinedCrossRule& declinedRule = backHops.declinedRule(); // HOP1 #11: one builtin rule for every declined crossing NodeId hit = kNoNode; for( std::uint32_t k = 0; k <= kBridgeSlotCrossings && hit == kNoNode && !frontier.empty(); ++k ) { @@ -1111,29 +1151,7 @@ inline PathBridge pathBridge( const IngestResult& ing, const Graph& g, std::span { continue; } - // SLOT (HOP1): each function a struct-field call in u may call, kept only when the stored function's own backward - // hops name u (the --callers join and its tier rule, read from the provider: one copy) within the width bound - for( const std::uint32_t call : values.slots().callsIn( u ) ) - { - for( const SlotIndex::Hit& stored : values.slots().valuesFor( call ) ) - { - for( const NodeId f : values.targetsOf( stored.ref ) ) - { - if( f >= S || layer[f] != kUnseen || !routable( f ) ) - { - continue; - } - for( const BackHop& h : backHops.backwardHops( f ) ) - { - if( h.from == u && isSlotHop( h.via ) && h.width <= kBridgeFanOut ) - { - cross( u, f, h.via == BackHopVia::Slot ? BridgeVia::Slot : BridgeVia::SlotName, h.width ); - break; - } - } - } - } - } + bridgeSlotCrossings( values, backHops, u, [ & ]( NodeId to, BridgeVia via, std::size_t fan ) { cross( u, to, via, fan ); } ); // HOP1 if( slotStepOnly ) { continue; @@ -1141,20 +1159,7 @@ inline PathBridge pathBridge( const IngestResult& ing, const Graph& g, std::span const std::pair lo( u, 0u ), hi( u, std::numeric_limits::max() ); for( auto it = std::ranges::lower_bound( declinedBy, lo ); it != declinedBy.end() && *it <= hi; ++it ) { - const std::uint32_t list = g.declinedSiteList[ it->second ]; - if( list == kNoDeclinedList || list + 1 >= g.declinedListOff.size() ) - { - continue; // a call with no stored candidate list names nothing to cross to - } - if( declinedRule.builtinSpelled( it->second ) ) - { - continue; // not a hand-off to cross (DeclinedCrossRule: spelled like a builtin method, receiver not typed in-tree) - } - const std::size_t fan = g.declinedListOff[list + 1] - g.declinedListOff[list]; - for( std::uint32_t slot = g.declinedListOff[list]; fan <= kBridgeFanOut && slot < g.declinedListOff[list + 1]; ++slot ) - { - cross( u, g.declinedListCand[slot], BridgeVia::Declined, fan ); - } + bridgeDeclinedCrossings( g, backHops.declinedRule(), it->second, [ & ]( NodeId to, BridgeVia via, std::size_t fan ) { cross( u, to, via, fan ); } ); } for( auto it = std::ranges::lower_bound( valueBy, lo ); it != valueBy.end() && *it <= hi; ++it ) { From be2c76ff0e89f548d1cb42f36d8ee907afe4e522 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I <210578314+joyful-ii-V-I@users.noreply.github.com> Date: Fri, 23 Oct 2026 11:49:49 -0400 Subject: [PATCH 43/77] refactor(hop1): DeclinedCrossRule keys through graph.h jsImportKey (qd duplication) Co-Authored-By: Claude Opus 5.5 --- src/backhops.h | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/backhops.h b/src/backhops.h index 2e97cff36..bc8a90dd6 100644 --- a/src/backhops.h +++ b/src/backhops.h @@ -127,11 +127,7 @@ class DeclinedCrossRule { Lang::JavaScript, "this" }, { Lang::TypeScript, "this" }, { Lang::Python, "self" }, { Lang::Ruby, "self" } }; return std::ranges::find( kSelf, std::pair( lang, root ) ) != std::end( kSelf ); } - static std::string keyOf( NodeId id, std::string_view name ) - { - return std::to_string( id ) + "#" + std::string( name ); - } - // One stated class per key: a second, different spelling leaves "" (decides nothing), as ReceiverEvidence's tombstones do. + // One stated class per key (graph.h jsImportKey's "id#name", here a definition id): a second, different spelling leaves "" (decides nothing), as ReceiverEvidence's tombstones do. static void noteOnce( HashMap& m, std::string key, std::string_view type ) { const auto [ it, fresh ] = m.try_emplace( std::move( key ), type ); @@ -142,7 +138,7 @@ class DeclinedCrossRule } static std::string_view lookup( const HashMap& m, NodeId id, std::string_view name ) { - const auto it = m.find( keyOf( id, name ) ); + const auto it = m.find( jsImportKey( id, name ) ); return it == m.end() ? std::string_view() : it->second; } @@ -223,11 +219,11 @@ class DeclinedCrossRule } if( b.kind == LocalBindKind::RecvType ) { - noteOnce( m_recvTypes, keyOf( b.fromSymbol, b.var ), b.typeName ); // keyed by the function that states it + noteOnce( m_recvTypes, jsImportKey( b.fromSymbol, b.var ), b.typeName ); // keyed by the function that states it } else if( const NodeId owner = ownerOf( b.fromSymbol ); owner != kNoNode ) { - noteOnce( m_fields, keyOf( owner, b.var ), b.typeName ); + noteOnce( m_fields, jsImportKey( owner, b.var ), b.typeName ); } } } From 76022e3badcaef119315ffcd6e298d41fa5b52d7 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I <210578314+joyful-ii-V-I@users.noreply.github.com> Date: Fri, 23 Oct 2026 13:03:50 -0400 Subject: [PATCH 44/77] --lego Rust: pin a dependency key that is also an in-tree package name (red arm) Co-Authored-By: Claude Sonnet 5.5 --- test/legotransitivecheck.sh | 3 +++ test/legotransitivefix/xp/rspkgkey/Cargo.toml | 2 ++ test/legotransitivefix/xp/rspkgkey/crates/pkgapp/Cargo.toml | 4 ++++ test/legotransitivefix/xp/rspkgkey/crates/pkgapp/src/lib.rs | 1 + .../xp/rspkgkey/crates/pkgapp/src/pkguser.rs | 3 +++ test/legotransitivefix/xp/rspkgkey/crates/pkgcore/Cargo.toml | 2 ++ test/legotransitivefix/xp/rspkgkey/crates/pkgcore/src/lib.rs | 1 + test/legotransitivefix/xp/rspkgkey/crates/pkgother/Cargo.toml | 4 ++++ test/legotransitivefix/xp/rspkgkey/crates/pkgother/src/lib.rs | 1 + .../xp/rspkgkey/crates/pkgother/src/pkgreg.rs | 3 +++ 10 files changed, 24 insertions(+) create mode 100644 test/legotransitivefix/xp/rspkgkey/Cargo.toml create mode 100644 test/legotransitivefix/xp/rspkgkey/crates/pkgapp/Cargo.toml create mode 100644 test/legotransitivefix/xp/rspkgkey/crates/pkgapp/src/lib.rs create mode 100644 test/legotransitivefix/xp/rspkgkey/crates/pkgapp/src/pkguser.rs create mode 100644 test/legotransitivefix/xp/rspkgkey/crates/pkgcore/Cargo.toml create mode 100644 test/legotransitivefix/xp/rspkgkey/crates/pkgcore/src/lib.rs create mode 100644 test/legotransitivefix/xp/rspkgkey/crates/pkgother/Cargo.toml create mode 100644 test/legotransitivefix/xp/rspkgkey/crates/pkgother/src/lib.rs create mode 100644 test/legotransitivefix/xp/rspkgkey/crates/pkgother/src/pkgreg.rs diff --git a/test/legotransitivecheck.sh b/test/legotransitivecheck.sh index 3ebea8ae8..456e1d5e3 100755 --- a/test/legotransitivecheck.sh +++ b/test/legotransitivecheck.sh @@ -423,6 +423,9 @@ xp XpWsInhBase "XpWsInhOutAlias" "XpWsInhReal XpWsInhTarget" xpfloor XpCfgBase "XpCfgOutSerde" xpfloor XpCfgTwoBase "" xpfloor XpKeyBase "XpKeyReal XpKeyOutReg" +# fix round 5 (review rv-fc2-delta4 C1): the same two meanings when the key is also an in-tree PACKAGE name (`xp-pkg-core` is a path +# dependency of one member and a registry crate of another): floor, no row either way +xpfloor XpPkgBase "XpPkgReal XpPkgOutReg" xp XpKeyBothBase "" "XpKeyBothA XpKeyBothB" xp XpKeyCtlBase "XpKeyCtlOut" "" if lego XpKeyCtlBase && iface | grep -q 'implementors_floor'; then no "XpKeyCtlBase carries a floor for a key every member declares as a registry crate: $( iface )"; else ok "XpKeyCtlBase: a key registry in every member stays decided (no floor)"; fi diff --git a/test/legotransitivefix/xp/rspkgkey/Cargo.toml b/test/legotransitivefix/xp/rspkgkey/Cargo.toml new file mode 100644 index 000000000..c44b2b237 --- /dev/null +++ b/test/legotransitivefix/xp/rspkgkey/Cargo.toml @@ -0,0 +1,2 @@ +[workspace] +members = ["crates/pkgcore", "crates/pkgapp", "crates/pkgother"] diff --git a/test/legotransitivefix/xp/rspkgkey/crates/pkgapp/Cargo.toml b/test/legotransitivefix/xp/rspkgkey/crates/pkgapp/Cargo.toml new file mode 100644 index 000000000..de7fba259 --- /dev/null +++ b/test/legotransitivefix/xp/rspkgkey/crates/pkgapp/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "xp-pkg-app" +[dependencies] +xp-pkg-core = { path = "../pkgcore" } diff --git a/test/legotransitivefix/xp/rspkgkey/crates/pkgapp/src/lib.rs b/test/legotransitivefix/xp/rspkgkey/crates/pkgapp/src/lib.rs new file mode 100644 index 000000000..5abd4d08c --- /dev/null +++ b/test/legotransitivefix/xp/rspkgkey/crates/pkgapp/src/lib.rs @@ -0,0 +1 @@ +mod pkguser; diff --git a/test/legotransitivefix/xp/rspkgkey/crates/pkgapp/src/pkguser.rs b/test/legotransitivefix/xp/rspkgkey/crates/pkgapp/src/pkguser.rs new file mode 100644 index 000000000..d3ae89a4a --- /dev/null +++ b/test/legotransitivefix/xp/rspkgkey/crates/pkgapp/src/pkguser.rs @@ -0,0 +1,3 @@ +use xp_pkg_core::XpPkgBase; +pub struct XpPkgReal; +impl XpPkgBase for XpPkgReal {} diff --git a/test/legotransitivefix/xp/rspkgkey/crates/pkgcore/Cargo.toml b/test/legotransitivefix/xp/rspkgkey/crates/pkgcore/Cargo.toml new file mode 100644 index 000000000..228764de6 --- /dev/null +++ b/test/legotransitivefix/xp/rspkgkey/crates/pkgcore/Cargo.toml @@ -0,0 +1,2 @@ +[package] +name = "xp-pkg-core" diff --git a/test/legotransitivefix/xp/rspkgkey/crates/pkgcore/src/lib.rs b/test/legotransitivefix/xp/rspkgkey/crates/pkgcore/src/lib.rs new file mode 100644 index 000000000..a5350b0dc --- /dev/null +++ b/test/legotransitivefix/xp/rspkgkey/crates/pkgcore/src/lib.rs @@ -0,0 +1 @@ +pub trait XpPkgBase {} diff --git a/test/legotransitivefix/xp/rspkgkey/crates/pkgother/Cargo.toml b/test/legotransitivefix/xp/rspkgkey/crates/pkgother/Cargo.toml new file mode 100644 index 000000000..badba9380 --- /dev/null +++ b/test/legotransitivefix/xp/rspkgkey/crates/pkgother/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "xp-pkg-other" +[dependencies] +xp-pkg-core = "1" diff --git a/test/legotransitivefix/xp/rspkgkey/crates/pkgother/src/lib.rs b/test/legotransitivefix/xp/rspkgkey/crates/pkgother/src/lib.rs new file mode 100644 index 000000000..1e09c4bd7 --- /dev/null +++ b/test/legotransitivefix/xp/rspkgkey/crates/pkgother/src/lib.rs @@ -0,0 +1 @@ +mod pkgreg; diff --git a/test/legotransitivefix/xp/rspkgkey/crates/pkgother/src/pkgreg.rs b/test/legotransitivefix/xp/rspkgkey/crates/pkgother/src/pkgreg.rs new file mode 100644 index 000000000..20a632f3f --- /dev/null +++ b/test/legotransitivefix/xp/rspkgkey/crates/pkgother/src/pkgreg.rs @@ -0,0 +1,3 @@ +use xp_pkg_core::XpPkgBase; +pub struct XpPkgOutReg; +impl XpPkgBase for XpPkgOutReg {} From fa8f9b011349a36c653cca9b75b925a0d06780b4 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I <210578314+joyful-ii-V-I@users.noreply.github.com> Date: Fri, 23 Oct 2026 13:13:17 -0400 Subject: [PATCH 45/77] docs(hop1): regenerate LIMITS.md / TUNING.md and the README cap count (275) for the two new caps Co-Authored-By: Claude Opus 5.5 --- README.md | 2 +- docs/LIMITS.md | 19 ++++++++++++++----- docs/TUNING.md | 24 ++++++++++++------------ 3 files changed, 27 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index f07180754..8198be345 100644 --- a/README.md +++ b/README.md @@ -2878,7 +2878,7 @@ python3 test/pargates.py . ./build/ripwire -j 6 A new gate script must be added to `test/regression.sh` in the same change. The gate `test/manifestcheck.sh` enforces this rule. -Another gate derives the cap inventory. The tool has 273 compile-time caps and 7 ranking parameters. +Another gate derives the cap inventory. The tool has 275 compile-time caps and 7 ranking parameters. `docs/LIMITS.md` lists each cap, its value, and whether the file discloses a truncation when the cap fires, and `python3 docs/limits_build.py --check` proves that list against `src/`. `docs/TUNING.md` lists the measured cost of each cap. diff --git a/docs/LIMITS.md b/docs/LIMITS.md index d90d557d4..f37792711 100644 --- a/docs/LIMITS.md +++ b/docs/LIMITS.md @@ -30,10 +30,10 @@ a small explicit bound" is safe only when nothing inside the group repeats; it i | total caps | files | caps whose file discloses | caps whose file discloses NOTHING | | --- | --- | --- | --- | -| 273 | 106 | 162 | **111** | +| 275 | 107 | 162 | **113** | Plus 7 ranking and apportionment parameters, in their own table below: they are not caps, they -are not counted as caps, and 273 + 7 is the 280 constants this generator parses out of `src/`. +are not counted as caps, and 275 + 7 is the 282 constants this generator parses out of `src/`. ## INDEXING, OUTPUT or BOUNDARY — which half of the answer a cap bounds @@ -51,8 +51,8 @@ None of them truncates anything, so none can be judged by `shown=`/`total=` and a disclosure — labelling them OUTPUT would ask for a `capped="1"` that could never honestly fire. The distinction was named in review on #108 and the rows below now carry it. -The `class` column below carries that answer where it is known. **140 of 273 caps are classified -(50 INDEXING, 46 OUTPUT, 44 BOUNDARY); the remaining 133 render `—`, which means NOT YET +The `class` column below carries that answer where it is known. **140 of 275 caps are classified +(50 INDEXING, 46 OUTPUT, 44 BOUNDARY); the remaining 135 render `—`, which means NOT YET CLASSIFIED — never "neither".** Classifications live in `docs/limits_classes.tsv`, a sidecar with a known expiry: the tag belongs on the declaration itself, and this file exists only because the round that @@ -102,7 +102,7 @@ refuse to write, so the column cannot be satisfied by pointing at nothing. ## Caps, by file -One table for each of the 106 files that declare a cap — the 273 caps counted above, and no parameter. +One table for each of the 107 files that declare a cap — the 275 caps counted above, and no parameter. ### `src/abicheck.h` @@ -129,6 +129,15 @@ Discloses: `count_capped` | --- | --- | --- | --- | | `kAtomsQueryBudget` | `100000` | — | — | +### `src/backhops.h` + +Discloses: **none** + +| constant | value | class | note | +| --- | --- | --- | --- | +| `kBackHopWidth` | `16` | — | — | +| `kImpactRingRowCap` | `16` | — | — | + ### `src/binstale.h` Discloses: **none** diff --git a/docs/TUNING.md b/docs/TUNING.md index 91bf28bf2..613884fbc 100644 --- a/docs/TUNING.md +++ b/docs/TUNING.md @@ -14,25 +14,25 @@ to production at defaults; that control is what makes these numbers mean anythin | cap declarations | distinct names | tunable | must stay `constexpr` | move >= 1 invocation | move nothing measurable | | --- | --- | --- | --- | --- | --- | -| 181 | 180 | 112 | 12 | **37** | 75 | +| 183 | 182 | 112 | 12 | **37** | 75 | -The first two columns are not the same number, and the gap is not a rounding: `src/` holds **181 cap -declarations** under **180 distinct names** (`kRowCap` declared in more than one file). The sweep +The first two columns are not the same number, and the gap is not a rounding: `src/` holds **183 cap +declarations** under **182 distinct names** (`kRowCap` declared in more than one file). The sweep patches by NAME, so its own population is NAMES and not declarations — and the two columns beside those are frozen at the commit named under Provenance while this census is re-read from `src/` on -every run, which makes them a third population again. Enumerated over the 180 names `src/` declares -today: **111 tunable**, **12 must stay `constexpr`**, and **57 declared since the sweep was -prepared, which no measurement has touched** (`kBaseScopeScanCap`, `kBridgeMaxHops`, -`kCallSitesPerRowCap`, `kCallersBandMaxBytes`, `kChurnMergeBombMaxFiles`, `kClosureRows`, +51 more). -111 + 12 + 57 = 180, and `emit` refuses to render a partition that does not add up. The `tunable` -column above reads 112 rather than 111 because one name the sweep classified is no longer declared -in `src/` at all: `kSituTestRowsShown`. Quoting "113 of 181" would be wrong in both halves at once, -which is the shape of error a generated table exists to prevent. +every run, which makes them a third population again. Enumerated over the 182 names `src/` declares +today: **111 tunable**, **12 must stay `constexpr`**, and **59 declared since the sweep was +prepared, which no measurement has touched** (`kBackHopWidth`, `kBaseScopeScanCap`, +`kBridgeMaxHops`, `kCallSitesPerRowCap`, `kCallersBandMaxBytes`, `kChurnMergeBombMaxFiles`, +53 +more). 111 + 12 + 59 = 182, and `emit` refuses to render a partition that does not add up. The +`tunable` column above reads 112 rather than 111 because one name the sweep classified is no longer +declared in `src/` at all: `kSituTestRowsShown`. Quoting "113 of 183" would be wrong in both halves +at once, which is the shape of error a generated table exists to prevent. ## Read this ratio before the tables **37 of 112 tunable caps move any invocation at all. 75 move nothing measurable.** That is the -finding, and it says what NOT to do: this is not a 181-cap audit. Most of these constants are +finding, and it says what NOT to do: this is not a 183-cap audit. Most of these constants are inert on real invocations and should be left alone. The work worth doing is the small set below, plus the caps that fire SILENTLY — a cap that bites without disclosing is a defect independent of whether its value is right, and that fix is both cheaper and larger than any retuning. From 343696c8c239cd628b147ff3b77e7abc37ef99ae Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I <210578314+joyful-ii-V-I@users.noreply.github.com> Date: Fri, 23 Oct 2026 13:27:18 -0400 Subject: [PATCH 46/77] --lego Rust: a dependency key that is also an in-tree package name is undecided when another member means it as a registry crate Co-Authored-By: Claude Sonnet 5.5 --- CHANGELOG.md | 5 +++-- src/graph.h | 19 +++++++++++++++++-- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9a02e50a8..de471f0b7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,8 +33,9 @@ not published here — see `docs/EVALS.md` for the instruments behind the headli `[patch]`, `[replace]` or `.cargo/config` names, a `path =` crate outside the indexed root — is undecided: it lists no row and the interface carries `implementors_floor=`, never a confident `implementors="0"`. A tree with no Cargo.toml decides nothing outside but `std` and its siblings. A key that two members of the tree mean differently (in the tree for one, a registry - crate for another) and a key a `.cargo/config(.toml)` `[patch]` names — read in each manifest's directory and above it, within the - indexed root — are undecided too; a Cargo file the run cannot read decides nothing outside. This does NOT + crate for another, also when it spells a package name of the tree) and a key a `.cargo/config(.toml)` `[patch]` names — read in + each manifest's directory and above it, within the indexed root, so a config ABOVE the indexed root is not read — are undecided + too; a Cargo file the run cannot read decides nothing outside. This does NOT cover a C++ `ext::Base` or `using ext::Base`, a C# `using`, a Swift `import`, a PHP `use`, a JS `require` destructure or a Rust `impl ext::Base for S` with no `use`: those rows are still listed by name. Gate: `test/legotransitivecheck.sh` (XP). - `--quality-delta=A..B` extracts its per-commit archive through stdin (`tar -x -f -`), the same Windows drive-colon fix as #402 (thanks @lennix1337). diff --git a/src/graph.h b/src/graph.h index a5b42f45d..8124e4d16 100644 --- a/src/graph.h +++ b/src/graph.h @@ -6479,10 +6479,25 @@ inline void cargoSettleNames( TomlDeps& deps, CargoNames& names ) { notRegistry.try_emplace( std::move( name ), char( 1 ) ); } + HashMap twoMeanings; // registry for one manifest and a crate of the tree for another (or for the package list) + for( const std::string& name : deps.registry ) + { + if( names.inTree.contains( name ) || std::ranges::find( deps.inTree, name ) != deps.inTree.end() ) + { + twoMeanings.try_emplace( name, char( 1 ) ); + } + } for( std::string& name : deps.inTree ) { - const bool twoMeanings = std::ranges::find( deps.registry, name ) != deps.registry.end(); - ( twoMeanings ? notRegistry : names.inTree ).try_emplace( std::move( name ), char( 1 ) ); + if( !twoMeanings.contains( name ) ) + { + names.inTree.try_emplace( std::move( name ), char( 1 ) ); + } + } + for( const auto& [ name, flag ] : twoMeanings ) + { + names.inTree.erase( name ); // a package name of the tree too: the registry meaning of the key is as good as the tree's + notRegistry.try_emplace( name, flag ); } for( std::string& name : deps.declared ) { From 3d4c6ad409fe59bd617aae0ed9d1c4c9b0d61cb5 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I <210578314+joyful-ii-V-I@users.noreply.github.com> Date: Fri, 23 Oct 2026 13:45:44 -0400 Subject: [PATCH 47/77] fix(ci): honest-lex CI round 1 -- gate count, clean-env, one cast per line, compact term rows, grep pin CI 39970610297 on 7f190c6d failed ~19 release shards on six lane-caused gates: - gatecountcheck/manifestcheck/readmedriftcheck (F2): honestlexcheck.sh made the loop name 704 gates; regenerated the 8 marked count sites with docs/gatecount_build.py (703 -> 704). - gitenvhermeticcheck (D): honestlexcheck.sh now sources test/lib/clean-env.sh. - lintbudgetcheck: isAllCapsMacro put two C-style casts on one line; --lint collapses same-line rows (dedupeLintFindings) while --match counts both (623 vs 624). Now static_cast, one per line. - compactlegendcheck (S): line_bytes= and occ= are spelled through countAttrXmlOrEmpty, so their readings moved (not copied) into kCompactCompletenessTerms, hit-qualified and grep-only. - compactlegendcheck (A-PIN): grep.xml re-anchored by hand to the lane's intended output (occ= legend sentence, hits 8 -> 7, n=2+at -> occ=2), history entry in the gate. Co-Authored-By: Claude Sonnet 5.5 --- README.md | 4 ++-- docs/EVALS.md | 6 +++--- present/deck5_ripwire_build.js | 6 +++--- src/compactlegend.h | 7 +++++-- src/crossref.h | 8 ++++++-- test/compactlegendcheck.sh | 5 +++++ test/compactlegendfix/pre_l1_full/grep.xml | 2 +- test/honestlexcheck.sh | 1 + 8 files changed, 26 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index df8542c7c..f07180754 100644 --- a/README.md +++ b/README.md @@ -2120,9 +2120,9 @@ wrong, and it has. These are the results that say so, all in-tree, all published ### In the tests
-703 gate scripts, five contracts no unit test can hold, and the house rule: write the gate before the code it measures +704 gate scripts, five contracts no unit test can hold, and the house rule: write the gate before the code it measures -`test/regression.sh` names **703 gate scripts** and is the authoritative list; +`test/regression.sh` names **704 gate scripts** and is the authoritative list; `python3 test/pargates.py . ./build/ripwire -j 6` runs the same set in parallel. On top of them sit the contracts that do not fit a unit test: two runs byte-identical, warm output identical to cold, output that pipes clean through `xmllint --noout`, a sanitizer build with `-fno-sanitize-recover=all`, and a diff --git a/docs/EVALS.md b/docs/EVALS.md index a77007342..ab0849671 100644 --- a/docs/EVALS.md +++ b/docs/EVALS.md @@ -21,7 +21,7 @@ section, and it is not an afterthought. | **Co-change / known-item evals** | `--eval`, `--eval-retrieval` (see `bench/ANSWERQUALITY.md`) | Whether the tool surfaces the other files a real historical commit touched; and known-item retrieval across four rankers. | | **Ensemble calibration harness** | `bench/ensemblecal/` | Whether `--ensemble`'s four evidence families are actually orthogonal, how often each fires, how stable each is across commits — and the preset ladder derived from that (§9). | | **Differential argv harness** | `test/argvdiffcheck.sh` | That a refactor changed *nothing observable*: two binaries, every argv vector, stdout + stderr + exit code byte-identical. | -| **The gate suite** | `test/regression.sh`, `test/pargates.py` | 703 gate scripts plus the determinism, cache-transparency and golden contracts. | +| **The gate suite** | `test/regression.sh`, `test/pargates.py` | 704 gate scripts plus the determinism, cache-transparency and golden contracts. | | **`--quality-delta`** | `src/quality.h` | Ten measured code-quality failure modes, reported only where a change made them worse. | ### The labeling protocol (why the held-out eval is allowed to disagree with the ranker) @@ -5888,7 +5888,7 @@ copy here would be exactly the dialect divergence that gate exists to catch. Com tags, wrap, stable-order defaults), seven individually invoked standalone gates (`g1freshcheck`, `skillscan`, `htmlexport`, `compresscheck`, `handoffcheck`, `releaseinstallcheck`, `taskroutecheck`), and a single loop -naming **703 gate scripts**, all of which exist on disk. +naming **704 gate scripts**, all of which exist on disk. `python3 test/pargates.py . ./build/ripwire -j 6` runs the same scripts in parallel so a full verification fits in one sitting. It does not modify `regression.sh`. @@ -7005,7 +7005,7 @@ Listed because the reason is more useful than the silence. shipped**. See `bench/locbench/anchorhop_calib.json`. The mention anchor's reproducible numbers are the ablations in §4. - **A single round gate-count.** Two in-tree numbers disagree (`test/pargates.py`'s docstring says - ~210; `test/argvdiffcheck.sh` says 200+), while the loop in `test/regression.sh` names 703. The + ~210; `test/argvdiffcheck.sh` says 200+), while the loop in `test/regression.sh` names 704. The loop is the authority; the stale docstrings are a known drift. Since 2026-09-10 the number is not written by hand anywhere: `docs/gatecount_build.py` derives it from the loop and rewrites every published site, `test/gatecountcheck.sh` fails if any of them drifts, and `test/manifestcheck.sh` diff --git a/present/deck5_ripwire_build.js b/present/deck5_ripwire_build.js index ac507279a..04c345674 100644 --- a/present/deck5_ripwire_build.js +++ b/present/deck5_ripwire_build.js @@ -1214,7 +1214,7 @@ function storyCards(s, { kick, head, stories, footText }){ kicker(s, "// how it stays true", AMBER); title(s, "Proven, not promised"); const cards = [ - ["703 gate scripts", "the suite runs on every push — plus determinism, cache-transparency and golden contracts; the gate count itself is gated against the runner's own loop"], // gatecount + ["704 gate scripts", "the suite runs on every push — plus determinism, cache-transparency and golden contracts; the gate count itself is gated against the runner's own loop"], // gatecount ["byte-identical, always", "two runs over the same tree produce the same bytes; warm equals cold. Enforced in CI, twice — Release AND a plain flavour, because NDEBUG once blinded a whole class of checks"], ["differential refactoring", "a refactor must prove it changed nothing observable: two binaries, hundreds of argv vectors, stdout + stderr + exit codes byte-identical"], ["held-out labels, authored blind", "eval labels were written by reading source before the ranker ever ran on them — so the eval is allowed to say the ranker is wrong. It has."], @@ -1238,7 +1238,7 @@ function storyCards(s, { kick, head, stories, footText }){ title(s, "Claims you can trust, because we publish what failed", { size: 32 }); card(s, MX, 1.72, 3.86, 1.72); - stat(s, "703", "gate scripts named by test/regression.sh — and the COUNT itself is gated against the runner's own loop, so it cannot go stale quietly", // gatecount + stat(s, "704", "gate scripts named by test/regression.sh — and the COUNT itself is gated against the runner's own loop, so it cannot go stale quietly", // gatecount MX+0.15, 1.86, 3.56, CYAN, { bsize: 42, bh: 0.66, lsize: 9.5 }); card(s, 4.68, 1.72, 3.86, 1.72, CARD2); stat(s, "8", "registered NEGATIVES — changes built, gated green, measured against a band written before the code, and reverted rather than tuned", @@ -1540,7 +1540,7 @@ function storyCards(s, { kick, head, stories, footText }){ ["192 long flags · 36 slides", "bash test/deckclaimcheck.sh"], ["every --flag named here exists", "bash test/deckcheck.sh"], ["74.7% fewer element bytes", "bash test/showcasecapturecheck.sh"], - ["703 gate scripts", "bash test/manifestcheck.sh"], // gatecount + ["704 gate scripts", "bash test/manifestcheck.sh"], // gatecount ["49 repos · 71 papers · 237 surveyed","bash test/readmedriftcheck.sh"], ["the ten moments, any row", "ripwire . --callers=SYM | wc -c"], ["the head-to-head table", "bench/headtohead/r4-2026-08-06/"], diff --git a/src/compactlegend.h b/src/compactlegend.h index 097b2efab..27a3f3431 100644 --- a/src/compactlegend.h +++ b/src/compactlegend.h @@ -723,6 +723,11 @@ inline constexpr CompactCompletenessTerm kCompactCompletenessTerms[] = // --naming-consistency's absent-at-zero count (countAttrXmlOrEmpty, so (S) requires its row HERE): the JSX components // kept out of the vote (namingconsistency.h isJsxComponentName). Key-qualified: no other verb spells it. { "component_exempt", "component_exempt=N: PascalCase .tsx/.jsx functions (read as JSX components by extension alone), kept out of voting and flagging", false, "naming-consistency", MapHeaderRead::No, {}, "naming-consistency" }, + // grep's per-hit tail, spelled through countAttrXmlOrEmpty (verbs_grep.h grepRowTailAttrs). Arm (S) of test/compactlegendcheck.sh + // reads every such name from SOURCE and demands a row in THIS table, so these two moved here from kCompactAttributeReadings + // (moved, not copied: the joint reading space would print a name twice). Still -qualified and grep-only. + { "line_bytes", "line_bytes=N: whole line N bytes; text is a cut prefix", true, "hit", MapHeaderRead::No, {}, "grep" }, + { "occ", "occ=N: the pattern occurs N times on this one line; a hit is a line, hits= counts lines; absent = once", true, "hit", MapHeaderRead::No, {}, "grep" }, }; @@ -957,8 +962,6 @@ inline constexpr CompactCompletenessTerm kCompactAttributeReadings[] = { "string_hits", "string_hits=N: of hits=, string-tier; rows: files named like the pattern, source, then the rest", false, "grep", MapHeaderRead::No, {}, "grep" }, { "ref", "hit ref=: first code site (not a comment/string) naming the hit's callerless enclosing def (where it is registered; one site)", true, "hit", MapHeaderRead::No, {}, "grep" }, { "tier_budget", "tier_budget=: files|bytes cap hit after tier_parsed= of tier_files= hit files; tier counts floors, every row served", false, "grep", MapHeaderRead::No, {}, "grep" }, - { "line_bytes", "line_bytes=N: whole line N bytes; text is a cut prefix", true, "hit", MapHeaderRead::No, {}, "grep" }, - { "occ", "occ=N: the pattern occurs N times on this one line; a hit is a line, hits= counts lines; absent = once", true, "hit", MapHeaderRead::No, {}, "grep" }, { "pattern_meta", "pattern_meta=: regex-shaped pieces of this LITERAL pattern, matched as text; next= is then the regex verb on it", false, "grep", MapHeaderRead::No, {}, "grep" }, { "unindexed_files_scanned", "unindexed_files_scanned=N: off-index text files also scanned; outside complete=", false, "grep", MapHeaderRead::No, {}, "grep" }, { "unindexed_hits", "unindexed_hits=N: hits in off-index files, NOT in hits=/total=; listed in the trailing unindexed element", false, "grep", MapHeaderRead::No, {}, "grep" }, diff --git a/src/crossref.h b/src/crossref.h index 3f32f0224..c9087c543 100644 --- a/src/crossref.h +++ b/src/crossref.h @@ -1896,8 +1896,12 @@ inline bool isDeclMarkerWord( std::string_view w ) noexcept // An export or attribute macro between `class` and its name (`class RW_API Foo`): ALL-CAPS, digits and '_'. inline bool isAllCapsMacro( std::string_view w ) noexcept { - return w.size() >= 2 && std::ranges::all_of( w, []( char c ) { return std::isupper( (unsigned char)c ) || std::isdigit( (unsigned char)c ) || c == '_'; } ) - && std::ranges::any_of( w, []( char c ) { return std::isupper( (unsigned char)c ); } ); + // static_cast, one per line: --lint's c-style-cast count collapses two casts on one line into one visible row + // (dedupeLintFindings), which lintbudgetcheck then reads as a disagreement with --match's raw capture count. + const auto upper = []( char c ) { return std::isupper( static_cast( c ) ) != 0; }; + const auto digit = []( char c ) { return std::isdigit( static_cast( c ) ) != 0; }; + return w.size() >= 2 && std::ranges::all_of( w, [ & ]( char c ) { return upper( c ) || digit( c ) || c == '_'; } ) + && std::ranges::any_of( w, upper ); } // The index of the bracket that closes the group opening at `open` (`<…>`), or npos when it does not close on this line. diff --git a/test/compactlegendcheck.sh b/test/compactlegendcheck.sh index a4239af5d..50b9d3491 100755 --- a/test/compactlegendcheck.sh +++ b/test/compactlegendcheck.sh @@ -356,6 +356,11 @@ echo "=== (A-PIN) --legend=full is BYTE-IDENTICAL to the pre-L1 default (pinned # 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. +# RE-ANCHORED BY HAND 2026-10-23 (lane honest-lex, CI fix round 1), grep.xml only: the lane's intended output change, nothing +# else. Checked: the pin equals the previous head's --legend=full output with exactly four edits -- the full legend gains the +# sentence "A hit is one LINE: occ= on a row (present only above one) counts the pattern's occurrences on that line, and +# hits=, shown= and the window count lines, not occurrences."; the root's hits=/shown= read 7 (was 8: the two occurrences on +# geometry.cpp:16 are one line now); that row's n="2" + its at-tagged sibling become occ="2" (one hit per line). 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. diff --git a/test/compactlegendfix/pre_l1_full/grep.xml b/test/compactlegendfix/pre_l1_full/grep.xml index 1126db064..89b9a15dc 100644 --- a/test/compactlegendfix/pre_l1_full/grep.xml +++ b/test/compactlegendfix/pre_l1_full/grep.xml @@ -1 +1 @@ - distance]]> distance)]]> \ No newline at end of file + distance]]> distance)]]> \ No newline at end of file diff --git a/test/honestlexcheck.sh b/test/honestlexcheck.sh index 2134dbf24..21be57d50 100755 --- a/test/honestlexcheck.sh +++ b/test/honestlexcheck.sh @@ -21,6 +21,7 @@ # Usage: test/honestlexcheck.sh [BIN] | RIPWIRE_BIN=asan/ripwire test/honestlexcheck.sh set -u ROOT="$( cd "$( dirname "$0" )/.." && pwd )" +. "$ROOT/test/lib/clean-env.sh" BIN="${1:-${RIPWIRE_BIN:-$ROOT/build/ripwire}}" [ "${BIN#/}" = "$BIN" ] && BIN="$ROOT/$BIN" fail=0 From 82d8a2ac4b41870162b439b369c346935eca570f Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I <210578314+joyful-ii-V-I@users.noreply.github.com> Date: Fri, 23 Oct 2026 14:32:06 -0400 Subject: [PATCH 48/77] =?UTF-8?q?graph:=20FC3=20fix=20round=202=20?= =?UTF-8?q?=E2=80=94=20a=20member=20call=20through=20a=20written=20root=20?= =?UTF-8?q?keeps=20the=20by-name=20hedge,=20its=20spelling=20names=20no=20?= =?UTF-8?q?class?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit C1 a member call through a pinned written root (let tank = new Tank(); tank = b; tank.spill()) is hedged by name instead of unresolved: receiverevidencecheck's visible arm holds again. C2 a root assigned anywhere in scope (let Eng; Eng = Foo; Eng.make(), an undeclared global, var Foo; Foo = require(a).Eng) never decides by its own spelling: Rule 2c, the class-name receiver reading and the ladder's class-name proof step aside. C3 floored: refusing every JS/TS member call a free function changed ~9,000 census rows (CHANGELOG residual). Co-Authored-By: Claude Sonnet 5.5 --- CHANGELOG.md | 13 +++++ src/graph.h | 47 +++++++++++++------ src/model.h | 9 ++-- src/receiverevidence.h | 6 ++- test/falseedgecheck.sh | 39 +++++++++++++-- .../jsrebind/cases/pinnedhedgeP.js | 3 ++ .../jsrebind/cases/spellimplicitP.js | 3 ++ .../jsrebind/cases/spellnodeclP.js | 4 ++ test/falseedgefix/jsrebind/cases/spellvarP.js | 3 ++ test/falseedgefix/jsrebind/src/gear.js | 2 + 10 files changed, 104 insertions(+), 25 deletions(-) create mode 100644 test/falseedgefix/jsrebind/cases/pinnedhedgeP.js create mode 100644 test/falseedgefix/jsrebind/cases/spellimplicitP.js create mode 100644 test/falseedgefix/jsrebind/cases/spellnodeclP.js create mode 100644 test/falseedgefix/jsrebind/cases/spellvarP.js create mode 100644 test/falseedgefix/jsrebind/src/gear.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 94cd9ce1b..f8764086b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,19 @@ not published here — see `docs/EVALS.md` for the instruments behind the headli ## [Unreleased] +### Fixed — JS/TS calls through a name written after its declaration are no longer read as the declared class + +`let { Eng, Foo } = require('a'); Eng = Foo; new Eng()` builds a `Foo`, so a BARE call by a name written in its scope is +unresolved (`unresolved=`), never its declared target nor a same-spelled class. A MEMBER call through such a root (`Eng.make()`, +`tank.spill()`) is not receiver-typed: the root's spelling does not name a class, so the call keeps only the by-name hedge +(`via="name"`, every candidate), with or without a declaration (`let Eng;`, an undeclared global). A write whose value keeps the +name (`Environment = require('x').Environment` over `var Environment;`) and a stated constructor (`let app; app = new Hono()`) are +unchanged. Residual, not fixed: a member call whose receiver is a typed chain the tool does not follow (kysely +`ctx.db.selectFrom('person').selectAll().limit(100)`) still falls to the by-name ladder, and the merged caller row carries the +sites of a bare imported `limit(…)` call in the same file unhedged. Refusing every member call a free function (which JS/TS allow +through an object property) changed about 9,000 census rows on six in-sample repos (hono, kysely, nunjucks, fastify, commander, koa), +so it is left to a later card. Gate: `test/falseedgecheck.sh` arm (S). + ### Added — `--for` names the members of its shown owner rows on the compact route (`mem=`) On a conceptual `--for` answer (root `bundle="compact"`), a shown class, struct or interface/trait row now carries diff --git a/src/graph.h b/src/graph.h index 6faa1079a..b89c1ad60 100644 --- a/src/graph.h +++ b/src/graph.h @@ -2535,7 +2535,7 @@ struct JsImportTables HashMap targets; HashMap> shadows; HashMap> rebinds; // FC3: "#name" → JsRebind spans (model.h): a call there is undecided - HashMap> pinnedRebinds; // FC3: the rebinds whose declaration already bound a value: a MEMBER call through the name is undecided there + HashMap> pinnedRebinds; // FC3: the rebinds whose declaration already bound a value: a MEMBER call through the name has no receiver at all there (jsMemberRootWritten) }; inline std::string jsImportKey( std::uint32_t fileId, std::string_view name ) @@ -2543,21 +2543,36 @@ inline std::string jsImportKey( std::uint32_t fileId, std::string_view name ) return std::to_string( fileId ) + "#" + std::string( name ); } -// FC3: is this call made through a name WRITTEN after its declaration, at a byte that declaration's scope holds? A bare -// name (`new Eng()`) is that name itself. A member call (`Eng.make()`) holds whatever its ROOT name holds: as undecided as -// the root, but only where the root's declaration had bound a value that the write replaced (`let app; app = new Hono()` -// replaces nothing, and its `app.request()` keeps the receiver reading). +// FC3: is this BARE call made through a name WRITTEN after its declaration, at a byte that declaration's scope holds +// (`Eng = Foo; new Eng()`)? Such a call is undecided: the name no longer names what it was declared as. inline bool jsCallThroughWrittenBinding( const JsImportTables& tables, const Reference& r ) { - if( r.role != RefRole::Call ) { return false; } - const bool bare = r.recv == RecvKind::None && r.qualifier.empty() && !r.memberCall; - if( !bare && ( !r.memberCall || r.memberRoot.empty() ) ) { return false; } - const auto& spansByName = r.memberCall ? tables.pinnedRebinds : tables.rebinds; - const auto spans = spansByName.find( jsImportKey( r.fileId, r.memberCall ? std::string_view( r.memberRoot ) : std::string_view( r.calleeName ) ) ); - return spans != spansByName.end() + if( r.role != RefRole::Call || r.memberCall || r.recv != RecvKind::None || !r.qualifier.empty() ) { return false; } + const auto spans = tables.rebinds.find( jsImportKey( r.fileId, r.calleeName ) ); + return spans != tables.rebinds.end() && std::any_of( spans->second.begin(), spans->second.end(), [ & ]( const VarSpan& span ) { return r.startByte >= span.startByte && r.startByte < span.endByte; } ); } +// FC3: what a MEMBER call's ROOT name (`tank` of `tank.spill()`, `Eng` of `Eng.make()`) holds at the call's byte. A root WRITTEN +// anywhere in its declaration's scope never decides by its own spelling: `Eng = Foo; Eng.make()` is Foo's make, not the class +// the name spells. Pinned = the declaration had already bound a value the write replaced (`let tank = new Tank(); tank = b`): the +// receiver is unknown, so no receiver rule reads it. Unpinned = `let Eng;` / undeclared: the declared type is nothing, but a +// constructed assignment (`let rig; rig = new Rig()`) is still a stated class, so only the SPELLING readings step aside. +// Either way the call falls to the by-name ladder, which hedges it (via=name). +enum class JsWrittenRoot : std::uint8_t { None, Unpinned, Pinned }; + +inline JsWrittenRoot jsMemberRootWritten( const JsImportTables& tables, const Reference& r ) +{ + if( r.role != RefRole::Call || !r.memberCall || r.memberRoot.empty() ) { return JsWrittenRoot::None; } + const auto holds = [ & ]( const HashMap>& byName ) + { + const auto spans = byName.find( jsImportKey( r.fileId, r.memberRoot ) ); + return spans != byName.end() + && std::any_of( spans->second.begin(), spans->second.end(), [ & ]( const VarSpan& span ) { return r.startByte >= span.startByte && r.startByte < span.endByte; } ); + }; + return holds( tables.pinnedRebinds ) ? JsWrittenRoot::Pinned : holds( tables.rebinds ) ? JsWrittenRoot::Unpinned : JsWrittenRoot::None; +} + // `utils.ts` and `utils` name the same module, so the vocabulary probe below has to compare stems. inline std::string_view jsModuleStem( std::string_view segment ) { @@ -7525,6 +7540,7 @@ inline Graph buildGraph( const IngestResult& ing, const ScipOverlay* scip = null const Symbol& cs = ing.symbols[ c ]; if( isMemberCallRef( ref ) ) { + if( jsMemberRootWritten( jsImports, ref ) != JsWrittenRoot::None ) { return false; } // a written root names no class by its spelling const ReceiverEvidence::Chain ch = ReceiverEvidence::chainOf( ref ); if( !ch.valid || !ch.path.empty() || !ch.ctor.empty() ) { @@ -8442,6 +8458,9 @@ inline Graph buildGraph( const IngestResult& ing, const ScipOverlay* scip = null disposition = CallDisposition::Unresolved; continue; } + // …and a MEMBER call through a written root is not receiver-typed by its spelling (jsMemberRootWritten): it goes on to the by-name ladder + const JsWrittenRoot writtenRoot = !scipPinned && !jsImports.rebinds.empty() && langFacts( r.lang ).writtenBindingUndecided + ? jsMemberRootWritten( jsImports, r ) : JsWrittenRoot::None; if( !scipPinned && r.role == RefRole::Call && r.recv == RecvKind::None && r.qualifier.empty() && ( r.lang == Lang::TypeScript || r.lang == Lang::JavaScript ) ) { @@ -8619,7 +8638,7 @@ inline Graph buildGraph( const IngestResult& ing, const ScipOverlay* scip = null // nested namesakes dropped, an inherited body, an interface's dispatch split); otherwise narrowed stays false and the name-based fallback // runs. Skipped when the call was already pinned canonically or by Rule 1 (the more specific / already-resolved signals). const bool narrowedBeforeReceiverRules = narrowed; - if( !scipPinned && !canonical && !narrowed ) + if( !scipPinned && !canonical && !narrowed && writtenRoot != JsWrittenRoot::Pinned ) { narrowed = narrowTo( narrower.rule2RecvVarType( r, classIds, chaUp ), r, cand ) || narrower.forgetClaim(); } @@ -8627,7 +8646,7 @@ inline Graph buildGraph( const IngestResult& ing, const ScipOverlay* scip = null // local and no C++ member of the caller's class or its bases hides (then 2b reads the member). After Rule 2, before 2b. See Narrower::rule2cClassNameRecv. // (L) fix round 2 / 3 (D3, D6): a JVM receiver the caller does not SEE in the tree (`import ext.Util;`, `import ext.*;` // alone) is an outside class, never an in-tree namesake — the call falls to the ladder, which hedges it. - if( !scipPinned && !canonical && !narrowed + if( !scipPinned && !canonical && !narrowed && writtenRoot == JsWrittenRoot::None && !( callResultFamilyOf( r.lang ) == CallResultFamily::Jvm && recvEv.types.jvmHides( r.fileId, r.startByte, std::string_view( r.recvVar ).substr( 0, std::min( r.recvVar.find( '.' ), r.recvVar.size() ) ) ) ) ) { @@ -8651,7 +8670,7 @@ inline Graph buildGraph( const IngestResult& ing, const ScipOverlay* scip = null // FE-B receiver evidence (receiverevidence.h, point 1): a receiver whose class the source states — `this`/`super` // in a class, a typed parameter or local, a constructed receiver, a class-name receiver, a chain of stated field // classes, Python's `feed = parser.feed` — resolves to that class's member or the shallowest base defining it. - if( !scipPinned && !canonical && !narrowed && recvEv.narrow( r ) ) + if( !scipPinned && !canonical && !narrowed && writtenRoot != JsWrittenRoot::Pinned && recvEv.narrow( r, writtenRoot == JsWrittenRoot::None ) ) { for( NodeId c : recvEv.found ) { diff --git a/src/model.h b/src/model.h index 3cc88f7f7..641dfa3f1 100644 --- a/src/model.h +++ b/src/model.h @@ -1264,16 +1264,17 @@ enum class LocalBindKind : std::uint8_t // `-> "Color"`). A method that proves neither records nothing. APPENDED. // FC3 (T1 class 14 ALIAS-THROUGH-ASSIGNMENT): a JS/TS binding WRITTEN after its declaration — `Eng = Foo`, `[Eng] = [Foo]`, // `({ Eng } = { Eng: Foo })`, `Eng += …`, `Eng++`, `for ( Eng of xs )`, a second `var Eng = Foo`, but not a write whose - // value keeps the name (`equal = util.equal`) — so a call or construction by that name in its scope is undecided + // value keeps the name (`equal = util.equal`) — so a BARE call or construction by that name in its scope is undecided // (ingest_jsimports.h captureJsRebindFacts). var = the name; spanStart/spanEnd = the WHOLE scope of the declaration the // write resolves to (the program, for a name the file never declares): byte order is not run order, a closure or a loop // sees the write anywhere in it; startByte = the file's END, so the record has no owner (fromSymbol kNoNode) and no // (fromSymbol, var) local-name reader takes it for a function's local. An ES import or `const` binding records nothing: - // writing one throws. Read by graph.h buildGraph (the call is unresolved, no edge) and FalseEdgeRules (as a shadow: no + // writing one throws. Read by graph.h buildGraph (a bare call is unresolved, no edge; a member call is hedged by name) and FalseEdgeRules (as a shadow: no // external veto) ONLY. isFromAssignment = the declaration already BOUND a value (an initializer, a parameter, a `class` / // `function` / import / `for` head — not `var x;` / `let x;` / an undeclared name): a MEMBER call through the name - // (`Eng.make()` after `Eng = Foo`) is undecided only then; `let app; … app = new Hono(); app.request()` replaces - // nothing a declaration pinned and keeps its receiver reading. APPENDED (cache u8). + // (`tank.spill()` after `let tank = new Tank(); tank = b`) has no receiver evidence at all then and goes to the by-name hedge + // (via=name); over a `var x;` / `let x;` / undeclared name only the root's SPELLING stops deciding (`Eng = Foo; Eng.make()` is + // not the class `Eng`) while a stated type (`let app; … app = new Hono(); app.request()`) keeps its receiver reading. APPENDED (cache u8). JsRebind, }; // fix round 4: the TypeAlias rows that bind a spelling with no alias target (model.h LocalBindKind::TypeAlias, typeName) diff --git a/src/receiverevidence.h b/src/receiverevidence.h index 9d38b4528..87dd4711c 100644 --- a/src/receiverevidence.h +++ b/src/receiverevidence.h @@ -2543,7 +2543,9 @@ struct ReceiverEvidence // RESOLVE: the definitions this member call's typed receiver reaches, in `found`; false when the receiver proves // nothing (untyped, a type this tree does not define, or a type that defines no such member anywhere in its cone). - bool narrow( const Reference& r ) const + // `spellingDecides` false: the receiver root is a name WRITTEN in its scope (graph.h JsWrittenRoot), so the class its SPELLING names + // is not its class — only a stated type (a typed local, a constructed receiver, `this`) may decide. + bool narrow( const Reference& r, bool spellingDecides = true ) const { // C++, Ruby, Rust and C keep their own receiver rules (Rules 1, 2, 2b, 2c): this one adds the languages that had none const bool typedLang = r.lang == Lang::JavaScript || r.lang == Lang::TypeScript || r.lang == Lang::Python || r.lang == Lang::Go @@ -2569,7 +2571,7 @@ struct ReceiverEvidence bool isStatic = false, superOnly = false; NodeId cls = kNoNode; std::string_view type = rootClass( r, ch, isStatic, superOnly, &cls ); - if( type.empty() || ( cls != kNoNode && !ch.path.empty() && types.sameNamedClasses( cls ) != 1 ) ) + if( type.empty() || ( isStatic && !spellingDecides ) || ( cls != kNoNode && !ch.path.empty() && types.sameNamedClasses( cls ) != 1 ) ) { return false; // a field path is read by NAME (memberType): below a class another in-tree class names too, a floor } diff --git a/test/falseedgecheck.sh b/test/falseedgecheck.sh index a3f03094d..9172b0360 100755 --- a/test/falseedgecheck.sh +++ b/test/falseedgecheck.sh @@ -83,7 +83,9 @@ # WITHOUT an initializer (`var Eng;` / `let Eng;`) — the declare-then-assign test idiom (over `let { Eng } = # require( 'a' )` or `class Eng` the write replaces a pinned definition: withheld) — while a default (`({ Eng = mod.Foo } = mod)`) does not keep it; `reply.run()` beside # a written local `run` (a member call is not the bare name); a MEMBER call through a written ROOT name -# (`Eng = Foo; Eng.run()`, `m = require( b ); m.Eng()`) is withheld; patterns nested 20000 deep. +# (`Eng = Foo; Eng.run()`, `m = require( b ); m.Eng()`, `let tank = new Tank(); tank = b; tank.drip()`) keeps only the +# by-name hedge (via="name", every candidate) — never a pinned row, never the class its SPELLING names, with or +# without a declaration (FIX2 C1/C2); patterns nested 20000 deep. # (F) propagation: --callers and --impact of the in-repo decoys no longer list the false callers. # (G) disclosure: every call the arms above unbind is a `C external` census row (empty targets) — except a Python bare # name nothing binds and no builtin table holds (run_all's `process`, siblings.py's `helper`), which has no @@ -202,6 +204,24 @@ PY then ok "($r) --$v=$s lists [$want] as a hedged via=name row" else no "($r) --$v=$s has no hedged via=name row [$want]"; fi } +# unpinned ROOT VERB SEL "n file" … — each named row is absent or carries via="name": never a confident (pinned) row +unpinned(){ + local r="$1" v="$2" s="$3"; shift 3 + local f; f="$( answer "$r" "$v" "$s" )"; ran_ok "$r" "$v" "$s" "$f" || return + local want + for want in "$@"; do + if python3 - "$f" "${want% *}" "${want#* }" <<'PY' +import re, sys +doc, name, path = sys.argv[ 1: ] +for s in re.findall( r"]*/>", open( doc, encoding="utf-8", errors="replace" ).read() ): + n = re.search( r' n="([^"]*)"', s ); p = re.search( r' p="([^"]*)"', s ) + if n and p and n.group( 1 ) == name and p.group( 1 ).rsplit( ":", 1 )[ 0 ] == path and ' via="name"' not in s: sys.exit( 1 ) +sys.exit( 0 ) +PY + then ok "($r) --$v=$s has no confident row [$want]" + else no "($r) --$v=$s lists [$want] as a confident (pinned) row"; fi + done +} # exactly ROOT VERB SEL "t n file;t n file" — the exact row set (";"-separated, sorted); "" = no rows at all exactly(){ local r="$1" v="$2" s="$3" want="$4" @@ -390,19 +410,28 @@ exactly rs callees src/ufcs.rs:draw_all "method render_all src/ufcs.rs" echo "=== (S) JS/TS rebind: a written binding names nothing (FC3) ===" # the exact row set: every case file whose binding is written is GONE; each near miss keeps its row (nestedP keeps h, # its g wrote its own `let Eng`; classexprP's m constructs its own class expression) -exactly jsrebind callers Eng "fn h cases/nestedP.js;method m cases/classexprP.js;modscope <file-scope> cases/constwriteP.js;modscope <file-scope> cases/esimport.js;modscope <file-scope> cases/keepdestrP.js;modscope <file-scope> cases/keepmemberP.js;modscope <file-scope> cases/memberwriteP.js;modscope <file-scope> cases/norewriteP.js;modscope <file-scope> cases/otherfileP_y.js;modscope <file-scope> cases/paramP.js;modscope <file-scope> cases/redeclnoinitP.js" +exactly jsrebind callers Eng "fn h cases/nestedP.js;method m cases/classexprP.js;modscope <file-scope> cases/constwriteP.js;modscope <file-scope> cases/esimport.js;modscope <file-scope> cases/keepdestrP.js;modscope <file-scope> cases/keepmemberP.js;modscope <file-scope> cases/memberroot.js;modscope <file-scope> cases/memberwriteP.js;modscope <file-scope> cases/norewriteP.js;modscope <file-scope> cases/otherfileP_y.js;modscope <file-scope> cases/paramP.js;modscope <file-scope> cases/redeclnoinitP.js" lacks jsrebind callers leaf "<file-scope> cases/fncall.js" has jsrebind callers leaf "method run src/eng.js" lacks jsrebind callers Foo "<file-scope> cases/destr.js" "<file-scope> cases/plainreq.js" # undecided is not Foo either # a member call spelled like a written LOCAL is not that local: `let run = ''; run += ' '; reply.run( … )` keeps its row has jsrebind callers run "fn onSendEnd cases/memberwriteP.js" # FIX1 B3: a MEMBER call is read through its ROOT name — `Eng = Foo; Eng.run()` calls Foo's, a rebound module alias -# `m = require( b ); m.Eng()` whatever b holds (memberroot.js, once a floor, is gone from the Eng rows above), and a +# `m = require( b ); m.Eng()` whatever b holds (memberroot.js: a hedged row below), and a # local class `Pack = Crate; Pack.make()` — when the write REPLACES a value the declaration bound. Near misses keep: a # name nobody writes, and `let rig; … rig = new Rig(); rig.fire()` (the declaration bound nothing: the test-file idiom; # measured: refusing it dropped 400+ true rows from hono alone) -lacks jsrebind callers run "<file-scope> cases/memberclass.js" -exactly jsrebind callers make "modscope <file-scope> cases/membernowriteP.js" +# FIX2 C1: such a member call is not DROPPED either — a written root is no receiver, so the call keeps the by-name ladder's +# hedge (via="name", every candidate): the true target stays visible, never a pinned row (memberclass, memberlocal, memberroot) +hedged jsrebind callers run "<file-scope> cases/memberclass.js" +hedged jsrebind callers Eng "<file-scope> cases/memberroot.js" +exactly jsrebind callers make "modscope <file-scope> cases/memberlocal.js;modscope <file-scope> cases/membernowriteP.js" +hedged jsrebind callers make "<file-scope> cases/memberlocal.js" +hedged jsrebind callers drip "reassign cases/pinnedhedgeP.js" # `let tank = new Tank(); tank = b; tank.drip()` +# FIX2 C2: a root ASSIGNED anywhere in its scope never decides by its own SPELLING — `Eng = Foo; Eng.run()` is not the class +# `Eng`, with or without a declaration, nor `Cog = require( … ).Gear; Cog.spin()` the class `Cog`: hedged at best, never pinned +unpinned jsrebind callers spin "<file-scope> cases/spellnodeclP.js" "<file-scope> cases/spellimplicitP.js" "<file-scope> cases/spellvarP.js" +hedged jsrebind callers spin "<file-scope> cases/spellnodeclP.js" # the by-name hedge keeps both candidates, not a guess at either has jsrebind callers fire "modscope <file-scope> cases/memberunpinnedP.js" # FIX1 FC5: `reply.leaf( … )` is a member call: the ES-imported function `leaf` of the same file does not PIN it hedged jsrebind callers leaf "send cases/esmemberP.js" # by name at best: not the imported `leaf` as a pinned row diff --git a/test/falseedgefix/jsrebind/cases/pinnedhedgeP.js b/test/falseedgefix/jsrebind/cases/pinnedhedgeP.js new file mode 100644 index 000000000..023d6af9d --- /dev/null +++ b/test/falseedgefix/jsrebind/cases/pinnedhedgeP.js @@ -0,0 +1,3 @@ +class Tank { drip() { return 1; } } +class Barrel { drip() { return 2; } } +export function reassign(b) { let tank = new Tank(); tank = b; tank.drip(); } diff --git a/test/falseedgefix/jsrebind/cases/spellimplicitP.js b/test/falseedgefix/jsrebind/cases/spellimplicitP.js new file mode 100644 index 000000000..663a9be12 --- /dev/null +++ b/test/falseedgefix/jsrebind/cases/spellimplicitP.js @@ -0,0 +1,3 @@ +const { Cog } = require("../src/gear"); +Gear = Cog; +test("x", () => { Gear.spin(); }); diff --git a/test/falseedgefix/jsrebind/cases/spellnodeclP.js b/test/falseedgefix/jsrebind/cases/spellnodeclP.js new file mode 100644 index 000000000..ecca12135 --- /dev/null +++ b/test/falseedgefix/jsrebind/cases/spellnodeclP.js @@ -0,0 +1,4 @@ +const { Cog } = require("../src/gear"); +let Gear; +Gear = Cog; +test("x", () => { Gear.spin(); }); diff --git a/test/falseedgefix/jsrebind/cases/spellvarP.js b/test/falseedgefix/jsrebind/cases/spellvarP.js new file mode 100644 index 000000000..326d9abfd --- /dev/null +++ b/test/falseedgefix/jsrebind/cases/spellvarP.js @@ -0,0 +1,3 @@ +var Cog; +Cog = require("../src/gear").Gear; +test("x", () => { Cog.spin(); }); diff --git a/test/falseedgefix/jsrebind/src/gear.js b/test/falseedgefix/jsrebind/src/gear.js new file mode 100644 index 000000000..0a0d68e0b --- /dev/null +++ b/test/falseedgefix/jsrebind/src/gear.js @@ -0,0 +1,2 @@ +export class Gear { static spin() { return 1; } } +export class Cog { static spin() { return 2; } } From 8fc7a98a25f69283e0130c395a4b5b59bb797be7 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I <210578314+joyful-ii-V-I@users.noreply.github.com> Date: Fri, 23 Oct 2026 15:28:31 -0400 Subject: [PATCH 49/77] fix(hop1): size kBackHopViaNames by an exact enum count (enumtablecheck 3) Co-Authored-By: Claude Opus 5.5 --- src/backhops.h | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/backhops.h b/src/backhops.h index bc8a90dd6..8af84192a 100644 --- a/src/backhops.h +++ b/src/backhops.h @@ -29,6 +29,7 @@ #include "valuerefs.h" // VrRender / vrPath — p= spelled as every other row #include "infra/Diagnostics.h" // EXPECTS #include "infra/sortutil.h" // svLess +#include "infra/enumcount.h" // enumCountIsExact — kBackHopViaNames is sized by the enum #include #include @@ -337,7 +338,9 @@ class BackwardHops mutable HashMap> m_memo; }; -inline constexpr std::array kBackHopViaNames = { "slot", "slot-name", "declined" }; // indexed by BackHopVia +inline constexpr std::size_t kBackHopViaCount = 3; +static_assert( enumCountIsExact(), "kBackHopViaCount must equal the BackHopVia count — raise it with the append" ); +inline constexpr std::array kBackHopViaNames = { "slot", "slot-name", "declined" }; // indexed by BackHopVia // Whether any of `sinks` has a backward hop a consumer follows (width within kBackHopWidth): --path's root next= swap. inline bool anyFollowedBackHop( const BackwardHops& hops, std::span sinks ) From a02e84397e2599c0518addc5ca3e638dfd1f250f Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I <210578314+joyful-ii-V-I@users.noreply.github.com> Date: Fri, 23 Oct 2026 16:12:56 -0400 Subject: [PATCH 50/77] pathgaps fix 6: a package that binds the name is not its submodule; a renamed re-export hop is not the name F1: pythonImportedBases reads the submodule route only when the package's __init__ does not bind the name (pythonPackageMayBind), so withhold-by-default replaces the f03/f03b confident-wrong rows. F2: pythonReexportChain stops at a hop whose import renames its member (pythonFileRenames); nameBinds also records the names a base clause is looked up under (addPythonLookupNames). C1: pyimportprecisecheck #382 pins the intended from:a marker, with a history entry. C2: pushPythonNodeDecl shares the one BindSite construction of the match/type/type-parameter VarDecl records (grepbytescheck BindSite 70). Co-Authored-By: Claude Sonnet 5.5 --- CHANGELOG.md | 5 ++- src/graph.h | 71 +++++++++++++++++++++++++++++++++--- src/ingest_binds.h | 24 +++++++----- test/pathgapcheck.sh | 58 +++++++++++++++++++++++++++++ test/pyimportprecisecheck.sh | 8 +++- 5 files changed, 149 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c19e8b66..71333ebcd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -47,7 +47,10 @@ not published here — see `docs/EVALS.md` for the instruments behind the headli (`import a.b as q`), since the import tables are per file; this over-hedges the same import repeated in two scopes, and a star import from an in-tree module whose `__all__` omits the name. `from pkg import other as a` with `a.Base` now reads `other` (the member's own name), not `a`; a dotted `import pkg.x` binds the root `pkg`, which no table names, so - `pkg.Base` is withheld (this also over-hedges `import pkg.x as pkg`). The import must be the name's one binding; + `pkg.Base` is withheld (this also over-hedges `import pkg.x as pkg`). `from pkg import a` (aliased or not) with `a.Base` + reads the submodule `a` only when the package's `__init__` does not bind `a` itself (an import, def, class or assignment of + it, a star import that could bind it, or a module `__getattr__`), and a re-export hop that renames (`from pkg.x.a import + Other as Base` in the package or a middle module) is not followed. The import must be the name's one binding; otherwise the site is withheld, never shown. Not read, and not detectable statically: a rebinding made from outside the file (`a.Base = Other` on an imported module). diff --git a/src/graph.h b/src/graph.h index 24dcd7931..ab714cad9 100644 --- a/src/graph.h +++ b/src/graph.h @@ -2039,7 +2039,7 @@ struct PythonNameBind struct PythonScopeFacts { HashMap> nameBinds; // "#name" → EVERY binding of the name in that file, in any scope; - // only for names a Python class's base clause starts with + // only for names a Python class's base clause starts with or is looked up under std::vector> starTargets; // Python fileId → the file each `from m import *` names (kNoFile: // unresolved) — a star import binds names no table lists }; @@ -2183,6 +2183,28 @@ inline HashMap pythonBaseClauseNames( const IngestResult& ing return names; } +// the names a base clause is LOOKED UP under in another file, beyond the clause's first name: a qualified clause's own name (`q.Base`: +// Base, what a re-export chain hops by) and the member an aliased import of a first name renames (`from pkg.x import a as q`: a, the name +// pkg's __init__ may bind). pythonReexportChain and pythonImportedBases read the bindings of exactly these (a renamed hop, a package +// that binds the submodule's name). +inline void addPythonLookupNames( const IngestResult& ing, HashMap& names ) +{ + for( const Reference& r : ing.references ) + { + if( r.isInherit && !r.memberRoot.empty() && r.fileId < ing.files.size() && isPythonSourcePath( rootRelPath( ing, r.fileId ) ) ) + { + names.try_emplace( r.calleeName, '\0' ); + } + } + for( const Binding& b : ing.bindings ) + { + if( b.kind == LocalBindKind::Import && b.importedName.starts_with( kPythonFromMemberMark ) && names.contains( b.var ) ) + { + names.try_emplace( b.importedName.substr( kPythonFromMemberMark.size() ), '\0' ); + } + } +} + // the scope a Python binding sits in: its enclosing def or class, kNoNode for module scope (including every binding at byte 0 — // pythonBindAtFileStart: a `global Base` rebinding inside a def that starts the file, and that def's own name). inline NodeId pythonBindScope( const IngestResult& ing, const Binding& b ) noexcept @@ -2196,12 +2218,13 @@ inline NodeId pythonBindScope( const IngestResult& ing, const Binding& b ) noexc } // PythonScopeFacts::nameBinds: every binding of a name a Python class's base clause starts with (the bare base, else its -// qualifier), per file and scope. buildPythonModuleRebindVetoes keeps only the module-scope facts; the base-name rule (pythonBaseNameProven) +// qualifier) or is looked up under (addPythonLookupNames), per file and scope. buildPythonModuleRebindVetoes keeps only the module-scope facts; the base-name rule (pythonBaseNameProven) // needs the scoped ones too, because a class statement resolves its base in ITS lexical scope chain, not the module's. inline HashMap> buildPythonNameBinds( const IngestResult& ing ) { HashMap> binds; - const HashMap wanted = pythonBaseClauseNames( ing ); + HashMap wanted = pythonBaseClauseNames( ing ); + addPythonLookupNames( ing, wanted ); std::string key; for( const Binding& b : ing.bindings ) { @@ -6375,6 +6398,21 @@ inline std::uint32_t pythonImportFileOf( const HashMapsecond; } +// the module-scope bindings of `name` in Python file `f`, if the table records any (only the names pythonBaseClauseNames lists) +inline const std::vector* pythonModuleBindsOf( const ExternalVetoTables& v, std::uint32_t f, std::string_view name ) +{ + std::string key; + const auto it = v.pythonScopes.nameBinds.find( fileNameKey( key, f, name ) ); + return it == v.pythonScopes.nameBinds.end() ? nullptr : &it->second; +} + +// does file `f` import `name` under another name than the member's own (`from m import Other as name`)? +inline bool pythonFileRenames( const ExternalVetoTables& v, std::uint32_t f, std::string_view name ) +{ + const std::vector* binds = pythonModuleBindsOf( v, f, name ); + return binds != nullptr && std::ranges::any_of( *binds, []( const PythonNameBind& b ) { return b.scope == kNoNode && !b.member.empty(); } ); +} + inline constexpr int kPythonReexportHops = 4; // member-import hops provenBasesOf follows (a package re-exporting a re-export) // a Python `from m import N` names m's file `f`; when m defines no candidate it may RE-EXPORT the name (a package @@ -6386,7 +6424,13 @@ inline void pythonReexportChain( const BaseEvidenceTables& t, const std::vector< for( int hop = 0; hop < kPythonReexportHops && f != kNoFile && out.empty(); ++hop ) { appendCandidatesIn( t.ing, cand, f, out ); - f = out.empty() ? pythonImportFileOf( t.extVeto.memberImportFile, f, link, key ) : f; + if( !out.empty() ) + { + return; + } + // hop through `f` only when its import of `link` keeps the name: `from pkg.x.a import Other as Base` exports Other, and the + // candidates here are named Base (the import keys memberImportFile by the alias) + f = pythonFileRenames( t.extVeto, f, link ) ? kNoFile : pythonImportFileOf( t.extVeto.memberImportFile, f, link, key ); } } @@ -6447,6 +6491,22 @@ inline bool pythonStarMayBind( const ExternalVetoTables& v, std::uint32_t f, std return false; } +// can package file `pkg` (its __init__.py) bind `name` itself? `from pkg import name` reads the package's attribute FIRST and falls back +// to the submodule `name` only when there is none, so the submodule is the answer only when nothing in the package binds the name: no +// import, def, class or assignment of it at module scope (a `global` anywhere counts, pythonModuleRebind), no module `__getattr__` +// (which answers the lookup before the fallback) and no star import that could bind it. Unknown answers yes. +inline bool pythonPackageMayBind( const ExternalVetoTables& v, std::uint32_t pkg, std::string_view name ) +{ + std::string key; + if( const std::vector* binds = pythonModuleBindsOf( v, pkg, name ); + binds != nullptr && std::ranges::any_of( *binds, []( const PythonNameBind& b ) { return b.scope == kNoNode; } ) ) + { + return true; + } + return v.pythonModuleRebind.contains( fileNameKey( key, pkg, name ) ) || v.pythonModuleRebind.contains( fileNameKey( key, pkg, "__getattr__" ) ) + || pythonStarMayBind( v, pkg, name ); +} + // Is the import the ONE thing `name` can mean where the class statement `r` sits? Proven only when, in the statement's scope chain // (pythonClassScopeChain) and the module, the name has exactly one binding, an import, and no star import could bind it: any other // binding (an assignment of any form, a def, a class, a loop or with or except target, a parameter, a global or nonlocal, a match @@ -6528,9 +6588,10 @@ inline void pythonImportedBases( const BaseEvidenceTables& t, const Reference& r } const std::string_view pkgDir = includerDir( rootRelPath( t.ing, pkg ) ); const std::string_view link = bind->member.empty() ? qual : std::string_view( bind->member ); // `from pkg import other as a`: pkg's name is `other` (E2) + const bool shadowed = pythonPackageMayBind( t.extVeto, pkg, link ); // the package's own `link` wins over the submodule (F1) for( NodeId c : cand ) { - if( pythonSubmoduleOf( rootRelPath( t.ing, t.ing.symbols[ c ].fileId ), pkgDir, link ) && std::find( out.begin(), out.end(), c ) == out.end() ) + if( !shadowed && pythonSubmoduleOf( rootRelPath( t.ing, t.ing.symbols[ c ].fileId ), pkgDir, link ) && std::find( out.begin(), out.end(), c ) == out.end() ) { out.push_back( c ); } diff --git a/src/ingest_binds.h b/src/ingest_binds.h index 5a4b8c88a..e07cbaaf5 100644 --- a/src/ingest_binds.h +++ b/src/ingest_binds.h @@ -3326,9 +3326,18 @@ inline TSNode pythonPatternTarget( TSNode n, const char* t ) return kindIs( t, "type_alias_statement" ) ? pythonTypeAliasName( n ) : TSNode{}; } +// a Python name node as a VarDecl at byte `at` (veto evidence: the match/`type`/type-parameter records below); a null node records nothing +inline void pushPythonNodeDecl( TSNode id, std::uint32_t at, std::uint32_t fileId, std::string_view src, std::vector& binds ) +{ + if( !ts_node_is_null( id ) ) + { + pushRawBind( fileId, Lang::Python, nodeTextOf( id, src ), std::string{}, BindSite{ at, 0u, 0u }, LocalBindKind::VarDecl, binds ); + } +} + // the name a match pattern or `type` statement binds, as a VarDecl at the statement (veto evidence like capturePythonRebindShadowDecls'), // or at byte 0 when the enclosing function declares it global/nonlocal -inline void capturePythonPatternDecls( TSNode n, std::uint32_t fileId, Lang lang, std::string_view src, std::vector& binds ) +inline void capturePythonPatternDecls( TSNode n, std::uint32_t fileId, std::string_view src, std::vector& binds ) { const TSNode id = pythonPatternTarget( n, ts_node_type( n ) ); if( ts_node_is_null( id ) ) @@ -3336,7 +3345,7 @@ inline void capturePythonPatternDecls( TSNode n, std::uint32_t fileId, Lang lang return; } const std::uint32_t at = pythonNameIsGlobalOrNonlocalHere( n, nodeTextOf( id, src ), src ) ? 0u : ts_node_start_byte( n ); - pushRawBind( fileId, lang, nodeTextOf( id, src ), std::string{}, BindSite{ at, 0u, 0u }, LocalBindKind::VarDecl, binds ); + pushPythonNodeDecl( id, at, fileId, src, binds ); } // PEP 695 `class C[Base](Base)` / `def f[Base]()`: each type parameter is a name the base clause can resolve to (a TypeVar, so CPython @@ -3353,7 +3362,7 @@ inline TSNode pythonTypeParamName( TSNode param ) // `type` > identifier | con return !ts_node_is_null( c ) && kindIs( ts_node_type( c ), "identifier" ) ? c : TSNode{}; } -inline void capturePythonTypeParamDecls( TSNode n, std::uint32_t fileId, Lang lang, std::string_view src, std::vector& binds ) +inline void capturePythonTypeParamDecls( TSNode n, std::uint32_t fileId, std::string_view src, std::vector& binds ) { const bool isClass = kindIs( ts_node_type( n ), "class_definition" ); const TSNode list = isClass || kindIs( ts_node_type( n ), "function_definition" ) ? fieldChild( n, NodeField::TypeParameters ) : TSNode{}; @@ -3365,10 +3374,7 @@ inline void capturePythonTypeParamDecls( TSNode n, std::uint32_t fileId, Lang la const std::uint32_t at = isClass ? ( own > 0 ? own - 1 : 0 ) : ts_node_start_byte( list ); for( std::uint32_t i = 0, k = ts_node_named_child_count( list ); i < k; ++i ) { - if( const TSNode id = pythonTypeParamName( ts_node_named_child( list, i ) ); !ts_node_is_null( id ) ) - { - pushRawBind( fileId, lang, nodeTextOf( id, src ), std::string{}, BindSite{ at, 0u, 0u }, LocalBindKind::VarDecl, binds ); - } + pushPythonNodeDecl( pythonTypeParamName( ts_node_named_child( list, i ) ), at, fileId, src, binds ); } } @@ -3505,8 +3511,8 @@ inline void captureShadowScopeDecls( TSNode n, const char* t, std::uint32_t file capturePythonParamShadowDecls( n, fileId, lang, src, binds ); // Phase 4b: veto evidence only (empty span) } capturePythonRebindShadowDecls( n, t, fileId, lang, src, binds ); // issue #287 round 2: rebind veto evidence - capturePythonPatternDecls( n, fileId, lang, src, binds ); // ind4b fix 4: match captures and `type` statements - capturePythonTypeParamDecls( n, fileId, lang, src, binds ); // ind4b fix 5: PEP 695 type parameters + capturePythonPatternDecls( n, fileId, src, binds ); // ind4b fix 4: match captures and `type` statements + capturePythonTypeParamDecls( n, fileId, src, binds ); // ind4b fix 5: PEP 695 type parameters return; } if( lang != Lang::Cpp && lang != Lang::ObjC ) diff --git a/test/pathgapcheck.sh b/test/pathgapcheck.sh index c63d6b72a..2082d5e80 100755 --- a/test/pathgapcheck.sh +++ b/test/pathgapcheck.sh @@ -86,6 +86,9 @@ # (K19-K20) fix 5: `from m import Other as Base` (memberalias), `from pkg.x import other as a` (qualalias) and `import pkg.x` (dottedroot, # binds the root) name something else than the written name says, and a PEP 695 type parameter shadows the base (tpclass, # tpdef): all withheld; K19 qualok now PROVES `from pkg.x import a as q` (q.Base) through the recorded member name; K20 stays. +# (K21-K22) fix 6: a package that BINDS the name (`from pkg.y import base2 as a` in pkg/x/__init__.py) is not its submodule a.py (shadowalias, +# shadowplain), and a re-export hop that RENAMES (`from pkg.x.a import Other as Base` in the package or a middle module) exports Other, +# not a.py's Base (reexpalias, reexpmod, reexpmid): all withheld; K22 re-exports under the SAME name and stays a way in. # # Usage: test/pathgapcheck.sh [BIN] (BIN defaults to RIPWIRE_BIN, then build/ripwire) set -u @@ -605,6 +608,38 @@ for v in memberalias qualalias dottedroot tpclass tpdef sameas qualok outform do printf "$out" > "$d/other/b.py" done # K19 end +# K21 fixtures (lane ind4b fix 6): what the package a name is imported from BINDS, and the renamed import a re-export chain hops through. +# CPython (3.13) truth per variant: shadowalias/shadowplain: pkg/x/__init__.py binds `a` to pkg.y.base2, so `from pkg.x import a [as q]` is +# base2 (getattr first, the submodule only as a fallback) - F1; reexpalias/reexpmod/reexpmid: the hop file reads `from pkg.x.a import Other as +# Base`, so Sub's base is pkg.x.a.Other, not the Base a.py also holds - F2 (reexpmod: `import pkg.x as q` + q.Base; reexpmid: the hop is +# pkg/x/mid.py, not the package). Controls (stay a way in): reexpplain (the hop re-exports the SAME name), reexpmidplain (the same, through mid.py). +# K21 begin +for v in shadowalias shadowplain reexpalias reexpmod reexpmid reexpplain reexpmidplain; do d="$TMP/c6_$v"; mkdir -p "$d/pkg/x" "$d/pkg/y" "$d/other" + printf 'class Base:\n def render_line(self):\n return 1\n\nclass Other:\n pass\n' > "$d/pkg/x/a.py" + printf 'from pkg.x.a import Base\ndef run(obj):\n return obj.render_line()\n' > "$d/pkg/x/c.py" + printf 'class Base:\n def other(self):\n return 3\n' > "$d/pkg/y/base2.py" + : > "$d/pkg/__init__.py"; : > "$d/pkg/x/__init__.py"; : > "$d/pkg/y/__init__.py" + tgt='def target():\n return 2\n\n' + sub() { printf 'class Sub(%s):\n def render_line(self):\n return target()\n' "$1"; } + case "$v" in + shadowalias) printf 'from pkg.y import base2 as a\n' > "$d/pkg/x/__init__.py" + out="from pkg.x import a as q\n\n${tgt}$(sub q.Base)\n" ;; + shadowplain) printf 'from pkg.y import base2 as a\n' > "$d/pkg/x/__init__.py" + out="from pkg.x import a\n\n${tgt}$(sub a.Base)\n" ;; + reexpalias) printf 'from pkg.x.a import Other as Base\n' > "$d/pkg/x/__init__.py" + out="from pkg.x import Base\n\n${tgt}$(sub Base)\n" ;; + reexpmod) printf 'from pkg.x.a import Other as Base\n' > "$d/pkg/x/__init__.py" + out="import pkg.x as q\n\n${tgt}$(sub q.Base)\n" ;; + reexpmid) printf 'from pkg.x.a import Other as Base\n' > "$d/pkg/x/mid.py" + out="from pkg.x.mid import Base\n\n${tgt}$(sub Base)\n" ;; + reexpplain) printf 'from pkg.x.a import Base\n' > "$d/pkg/x/__init__.py" + out="from pkg.x import Base\n\n${tgt}$(sub Base)\n" ;; + reexpmidplain) printf 'from pkg.x.a import Base\n' > "$d/pkg/x/mid.py" + out="from pkg.x.mid import Base\n\n${tgt}$(sub Base)\n" ;; + esac + printf "$out" > "$d/other/b.py" +done +# K21 end nsblock(){ grep -o ']*>.*' "$1"; } nshead(){ grep -o ']*>' "$1"; } nsrows(){ grep -o ']*/>' "$1"; } @@ -1153,6 +1188,29 @@ for fx in sameas:7:3 qualok:7:3 outform:10:6 dotted2:7:3; do v=${fx%%:*}; rest=$ "" done +# K21 (lane ind4b fix 6): a name the package already binds is not its submodule, and a renamed import is not the name it is filed under. +# Every withheld arm is shown + bridged at fix 5's head (4994f5ae); the two controls re-export under the SAME name and stay a way in. +for fx in shadowalias:7 shadowplain:7 reexpalias:7 reexpmod:7 reexpmid:7; do v=${fx%%:*}; ln=${fx##*:} + case "$v" in + shadowalias) why="pkg/x/__init__.py binds a to pkg.y.base2, so from pkg.x import a as q is base2, not the submodule a.py" ;; + shadowplain) why="the same unaliased: from pkg.x import a; Sub(a.Base)" ;; + reexpalias) why="the package reads from pkg.x.a import Other as Base: the Base it exports is Other" ;; + reexpmod) why="import pkg.x as q; Sub(q.Base): the package's Base is the renamed Other" ;; + reexpmid) why="the hop is pkg/x/mid.py, which renames Other to Base" ;; + esac + nsArm "(K21) 2c package binding ($v): $why - withheld" "$TMP/c6_$v" "run,target" \ + '' \ + "" + brArm "(K21b) 2c package binding ($v): no namesake bridge to it either" "$TMP/c6_$v" "run,target" '' +done +for fx in reexpplain:7:3 reexpmidplain:7:3; do v=${fx%%:*}; rest=${fx#*:}; ln=${rest%%:*}; tl=${rest##*:} + nsArm "(K22) 2c re-export under the same name ($v): Base is still pkg/x/a.py's class - a way in" "$TMP/c6_$v" "run,target" \ + '' \ + "" + brArm "(K22b) 2c re-export under the same name ($v): the bridge crosses to the override" "$TMP/c6_$v" "run,target" \ + "" +done + echo "(M) MCP path_between carries the same clause" # Two MCP legend postures (lean-answers): the unchanged assertions run on --mcp-legend=inline, the pre-posture spelling # where every answer carries its root facts on ; a TWIN runs the default session posture, whose answers after the diff --git a/test/pyimportprecisecheck.sh b/test/pyimportprecisecheck.sh index 61e3907d8..74341f7ee 100755 --- a/test/pyimportprecisecheck.sh +++ b/test/pyimportprecisecheck.sh @@ -277,9 +277,13 @@ want = { 'builtin_call.py': ([], []), 'importlib_call.py': ([include('importlib')], [bind('importlib', 'importlib', 'module')]), 'dotted.py': ([include('a.b as c')], [bind('c', 'a.b', 'module')]), + # HISTORY (lane ind4b, fix 5 -> fix 6): the Import RawBind of an ALIASED from-import used to carry importedName '' (no marker); fix 5 + # records the member's own name for it, 'from:a' (kPythonFromMemberMark + a), so `from . import a as b` + `b.Base` can be read as the + # submodule a. The change is on purpose, not erosion: the pin still asserts the exact value (a wrong or missing marker fails, see the + # wrong_marker control below), and only a plain `from m import x` or a module-form import keeps ''/'module'. # The receiver-evidence pass also writes this NameAlias. Pin all its fields and count; # its position relative to the Import bind is an implementation detail of the two passes. - 'relative.py': ([include('.')], [('b', 'a', '', 14, py, name_alias, 0, (0, 0)), bind('b', '.', '')]), + 'relative.py': ([include('.')], [('b', 'a', '', 14, py, name_alias, 0, (0, 0)), bind('b', '.', 'from:a')]), } def check(actual): assert actual.keys() == want.keys() and all( @@ -317,7 +321,7 @@ for path in ('dotted.py', 'relative.py'): else: raise AssertionError((path, channel, 'missing fact escaped the alias assertion')) print(' PASS #382: 16 corrupt-fact controls rejected by the exact Include/RawBind predicate') PY -then ok '#382: future/dynamic negatives; c -> a.b with module marker; relative Include . and b -> . without marker' +then ok '#382: future/dynamic negatives; c -> a.b with module marker; relative Include . and b -> . with the member marker from:a' else no '#382: negative/alias extraction or assertion controls failed' fi From c6049866c7a53850b91d218997b7a556effa68fd Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I <210578314+joyful-ii-V-I@users.noreply.github.com> Date: Fri, 23 Oct 2026 17:11:40 -0400 Subject: [PATCH 51/77] --lego Rust: only a path = under the root puts a dependency key in the tree; a no-path package rename, an unreadable inline table and a two-table key are undecided A renamed dependency with no path = (fc = { package = "foo-core", version = "1" }) was read as in the tree whenever the package name is an in-tree package, so a registry crate's implementor was listed. Class sweep: the three places that put a key in the tree without a path (the no-path rename, a multi-line inline table, the per-manifest settle across dependency tables) now floor. Gate: legotransitivecheck XP arms XpNpBase, XpMlBase, XpTtBase. Co-Authored-By: Claude Sonnet 5.5 --- CHANGELOG.md | 6 ++-- src/graph.h | 35 +++++++++++++------ test/legotransitivecheck.sh | 8 +++++ .../xp/rsmultiline/Cargo.toml | 2 ++ .../xp/rsmultiline/crates/mlcore/Cargo.toml | 2 ++ .../xp/rsmultiline/crates/mlcore/src/lib.rs | 1 + .../xp/rsmultiline/crates/mlother/Cargo.toml | 7 ++++ .../xp/rsmultiline/crates/mlother/src/lib.rs | 1 + .../rsmultiline/crates/mlother/src/mlreg.rs | 3 ++ test/legotransitivefix/xp/rsnopath/Cargo.toml | 2 ++ .../xp/rsnopath/crates/npapp/Cargo.toml | 4 +++ .../xp/rsnopath/crates/npapp/src/lib.rs | 1 + .../xp/rsnopath/crates/npapp/src/npuser.rs | 3 ++ .../xp/rsnopath/crates/npcore/Cargo.toml | 2 ++ .../xp/rsnopath/crates/npcore/src/lib.rs | 1 + .../xp/rsnopath/crates/npentry/Cargo.toml | 5 +++ .../xp/rsnopath/crates/npentry/src/lib.rs | 1 + .../rsnopath/crates/npentry/src/npentryreg.rs | 3 ++ .../xp/rsnopath/crates/npother/Cargo.toml | 4 +++ .../xp/rsnopath/crates/npother/src/lib.rs | 1 + .../xp/rsnopath/crates/npother/src/npreg.rs | 3 ++ .../xp/rstwotable/Cargo.toml | 2 ++ .../xp/rstwotable/crates/ttcore/Cargo.toml | 2 ++ .../xp/rstwotable/crates/ttcore/src/lib.rs | 1 + .../xp/rstwotable/crates/ttother/Cargo.toml | 6 ++++ .../xp/rstwotable/crates/ttother/src/lib.rs | 1 + .../xp/rstwotable/crates/ttother/src/ttreg.rs | 3 ++ 27 files changed, 97 insertions(+), 13 deletions(-) create mode 100644 test/legotransitivefix/xp/rsmultiline/Cargo.toml create mode 100644 test/legotransitivefix/xp/rsmultiline/crates/mlcore/Cargo.toml create mode 100644 test/legotransitivefix/xp/rsmultiline/crates/mlcore/src/lib.rs create mode 100644 test/legotransitivefix/xp/rsmultiline/crates/mlother/Cargo.toml create mode 100644 test/legotransitivefix/xp/rsmultiline/crates/mlother/src/lib.rs create mode 100644 test/legotransitivefix/xp/rsmultiline/crates/mlother/src/mlreg.rs create mode 100644 test/legotransitivefix/xp/rsnopath/Cargo.toml create mode 100644 test/legotransitivefix/xp/rsnopath/crates/npapp/Cargo.toml create mode 100644 test/legotransitivefix/xp/rsnopath/crates/npapp/src/lib.rs create mode 100644 test/legotransitivefix/xp/rsnopath/crates/npapp/src/npuser.rs create mode 100644 test/legotransitivefix/xp/rsnopath/crates/npcore/Cargo.toml create mode 100644 test/legotransitivefix/xp/rsnopath/crates/npcore/src/lib.rs create mode 100644 test/legotransitivefix/xp/rsnopath/crates/npentry/Cargo.toml create mode 100644 test/legotransitivefix/xp/rsnopath/crates/npentry/src/lib.rs create mode 100644 test/legotransitivefix/xp/rsnopath/crates/npentry/src/npentryreg.rs create mode 100644 test/legotransitivefix/xp/rsnopath/crates/npother/Cargo.toml create mode 100644 test/legotransitivefix/xp/rsnopath/crates/npother/src/lib.rs create mode 100644 test/legotransitivefix/xp/rsnopath/crates/npother/src/npreg.rs create mode 100644 test/legotransitivefix/xp/rstwotable/Cargo.toml create mode 100644 test/legotransitivefix/xp/rstwotable/crates/ttcore/Cargo.toml create mode 100644 test/legotransitivefix/xp/rstwotable/crates/ttcore/src/lib.rs create mode 100644 test/legotransitivefix/xp/rstwotable/crates/ttother/Cargo.toml create mode 100644 test/legotransitivefix/xp/rstwotable/crates/ttother/src/lib.rs create mode 100644 test/legotransitivefix/xp/rstwotable/crates/ttother/src/ttreg.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index de471f0b7..bb7f4a844 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,7 +26,7 @@ not published here — see `docs/EVALS.md` for the instruments behind the headli included; the key and its `package =` name; a `path =` crate or a key `[patch]`/`[replace]` names is not one), also through an alias the file itself spells (`use serde as sd;`, `use std::io::{Write as _};` moves no other head). A head that is a crate of the tree keeps its row: a Cargo `[package]`/`[lib]` name from its tests/, examples/ and main.rs, a workspace crate - spelled with `_` for its `-` directory, a dependency key with `path =` under the indexed root or an in-tree `package =` + spelled with `_` for its `-` directory, a dependency key with `path =` under the indexed root (only a path puts a key in the tree) (`core2 = { package = "foo-core", path = "../core" }`, also a `[dependencies.core2]` table or a dotted key), a leading `::`, an `extern crate foo_core as fc;` or `use foo_core as fc;` the file spells. EVERY other head — an unknown crate, an alias or re-export this scan did not see (`pub extern crate foo_core as fc;` in lib.rs, `use fc::T` from another file), a rename only @@ -35,7 +35,9 @@ not published here — see `docs/EVALS.md` for the instruments behind the headli nothing outside but `std` and its siblings. A key that two members of the tree mean differently (in the tree for one, a registry crate for another, also when it spells a package name of the tree) and a key a `.cargo/config(.toml)` `[patch]` names — read in each manifest's directory and above it, within the indexed root, so a config ABOVE the indexed root is not read — are undecided - too; a Cargo file the run cannot read decides nothing outside. This does NOT + too, as is a renamed key with no `path =` (`fc = { package = "foo-core", version = "1" }`: a registry crate, or a `[patch]`'s), an + inline table that does not close on its line, and a key one manifest writes as a registry crate in one dependency table and as a + `path =` crate in another; a Cargo file the run cannot read decides nothing outside. This does NOT cover a C++ `ext::Base` or `using ext::Base`, a C# `using`, a Swift `import`, a PHP `use`, a JS `require` destructure or a Rust `impl ext::Base for S` with no `use`: those rows are still listed by name. Gate: `test/legotransitivecheck.sh` (XP). - `--quality-delta=A..B` extracts its per-commit archive through stdin (`tar -x -f -`), the same Windows drive-colon fix as #402 (thanks @lennix1337). diff --git a/src/graph.h b/src/graph.h index 8124e4d16..ebbe17fc6 100644 --- a/src/graph.h +++ b/src/graph.h @@ -6182,10 +6182,10 @@ inline bool tomlInlineSets( std::string_view value, std::string_view field, std: // What the dependency tables of one Cargo.toml name, keys as written (a `foo-core` is normalised to `foo_core` by the caller). struct TomlDeps { - std::vector inTree; // keys naming a crate of this tree: a `path =` under the indexed root, or an in-tree `package =` + std::vector inTree; // keys naming a crate of this tree: only a `path =` under the indexed root proves it (never a name match) std::vector declared; // every dependency key, and every `package =` name std::vector pathed; // keys with any `path =` (inside the root or not) and every [patch] / [replace] key: never a registry or git dependency - std::vector registry; // keys a manifest declares with neither: a registry or git dependency (after tomlMergeManifest: of that manifest alone) + std::vector registry; // keys a manifest declares with neither: a registry or git dependency (after tomlMergeManifest: of that table alone) std::vector inherited; // keys a manifest writes `workspace = true`: the [workspace.dependencies] entry says what they are }; @@ -6243,8 +6243,18 @@ inline void tomlNoteDependency( const TomlDepScope& scope, const TomlDepEntry& e { out.pathed.emplace_back( e.name ); } - const bool inTree = e.hasPath ? tomlPathInRoot( scope.dir, e.path ) : !e.package.empty() && scope.inTree.contains( spelled ); - ( inTree ? out.inTree : out.registry ).emplace_back( e.name ); + if( e.hasPath ) + { + ( tomlPathInRoot( scope.dir, e.path ) ? out.inTree : out.registry ).emplace_back( e.name ); // an outside path is also in `pathed` + } + else if( !e.package.empty() && scope.inTree.contains( spelled ) ) + { + out.pathed.emplace_back( e.name ); // no path: Cargo takes the registry's crate of that name (or a [patch]): undecided, never in the tree + } + else + { + out.registry.emplace_back( e.name ); + } if( e.workspace ) { out.inherited.emplace_back( e.name ); @@ -6327,13 +6337,13 @@ inline void tomlDepLine( std::string_view line, const TomlDepCursor& cur, const tomlNoteDependency( scope, tomlDepEntry( key, sub, rest, cur ), out ); if( !cur.inEntry && sub.empty() && rest.find( '{' ) != std::string_view::npos && rest.find( '}' ) == std::string_view::npos ) { - out.inTree.emplace_back( key ); // an inline table over several lines: unreadable, never a registry dependency - out.pathed.emplace_back( key ); + out.pathed.emplace_back( key ); // an inline table over several lines: unreadable, no path proves it: undecided (never in the tree, never a registry crate) } } -// Fold one manifest's keys into the tree's. A key THIS manifest puts in the tree, paths or inherits is no registry key of its (an entry -// table's `version =` line reads as one beside its own `path =` line); what is left is a registry or git dependency of this manifest. +// Fold one dependency TABLE's keys into the tree's. A key THIS table puts in the tree, paths or inherits is no registry key of its (an entry +// table's `version =` line reads as one beside its own `path =` line); what is left is a registry or git dependency of this table. Another +// table of the same manifest that writes the key differently is a second meaning, kept for cargoSettleNames to see. inline void tomlMergeManifest( TomlDeps& one, TomlDeps& all ) { const auto has = []( const std::vector& v, const std::string& k ) { return std::ranges::find( v, k ) != v.end(); }; @@ -6351,9 +6361,10 @@ inline void tomlMergeManifest( TomlDeps& one, TomlDeps& all ) // The dependency KEYS of one Cargo.toml text, sorted into TomlDeps: an entry of a `[dependencies]` / `[dev-dependencies]` / // `[build-dependencies]` / `[workspace.dependencies]` / `[target.*.dependencies]` table (or its own `[dependencies.key]` table, or a dotted -// `key.path =`). A key that carries a `path =` under the root or `package = ""` names a crate of the tree in the -// code (`core2 = { package = "foo-core", path = "../core" }` -> `use core2::T`). An inline table that does not close on its line cannot be -// read, so its key counts as in the tree: that can only keep a row, never decide one. +// `key.path =`). A key that carries a `path =` under the root names a crate of the tree in the code (`core2 = { package = "foo-core", +// path = "../core" }` -> `use core2::T`); nothing else puts a key in the tree: `package = ""` with no path is a +// registry crate (or a [patch]), so its key is undecided. An inline table that does not close on its line cannot be read: its key is +// undecided too. inline void tomlDependencyKeys( std::string_view text, const TomlDepScope& scope, TomlDeps& out ) { TomlDeps one; @@ -6366,6 +6377,8 @@ inline void tomlDependencyKeys( std::string_view text, const TomlDepScope& scope line.remove_prefix( std::min( line.find_first_not_of( " \t\r" ), line.size() ) ); if( line.starts_with( '[' ) ) { + tomlMergeManifest( one, out ); // a table's own keys settle among themselves only + one = TomlDeps(); tomlDepHeader( line, cur, one ); } else if( cur.table || cur.inEntry ) diff --git a/test/legotransitivecheck.sh b/test/legotransitivecheck.sh index 456e1d5e3..92a3217d5 100755 --- a/test/legotransitivecheck.sh +++ b/test/legotransitivecheck.sh @@ -426,6 +426,14 @@ xpfloor XpKeyBase "XpKeyReal XpKeyOutReg" # fix round 5 (review rv-fc2-delta4 C1): the same two meanings when the key is also an in-tree PACKAGE name (`xp-pkg-core` is a path # dependency of one member and a registry crate of another): floor, no row either way xpfloor XpPkgBase "XpPkgReal XpPkgOutReg" +# fix round 6 (review rv-fc2-delta5 D1 and its class sweep: no key is in the tree by a NAME match, only by a `path =` under the root): +# (1) a renamed key with NO `path =` (`xpnp = { package = "xp-np-core", version = "1" }`, or the entry table's own `package =` / `version =` lines) is a +# registry crate even when its package name is an in-tree package: floor, never a listed implementor +# (2) an inline table that does not close on its line cannot be read, so no path proves it: undecided, not in the tree +# (3) ONE manifest that writes a key as a registry crate in [dependencies] and as a path crate in [dev-dependencies] means it two ways: floor +xpfloor XpNpBase "XpNpOutReg XpNpOutEntry" +xpfloor XpMlBase "XpMlOutReg" +xpfloor XpTtBase "XpTtOutReg" xp XpKeyBothBase "" "XpKeyBothA XpKeyBothB" xp XpKeyCtlBase "XpKeyCtlOut" "" if lego XpKeyCtlBase && iface | grep -q 'implementors_floor'; then no "XpKeyCtlBase carries a floor for a key every member declares as a registry crate: $( iface )"; else ok "XpKeyCtlBase: a key registry in every member stays decided (no floor)"; fi diff --git a/test/legotransitivefix/xp/rsmultiline/Cargo.toml b/test/legotransitivefix/xp/rsmultiline/Cargo.toml new file mode 100644 index 000000000..d6b1c4d49 --- /dev/null +++ b/test/legotransitivefix/xp/rsmultiline/Cargo.toml @@ -0,0 +1,2 @@ +[workspace] +members = ["crates/mlcore", "crates/mlother"] diff --git a/test/legotransitivefix/xp/rsmultiline/crates/mlcore/Cargo.toml b/test/legotransitivefix/xp/rsmultiline/crates/mlcore/Cargo.toml new file mode 100644 index 000000000..a67a8b6ef --- /dev/null +++ b/test/legotransitivefix/xp/rsmultiline/crates/mlcore/Cargo.toml @@ -0,0 +1,2 @@ +[package] +name = "xp-ml-core" diff --git a/test/legotransitivefix/xp/rsmultiline/crates/mlcore/src/lib.rs b/test/legotransitivefix/xp/rsmultiline/crates/mlcore/src/lib.rs new file mode 100644 index 000000000..6ed2c9bd8 --- /dev/null +++ b/test/legotransitivefix/xp/rsmultiline/crates/mlcore/src/lib.rs @@ -0,0 +1 @@ +pub trait XpMlBase {} diff --git a/test/legotransitivefix/xp/rsmultiline/crates/mlother/Cargo.toml b/test/legotransitivefix/xp/rsmultiline/crates/mlother/Cargo.toml new file mode 100644 index 000000000..fcee1f628 --- /dev/null +++ b/test/legotransitivefix/xp/rsmultiline/crates/mlother/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "xp-ml-other" +[dependencies] +xpml = { + package = "xp-ml-core", + version = "1" +} diff --git a/test/legotransitivefix/xp/rsmultiline/crates/mlother/src/lib.rs b/test/legotransitivefix/xp/rsmultiline/crates/mlother/src/lib.rs new file mode 100644 index 000000000..722101c01 --- /dev/null +++ b/test/legotransitivefix/xp/rsmultiline/crates/mlother/src/lib.rs @@ -0,0 +1 @@ +mod mlreg; diff --git a/test/legotransitivefix/xp/rsmultiline/crates/mlother/src/mlreg.rs b/test/legotransitivefix/xp/rsmultiline/crates/mlother/src/mlreg.rs new file mode 100644 index 000000000..ab71eabd0 --- /dev/null +++ b/test/legotransitivefix/xp/rsmultiline/crates/mlother/src/mlreg.rs @@ -0,0 +1,3 @@ +use xpml::XpMlBase; +pub struct XpMlOutReg; +impl XpMlBase for XpMlOutReg {} diff --git a/test/legotransitivefix/xp/rsnopath/Cargo.toml b/test/legotransitivefix/xp/rsnopath/Cargo.toml new file mode 100644 index 000000000..f4a17f1ba --- /dev/null +++ b/test/legotransitivefix/xp/rsnopath/Cargo.toml @@ -0,0 +1,2 @@ +[workspace] +members = ["crates/npcore", "crates/npapp", "crates/npother", "crates/npentry"] diff --git a/test/legotransitivefix/xp/rsnopath/crates/npapp/Cargo.toml b/test/legotransitivefix/xp/rsnopath/crates/npapp/Cargo.toml new file mode 100644 index 000000000..ded60ba90 --- /dev/null +++ b/test/legotransitivefix/xp/rsnopath/crates/npapp/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "xp-np-app" +[dependencies] +xp-np-core = { path = "../npcore" } diff --git a/test/legotransitivefix/xp/rsnopath/crates/npapp/src/lib.rs b/test/legotransitivefix/xp/rsnopath/crates/npapp/src/lib.rs new file mode 100644 index 000000000..55db44e18 --- /dev/null +++ b/test/legotransitivefix/xp/rsnopath/crates/npapp/src/lib.rs @@ -0,0 +1 @@ +mod npuser; diff --git a/test/legotransitivefix/xp/rsnopath/crates/npapp/src/npuser.rs b/test/legotransitivefix/xp/rsnopath/crates/npapp/src/npuser.rs new file mode 100644 index 000000000..f69c37b87 --- /dev/null +++ b/test/legotransitivefix/xp/rsnopath/crates/npapp/src/npuser.rs @@ -0,0 +1,3 @@ +use xp_np_core::XpNpBase; +pub struct XpNpReal; +impl XpNpBase for XpNpReal {} diff --git a/test/legotransitivefix/xp/rsnopath/crates/npcore/Cargo.toml b/test/legotransitivefix/xp/rsnopath/crates/npcore/Cargo.toml new file mode 100644 index 000000000..374d18cb5 --- /dev/null +++ b/test/legotransitivefix/xp/rsnopath/crates/npcore/Cargo.toml @@ -0,0 +1,2 @@ +[package] +name = "xp-np-core" diff --git a/test/legotransitivefix/xp/rsnopath/crates/npcore/src/lib.rs b/test/legotransitivefix/xp/rsnopath/crates/npcore/src/lib.rs new file mode 100644 index 000000000..6fdcceb2e --- /dev/null +++ b/test/legotransitivefix/xp/rsnopath/crates/npcore/src/lib.rs @@ -0,0 +1 @@ +pub trait XpNpBase {} diff --git a/test/legotransitivefix/xp/rsnopath/crates/npentry/Cargo.toml b/test/legotransitivefix/xp/rsnopath/crates/npentry/Cargo.toml new file mode 100644 index 000000000..98270db6f --- /dev/null +++ b/test/legotransitivefix/xp/rsnopath/crates/npentry/Cargo.toml @@ -0,0 +1,5 @@ +[package] +name = "xp-np-entry" +[dependencies.xpnpe] +package = "xp-np-core" +version = "1" diff --git a/test/legotransitivefix/xp/rsnopath/crates/npentry/src/lib.rs b/test/legotransitivefix/xp/rsnopath/crates/npentry/src/lib.rs new file mode 100644 index 000000000..e426430db --- /dev/null +++ b/test/legotransitivefix/xp/rsnopath/crates/npentry/src/lib.rs @@ -0,0 +1 @@ +mod npentryreg; diff --git a/test/legotransitivefix/xp/rsnopath/crates/npentry/src/npentryreg.rs b/test/legotransitivefix/xp/rsnopath/crates/npentry/src/npentryreg.rs new file mode 100644 index 000000000..0b3f129e8 --- /dev/null +++ b/test/legotransitivefix/xp/rsnopath/crates/npentry/src/npentryreg.rs @@ -0,0 +1,3 @@ +use xpnpe::XpNpBase; +pub struct XpNpOutEntry; +impl XpNpBase for XpNpOutEntry {} diff --git a/test/legotransitivefix/xp/rsnopath/crates/npother/Cargo.toml b/test/legotransitivefix/xp/rsnopath/crates/npother/Cargo.toml new file mode 100644 index 000000000..ede43cec8 --- /dev/null +++ b/test/legotransitivefix/xp/rsnopath/crates/npother/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "xp-np-other" +[dependencies] +xpnp = { package = "xp-np-core", version = "1" } diff --git a/test/legotransitivefix/xp/rsnopath/crates/npother/src/lib.rs b/test/legotransitivefix/xp/rsnopath/crates/npother/src/lib.rs new file mode 100644 index 000000000..e0b56bda8 --- /dev/null +++ b/test/legotransitivefix/xp/rsnopath/crates/npother/src/lib.rs @@ -0,0 +1 @@ +mod npreg; diff --git a/test/legotransitivefix/xp/rsnopath/crates/npother/src/npreg.rs b/test/legotransitivefix/xp/rsnopath/crates/npother/src/npreg.rs new file mode 100644 index 000000000..c6a61e7a3 --- /dev/null +++ b/test/legotransitivefix/xp/rsnopath/crates/npother/src/npreg.rs @@ -0,0 +1,3 @@ +use xpnp::XpNpBase; +pub struct XpNpOutReg; +impl XpNpBase for XpNpOutReg {} diff --git a/test/legotransitivefix/xp/rstwotable/Cargo.toml b/test/legotransitivefix/xp/rstwotable/Cargo.toml new file mode 100644 index 000000000..802defc24 --- /dev/null +++ b/test/legotransitivefix/xp/rstwotable/Cargo.toml @@ -0,0 +1,2 @@ +[workspace] +members = ["crates/ttcore", "crates/ttother"] diff --git a/test/legotransitivefix/xp/rstwotable/crates/ttcore/Cargo.toml b/test/legotransitivefix/xp/rstwotable/crates/ttcore/Cargo.toml new file mode 100644 index 000000000..7c0ae8afe --- /dev/null +++ b/test/legotransitivefix/xp/rstwotable/crates/ttcore/Cargo.toml @@ -0,0 +1,2 @@ +[package] +name = "xp-tt-core" diff --git a/test/legotransitivefix/xp/rstwotable/crates/ttcore/src/lib.rs b/test/legotransitivefix/xp/rstwotable/crates/ttcore/src/lib.rs new file mode 100644 index 000000000..17f03968f --- /dev/null +++ b/test/legotransitivefix/xp/rstwotable/crates/ttcore/src/lib.rs @@ -0,0 +1 @@ +pub trait XpTtBase {} diff --git a/test/legotransitivefix/xp/rstwotable/crates/ttother/Cargo.toml b/test/legotransitivefix/xp/rstwotable/crates/ttother/Cargo.toml new file mode 100644 index 000000000..eb30b1449 --- /dev/null +++ b/test/legotransitivefix/xp/rstwotable/crates/ttother/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "xp-tt-other" +[dependencies] +xp-tt-core = "1" +[dev-dependencies] +xp-tt-core = { path = "../ttcore" } diff --git a/test/legotransitivefix/xp/rstwotable/crates/ttother/src/lib.rs b/test/legotransitivefix/xp/rstwotable/crates/ttother/src/lib.rs new file mode 100644 index 000000000..801b56e5e --- /dev/null +++ b/test/legotransitivefix/xp/rstwotable/crates/ttother/src/lib.rs @@ -0,0 +1 @@ +mod ttreg; diff --git a/test/legotransitivefix/xp/rstwotable/crates/ttother/src/ttreg.rs b/test/legotransitivefix/xp/rstwotable/crates/ttother/src/ttreg.rs new file mode 100644 index 000000000..3ea29bce2 --- /dev/null +++ b/test/legotransitivefix/xp/rstwotable/crates/ttother/src/ttreg.rs @@ -0,0 +1,3 @@ +use xp_tt_core::XpTtBase; +pub struct XpTtOutReg; +impl XpTtBase for XpTtOutReg {} From 70b53038c19fee5d93e5155482d1840c4da2fa4d Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I <210578314+joyful-ii-V-I@users.noreply.github.com> Date: Fri, 23 Oct 2026 22:26:17 -0400 Subject: [PATCH 52/77] MCP quality_delta show_stale + edit_check verdict fold (card #9 MCP-FOLD) quality_delta: a regression-free answer with stale acks folds its sa rows to stale_by_kind/stale_next (show_stale=true restores the full document) when the fold pays (foldPays, the CLI margin). edit_check: the CLI's verdict-only fold (clean contract at HEAD, nothing flagged) with rows_next naming the limit. stale_by_kind is one shared helper (quality::staleByKindValue) for both surfaces. Co-Authored-By: Claude Opus 5.5 --- src/editcheck.h | 3 ++- src/mcp.h | 5 ++++- src/mcprefusal.h | 4 +++- src/mcpverbs.h | 28 ++++++++++++++++++++++++---- src/quality.h | 21 +++++++++++++++++++++ src/verbs_quality.h | 14 +------------- 6 files changed, 55 insertions(+), 20 deletions(-) diff --git a/src/editcheck.h b/src/editcheck.h index 3dfd1bb04..b31b373e3 100644 --- a/src/editcheck.h +++ b/src/editcheck.h @@ -1280,7 +1280,8 @@ inline bool editCheckShowsModuleScope( const IngestResult& ing, const std::vecto // `page` (2026-09-10, folded into one value by lane lean-trio): --limit/--offset, and — idea #11 — the CLI's clean-answer // posture: `verdictOnly` makes an unchanged contract with nothing flagged print the verdict and the call that lists the // callers instead of 40 caller rows, under every legend posture; `selector` is the CLI's own spelling of SYM, replayed in -// rows_next=. The MCP verb and the preview keep the full document (verdictOnly false). +// rows_next=. The MCP edit_check verb takes the same fold (lane lean2b: its limit= argument is the --limit= rows_next= names); +// the preview keeps the full document (preview=true never folds). struct EditCheckPage { int limit = 0; diff --git a/src/mcp.h b/src/mcp.h index dd58213ac..591d59ed9 100644 --- a/src/mcp.h +++ b/src/mcp.h @@ -1503,6 +1503,9 @@ inline McpDispatchResult dispatchMcpLine( const std::string& line, int topK, boo // whereis: full_text (the CLI --whereis-full-text twin) prints every t= line whole and lifts nothing else. A BOOLEAN, // read here with the other typed arguments so a wrong-shaped value refuses through the shared gate (checklist 26). const bool whereisFullText = boolArg( "full_text" ).value; + // quality_delta: show_stale (the CLI --show-stale twin, lane lean2b) lists the stale-ack "sa" rows a regression-free + // answer folds to stale_by_kind/stale_next. A BOOLEAN read here, before the shared shape gate (checklist 26). + const bool qdShowStale = boolArg( "show_stale" ).value; // grep: code|any (the CLI --grep-in twin). The `listing` sibling (CodeRabbit 5468003465, checklist 26): read inside the // grep arm, a non-string `in:5` decoded to "" after the shape gate had run, its refusal was dropped, and the arm then // answered the default tier as if nothing had been sent. test/greptiercheck.sh (9e). @@ -2580,7 +2583,7 @@ inline McpDispatchResult dispatchMcpLine( const std::string& line, int topK, boo } else if( name == "quality_delta" && !path.empty() ) { - const auto [ j, qerr ] = qualityDeltaJson( path ); + const auto [ j, qerr ] = qualityDeltaJson( path, qdShowStale ); resp = j.empty() ? errResultMsg( -32602, qerr.empty() ? std::string( "quality-delta unavailable" ) : qerr ) : textResult( j ); } else if( name == "quality_baseline" && !path.empty() ) diff --git a/src/mcprefusal.h b/src/mcprefusal.h index 82c6c0127..a913443f3 100644 --- a/src/mcprefusal.h +++ b/src/mcprefusal.h @@ -374,6 +374,8 @@ inline constexpr McpValueSpec kMcpValueFields[] = { { "no_route", "a BOOLEAN: true forces plain subtoken+body BM25 (omit it for the default router)", "no_route=true", "boolean" }, // whereis-lean (review C1): the CLI --whereis-full-text twin, the replay a names. Terse: it rides tools/list. { "full_text", "a BOOLEAN: true prints every t= line whole", "full_text=true", "boolean" }, + // lane lean2b: the CLI --show-stale twin, the replay quality_delta's stale_next names. Terse: it rides tools/list. + { "show_stale", "a BOOLEAN: true lists the stale-ack sa rows a regression-free answer folds", "show_stale=true", "boolean" }, // ── the ENVELOPE, outside `params` (§B6 M6/M7) ── // These four were read through the bare findString/findObject path, which collapses "absent" onto // "present but not the shape I read" — so `"method":5` became `-32700 "parse error"` (a JSON that parsed @@ -1047,7 +1049,7 @@ inline constexpr McpVerbFields kMcpVerbFields[] = { { "batch", "path queries legend" }, // ── flagship-reflex verbs ── { "exemplar", "path paths kind task legend" }, - { "quality_delta", "path" }, + { "quality_delta", "path show_stale" }, { "quality_baseline", "path", McpVerbFields::Effect::Writes }, { "impact", "path paths symbol limit offset legend" }, // LB-G (r10 GitNexus round): limit/offset are DECLARED here because the verb now HONORS them diff --git a/src/mcpverbs.h b/src/mcpverbs.h index 45d7ccb5b..26347d153 100644 --- a/src/mcpverbs.h +++ b/src/mcpverbs.h @@ -4518,7 +4518,13 @@ inline QualityDeltaOutcome computeQualityDelta( const std::string& root ) // facet names, displaySym's root-relative spelling, and the CLI's own was/now omission rule for the // zero-magnitude kinds). `regressions_count` is gone rather than kept as an alias: two names for one number // is how the next consumer picks the wrong one. Gate: test/mcpclidiffcheck.sh diffs the two key sets. -inline std::pair qualityDeltaJson( const std::string& root ) +// +// Lane lean2b (ideas-32b card #9, owner bar LOSSLESS): `showStale` is the MCP `show_stale` argument, the CLI --show-stale +// twin. Absent/false, a regression-free answer with stale acks folds its "sa" rows to "stale_by_kind" (stale split by ack +// kind, quality::staleByKindValue — the CLI's own counts) and "stale_next" (the argument that prints them), but only when +// that PAYS (compactlegend.h foldPays, the CLI's margin). Every count (stale, acked, regressions, gating, ...) stays on the +// root either way; show_stale=true returns the full document byte for byte as it read before this fold existed. +inline std::pair qualityDeltaJson( const std::string& root, bool showStale = false ) { const QualityDeltaOutcome oc = computeQualityDelta( root ); if( !oc.ok ) { return { std::string(), oc.errMsg }; } @@ -4617,9 +4623,20 @@ inline std::pair qualityDeltaJson( const std::string& } out += "}"; } - out += "],"; // L2 — "sa":[...], same taxonomy the CLI's rows carry (shared builder, quality::staleAcksJsonArray) - out += rw::quality::staleAcksJsonArray( oc.staleAcks ); + out += "],"; + // the regression-free fold (lane lean2b): "r" is empty, so the stale rows are the only thing left out — counted per kind + std::string folded; + if( !showStale && oc.regs.empty() && !oc.staleAcks.empty() ) + { + folded = out + "\"stale_by_kind\":\"" + mcpdetail::jsonEscape( rw::quality::staleByKindValue( oc.staleAcks ) ) + + "\",\"stale_next\":\"" + mcpdetail::jsonEscape( rw::nextFlag( "show_stale=", "true" ) ) + "\"}"; + } + out += rw::quality::staleAcksJsonArray( oc.staleAcks ); // L2 — "sa":[...], same taxonomy the CLI's rows carry out += "}"; + if( !folded.empty() && rw::foldPays( folded, out ) ) + { + return { std::move( folded ), std::string() }; + } return { std::move( out ), std::string() }; } @@ -4948,7 +4965,10 @@ inline EditCheckReply editCheckText( const std::string& root, const std::string& } return EditCheckReply{ editCheckBundleText( ing, g, root, kDefaultMaxFileBytes, {}, groups[0].lowestNode, - /*ni=*/nullptr, /*preview=*/false, EditCheckPage{ pg.limit, pg.offset }, unprovenDefs ), {} }; + /*ni=*/nullptr, /*preview=*/false, + // lane lean2b (card #9): the CLI's verdict-only fold — a clean contract at HEAD with nothing + // flagged answers the verdict + rows_next= (the limit= that lists the callers), callers= kept + EditCheckPage{ pg.limit, pg.offset, /*verdictOnly=*/true, symbol }, unprovenDefs ), {} }; } // ─── `slice` verb (lane/tc-sliceat): the ARISE def-use slice over MCP, mirroring the CLI --slice ──────── diff --git a/src/quality.h b/src/quality.h index d6255f526..9e18c35e1 100644 --- a/src/quality.h +++ b/src/quality.h @@ -8798,6 +8798,27 @@ inline std::string staleAcksXml( const std::vector& staleAcks, EscapeF return out; } +// idea #11 (lane lean-trio), shared by lane lean2b: stale= split by ack KIND as `kind:N,...`, in first-appearance order of the +// ledger's own row order — the stale_by_kind= value of the folded CLI --quality-delta AND of the MCP quality_delta twin, so the +// two surfaces cannot count differently. The KIND drops the ack's :new-symbol/:preexisting facet; the rows (--show-stale / +// show_stale=true) carry it. Unescaped: each caller escapes for its own dialect. +inline std::string staleByKindValue( const std::vector& staleAcks ) +{ + std::vector> byKind; + for( const StaleAck& a : staleAcks ) + { + const std::string kind = a.kind.substr( 0, a.kind.find( ':' ) ); + const auto it = std::find_if( byKind.begin(), byKind.end(), [ & ]( const auto& kv ) { return kv.first == kind; } ); + if( it == byKind.end() ) { byKind.emplace_back( kind, 1 ); } else { ++it->second; } + } + std::string kinds; + for( const auto& [ kind, n ] : byKind ) + { + kinds += ( kinds.empty() ? "" : "," ) + kind + ":" + std::to_string( n ); + } + return kinds; +} + inline std::string staleAcksJsonArray( const std::vector& staleAcks ) // returns `"sa":[...]`, ready to splice in { std::string out = "\"sa\":["; diff --git a/src/verbs_quality.h b/src/verbs_quality.h index 0e85e5efe..0976e7ec0 100644 --- a/src/verbs_quality.h +++ b/src/verbs_quality.h @@ -2272,19 +2272,7 @@ std::optional runQualityDelta( const MainDispatch& d ) std::string staleCollapseAttrs; if( collapseStale ) { - std::vector> byKind; // first-appearance order of the ledger's own row order - for( const quality::StaleAck& a : staleAcks ) - { - // the KIND, without the ack's :new-symbol/:preexisting facet — the rows (stale_next=) carry the facet - const std::string kind = a.kind.substr( 0, a.kind.find( ':' ) ); - const auto it = std::find_if( byKind.begin(), byKind.end(), [ & ]( const auto& kv ) { return kv.first == kind; } ); - if( it == byKind.end() ) { byKind.emplace_back( kind, 1 ); } else { ++it->second; } - } - std::string kinds; - for( const auto& [ kind, n ] : byKind ) - { - kinds += ( kinds.empty() ? "" : "," ) + kind + ":" + std::to_string( n ); - } + const std::string kinds = quality::staleByKindValue( staleAcks ); // shared with the MCP twin (lane lean2b) std::string replay = cfg.qualityDeltaRange.empty() ? std::string( "--quality-delta" ) : rw::nextFlag( "--quality-delta=", cfg.qualityDeltaRange ); if( scope.active() ) { From 042f4fc1a55478d332e8e0d40db62c3d222ccfe9 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I <210578314+joyful-ii-V-I@users.noreply.github.com> Date: Fri, 23 Oct 2026 23:24:24 -0400 Subject: [PATCH 53/77] gates: MCP fold arms (staleackcheck 2m, editcheckcheck 1v-m); mcpattrparitycheck RENAME -> count parity; changelog fragment Co-Authored-By: Claude Opus 5.5 --- changelog.d/mcp-fold.md | 12 ++++++++++++ test/editcheckcheck.sh | 21 ++++++++++++++++++++ test/mcpattrparitycheck.sh | 40 ++++++++++++++++++++++++++++++++++---- test/staleackcheck.sh | 39 +++++++++++++++++++++++++++++++++++++ 4 files changed, 108 insertions(+), 4 deletions(-) create mode 100644 changelog.d/mcp-fold.md diff --git a/changelog.d/mcp-fold.md b/changelog.d/mcp-fold.md new file mode 100644 index 000000000..c0e8d30ab --- /dev/null +++ b/changelog.d/mcp-fold.md @@ -0,0 +1,12 @@ +### Changed — MCP `quality_delta` and `edit_check` fold their bookkeeping rows like the CLI (`show_stale`) + +- MCP `quality_delta`: a regression-free answer with stale acks now drops its `sa` rows and carries `stale_by_kind` (the + stale count split by ack kind, the same string the CLI prints) and `stale_next` (`show_stale=true`), when that saves at + least a quarter of the bytes. Every count stays on the answer. The new boolean argument `show_stale=true` (the CLI + `--show-stale` twin) returns the full document unchanged. Measured on ripwire's own tree: 15,015 B to 580 B; on the eight + other sweep repos the answer has no stale acks and is byte-identical. +- MCP `edit_check`: an unchanged contract at git HEAD with no flagged caller answers the verdict and `rows_next=` (the + `limit` that lists the callers), as `--edit-check` already does; `callers=` keeps the count. Median 1.72x smaller on the + repos where it applies (1.62x to 3.46x). A dirty tree, a flagged caller, `new_body` previews and an explicit `limit` or + `offset` still list every row. +- It does NOT drop a regression row, a flagged caller or any count: the folded rows are one argument away. diff --git a/test/editcheckcheck.sh b/test/editcheckcheck.sh index 49532a696..c53a80bf1 100755 --- a/test/editcheckcheck.sh +++ b/test/editcheckcheck.sh @@ -124,6 +124,27 @@ F_TSL="$( fec blend --limit=16 )" [ "$( rows "$F_TSL" | wc -l | tr -d ' ' )" = 16 ] && rows "$F_TSL" | grep -qF '' \ && ok "(1v-b) the rows_next= call lists all 16 callers, main.ts:go among them" \ || { no "(1v-b) the rows_next= call does not list the 16 callers"; printf '%s\n' "$F_TSL"; } +# (1v-m) lane lean2b (card #9 MCP-FOLD): the MCP edit_check verb takes the SAME verdict fold. Default: the verdict alone with +# rows_next= naming --limit=16 (= the verb's limit argument); limit=16: the 16 rows, byte-identical to the CLI --limit=16 +# rows; 3 callers: the fold does not pay and the rows ride. RED on ffbec708: the MCP default lists all 16 rows. +mec(){ printf '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"edit_check","arguments":{"path":"%s","symbol":"%s"%s}}}\n' "$FOLD" "$1" "$2" \ + | "$BIN" --mcp --no-cache 2>/dev/null \ + | python3 -c 'import sys,json +for line in sys.stdin: + try: d=json.loads(line) + except Exception: continue + c=(d.get("result") or {}).get("content") + if c: print(c[0].get("text","")); break'; } +if command -v python3 >/dev/null 2>&1; then + M_TS="$( mec blend '' )"; M_TSL="$( mec blend ',"limit":16' )" + fold_verdict "(1v-m) MCP edit_check, clean tree, 16 callers: the verdict alone, rows_next= names --limit=16" "$M_TS" 16 blend + [ "$( rows "$M_TSL" | wc -l | tr -d ' ' )" = 16 ] && [ "$( rows "$M_TSL" )" = "$( rows "$F_TSL" )" ] \ + && ok "(1v-m) MCP limit=16 lists the 16 caller rows, identical to the CLI --limit=16 rows" \ + || { no "(1v-m) MCP limit=16 does not reproduce the CLI rows"; printf '%s\n' "$M_TSL"; } + fold_rows "(1v-m) MCP edit_check, 3 callers: the fold does not pay, m1 listed" "$( mec mix '' )" ' rows to stale_by_kind= and - # stale_next= (the --show-stale call); the twin LISTS those rows as "sa", so the two facts survive as the rows themselves. - "stale_by_kind": { "sa" }, - "stale_next": { "sa" }, + # quality_delta: lane lean-trio's RENAME stale_by_kind/stale_next -> "sa" is GONE (lane lean2b, card #9 MCP-FOLD). The twin + # now folds like the CLI and carries both keys BY NAME, so root parity is plain name parity again; the COUNT twin below + # asserts the fold lost nothing (stale on both surfaces, stale_by_kind summing to it, show_stale=true listing the rows). } def report( label, cliNames, twinNames, lens ): @@ -242,6 +241,39 @@ else: report( "quality_delta root", xmlRootAttrs( qdXml, "quality-delta" ) or set(), jsonTopKeys( qdMcp ), declaredLens( qdMcp ) ) +# lean2b (card #9) COUNT PARITY — the twin of the RENAME removal above. Name parity alone would pass a fold that dropped a +# row or miscounted a kind, so the values are compared where they are a closed count: CLI stale= == MCP stale; when either +# surface folds, BOTH fold, and stale_by_kind sums to stale on each and is the SAME string; MCP show_stale=true lists exactly +# `stale` "sa" rows and carries neither fold key; and the folded and show_stale=true documents agree on every other key. +def kindSum( v ): + try: return sum( int( kv.rsplit( ":", 1 )[ 1 ] ) for kv in v.split( "," ) if kv ) + except Exception: return -1 +qdFull = mcp( "quality_delta", { "path": ROOT, "show_stale": True } ) +try: + jDef = json.loads( qdMcp ) + jFull = json.loads( qdFull ) +except Exception: + jDef = jFull = None +check( jDef is not None and jFull is not None, "COUNT: MCP quality_delta default and show_stale=true both parse as JSON" ) +if jDef is not None and jFull is not None: + cliRoot = stripComments( qdXml ) + mStale = re.search( r']*\bstale="(\d+)"', cliRoot ) + mKinds = re.search( r']*\bstale_by_kind="([^"]*)"', cliRoot ) + cliStale = int( mStale.group( 1 ) ) if mStale else None + check( cliStale is not None and cliStale == jDef.get( "stale" ) == jFull.get( "stale" ), + "COUNT: stale= agrees on the CLI (%r), MCP default (%r) and show_stale=true (%r)" % ( cliStale, jDef.get( "stale" ), jFull.get( "stale" ) ) ) + mcpFolds = "stale_by_kind" in jDef + check( mcpFolds == bool( mKinds ), "COUNT: the MCP default folds exactly when the CLI does (cli=%s mcp=%s)" % ( bool( mKinds ), mcpFolds ) ) + if mcpFolds: + check( "sa" not in jDef and jDef.get( "stale_next" ) == "show_stale=true", + "COUNT: a folded twin drops sa and names show_stale=true in stale_next (got %r)" % ( jDef.get( "stale_next" ), ) ) + check( kindSum( jDef[ "stale_by_kind" ] ) == jDef.get( "stale" ), "COUNT: MCP stale_by_kind sums to stale (%r)" % ( jDef[ "stale_by_kind" ], ) ) + check( bool( mKinds ) and mKinds.group( 1 ) == jDef[ "stale_by_kind" ], "COUNT: CLI and MCP stale_by_kind are the same string" ) + check( "stale_by_kind" not in jFull and "stale_next" not in jFull and len( jFull.get( "sa", [] ) ) == jFull.get( "stale" ), + "COUNT: show_stale=true lists exactly stale (%r) sa rows (%r) and no fold key" % ( jFull.get( "stale" ), len( jFull.get( "sa", [] ) ) ) ) + rest = lambda d: { k: v for k, v in d.items() if k not in ( "sa", "stale_by_kind", "stale_next" ) } + check( rest( jDef ) == rest( jFull ), "COUNT: default and show_stale=true agree on every key outside the fold" ) + # A2 (found-items 2026-09-17): VALUE parity for the "sidecar present but unreadable" baseline marker. # attrparity above is deliberately NAME-only (a git stamp or a root path legitimately differs by value), # but this one string is a closed, documented vocabulary (verbs_quality.h's own legend) and CLI/MCP must diff --git a/test/staleackcheck.sh b/test/staleackcheck.sh index 3e85bba7c..9935cd637 100755 --- a/test/staleackcheck.sh +++ b/test/staleackcheck.sh @@ -124,6 +124,45 @@ fi sed 's//dev/null 2>&1; then + mcpqd(){ printf '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"quality_delta","arguments":{"path":"%s"%s}}}\n' "$REPO" "$1" \ + | env -u TMPDIR XDG_CACHE_HOME="$XDG" "$BIN" --mcp 2>/dev/null \ + | python3 -c 'import sys,json +for line in sys.stdin: + try: d=json.loads(line) + except Exception: continue + c=(d.get("result") or {}).get("content") + if c: print(c[0].get("text","")); break + if d.get("error"): print("__ERROR__"+json.dumps(d["error"])); break'; } + mcpqd '' >"$TMP/mcp_qd_def"; mcpqd ',"show_stale":true' >"$TMP/mcp_qd_full" + python3 - "$TMP/mcp_qd_def" "$TMP/mcp_qd_full" "$TMP/targetgone_c" "$TMP/targetgone" <<'PY' && ok "(2m) MCP quality_delta folds like the CLI: counts kept, stale_by_kind identical, show_stale=true lists every sa row" || no "(2m) the MCP fold is wrong" +import json, re, sys +d, f = ( json.loads( open( p ).read() ) for p in sys.argv[ 1:3 ] ) +cliC, cliF = ( open( p ).read() for p in sys.argv[ 3:5 ] ) +stale = int( re.search( r']*\bstale="(\d+)"', cliC ).group( 1 ) ) +kinds = re.search( r']*\bstale_by_kind="([^"]*)"', cliC ).group( 1 ) +cliKeys = sorted( re.findall( r' 0, "stale == the CLI's stale= (%r vs %r)" % ( d.get( "stale" ), stale ) ), + ( d.get( "stale_by_kind" ) == kinds, "stale_by_kind == the CLI's (%r vs %r)" % ( d.get( "stale_by_kind" ), kinds ) ), + ( d.get( "stale_next" ) == "show_stale=true", "stale_next names show_stale=true (%r)" % ( d.get( "stale_next" ), ) ), + ( "stale_by_kind" not in f and "stale_next" not in f, "show_stale=true carries no fold key" ), + ( len( f.get( "sa", [] ) ) == stale, "show_stale=true lists stale sa rows (%d)" % len( f.get( "sa", [] ) ) ), + ( sorted( a[ "key" ] for a in f.get( "sa", [] ) ) == cliKeys, "the sa keys are the CLI --show-stale rows' keys" ), + ( { k: v for k, v in d.items() if k not in ( "stale_by_kind", "stale_next" ) } == { k: v for k, v in f.items() if k != "sa" }, + "every other key is identical across the two answers" ) ): + if not cond: print( " (2m) " + msg ); ok = False +sys.exit( 0 if ok else 1 ) +PY +else + printf ' SKIP (2m) MCP fold (no python3)\n' +fi run --show-stale >/dev/null 2>&1 && no "(2c) --show-stale alone was accepted" || ok "(2c) --show-stale alone is refused (it modifies --quality-delta)" # ── (3) FINDING-GONE: knotty() survives (same canonId) but is simplified back under the complexity bar ── From 099d4e6e238880075307b385bdd8090ea6c69d48 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I <210578314+joyful-ii-V-I@users.noreply.github.com> Date: Fri, 23 Oct 2026 23:54:02 -0400 Subject: [PATCH 54/77] gates: MCP fold arms predict the exact default document (fold only when it pays) Co-Authored-By: Claude Opus 5.5 --- test/mcpattrparitycheck.sh | 8 +++++- test/staleackcheck.sh | 58 ++++++++++++++++++++++++-------------- 2 files changed, 44 insertions(+), 22 deletions(-) diff --git a/test/mcpattrparitycheck.sh b/test/mcpattrparitycheck.sh index 0ce6c8711..21d3e93d0 100755 --- a/test/mcpattrparitycheck.sh +++ b/test/mcpattrparitycheck.sh @@ -263,7 +263,13 @@ if jDef is not None and jFull is not None: check( cliStale is not None and cliStale == jDef.get( "stale" ) == jFull.get( "stale" ), "COUNT: stale= agrees on the CLI (%r), MCP default (%r) and show_stale=true (%r)" % ( cliStale, jDef.get( "stale" ), jFull.get( "stale" ) ) ) mcpFolds = "stale_by_kind" in jDef - check( mcpFolds == bool( mKinds ), "COUNT: the MCP default folds exactly when the CLI does (cli=%s mcp=%s)" % ( bool( mKinds ), mcpFolds ) ) + # the twin folds only where the CLI does, and declines only where its JSON fold would not PAY (foldPays: < 75 % of the bytes) + cut = qdFull.rfind( '"sa":[' ) + predict = qdFull[ :cut ] + '"stale_by_kind":"' + ( mKinds.group( 1 ) if mKinds else "" ) + '","stale_next":"show_stale=true"}' + jsonPays = bool( mKinds ) and cut >= 0 and len( predict.encode() ) * 100 < len( qdFull.encode() ) * 75 + check( mcpFolds == jsonPays and ( not mcpFolds or qdMcp == predict ), + "COUNT: the MCP default folds exactly when the CLI does AND the JSON fold pays, into the predicted bytes (cli=%s pays=%s mcp=%s)" + % ( bool( mKinds ), jsonPays, mcpFolds ) ) if mcpFolds: check( "sa" not in jDef and jDef.get( "stale_next" ) == "show_stale=true", "COUNT: a folded twin drops sa and names show_stale=true in stale_next (got %r)" % ( jDef.get( "stale_next" ), ) ) diff --git a/test/staleackcheck.sh b/test/staleackcheck.sh index 9935cd637..7a1a8d0d4 100755 --- a/test/staleackcheck.sh +++ b/test/staleackcheck.sh @@ -124,10 +124,13 @@ fi sed 's//dev/null 2>&1; then mcpqd(){ printf '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"quality_delta","arguments":{"path":"%s"%s}}}\n' "$REPO" "$1" \ | env -u TMPDIR XDG_CACHE_HOME="$XDG" "$BIN" --mcp 2>/dev/null \ @@ -136,30 +139,43 @@ for line in sys.stdin: try: d=json.loads(line) except Exception: continue c=(d.get("result") or {}).get("content") - if c: print(c[0].get("text","")); break - if d.get("error"): print("__ERROR__"+json.dumps(d["error"])); break'; } - mcpqd '' >"$TMP/mcp_qd_def"; mcpqd ',"show_stale":true' >"$TMP/mcp_qd_full" - python3 - "$TMP/mcp_qd_def" "$TMP/mcp_qd_full" "$TMP/targetgone_c" "$TMP/targetgone" <<'PY' && ok "(2m) MCP quality_delta folds like the CLI: counts kept, stale_by_kind identical, show_stale=true lists every sa row" || no "(2m) the MCP fold is wrong" + if c: sys.stdout.write(c[0].get("text","")); break + if d.get("error"): sys.stdout.write("__ERROR__"+json.dumps(d["error"])); break'; } + mcpfold(){ # LABEL EXPECT(fold|full) CLI_COLLAPSED CLI_SHOWSTALE + mcpqd '' >"$TMP/mcp_qd_def"; mcpqd ',"show_stale":true' >"$TMP/mcp_qd_full" + python3 - "$TMP/mcp_qd_def" "$TMP/mcp_qd_full" "$3" "$4" "$2" <<'PY' && ok "$1" || no "$1" import json, re, sys -d, f = ( json.loads( open( p ).read() ) for p in sys.argv[ 1:3 ] ) +dT, fT = ( open( p, encoding = "utf-8" ).read() for p in sys.argv[ 1:3 ] ) cliC, cliF = ( open( p ).read() for p in sys.argv[ 3:5 ] ) -stale = int( re.search( r']*\bstale="(\d+)"', cliC ).group( 1 ) ) -kinds = re.search( r']*\bstale_by_kind="([^"]*)"', cliC ).group( 1 ) +want = sys.argv[ 5 ] +f = json.loads( fT ) +stale = int( re.search( r']*\bstale="(\d+)"', cliC ).group( 1 ) ) +kinds = re.search( r']*\bstale_by_kind="([^"]*)"', cliC ).group( 1 ) cliKeys = sorted( re.findall( r' 0, "stale == the CLI's stale= (%r vs %r)" % ( d.get( "stale" ), stale ) ), - ( d.get( "stale_by_kind" ) == kinds, "stale_by_kind == the CLI's (%r vs %r)" % ( d.get( "stale_by_kind" ), kinds ) ), - ( d.get( "stale_next" ) == "show_stale=true", "stale_next names show_stale=true (%r)" % ( d.get( "stale_next" ), ) ), - ( "stale_by_kind" not in f and "stale_next" not in f, "show_stale=true carries no fold key" ), - ( len( f.get( "sa", [] ) ) == stale, "show_stale=true lists stale sa rows (%d)" % len( f.get( "sa", [] ) ) ), - ( sorted( a[ "key" ] for a in f.get( "sa", [] ) ) == cliKeys, "the sa keys are the CLI --show-stale rows' keys" ), - ( { k: v for k, v in d.items() if k not in ( "stale_by_kind", "stale_next" ) } == { k: v for k, v in f.items() if k != "sa" }, - "every other key is identical across the two answers" ) ): - if not cond: print( " (2m) " + msg ); ok = False + ( f.get( "stale" ) == stale > 0 and f.get( "regressions" ) == 0, "show_stale=true: stale == the CLI's (%r vs %r), no regression" % ( f.get( "stale" ), stale ) ), + ( sorted( a[ "key" ] for a in f.get( "sa", [] ) ) == cliKeys, "show_stale=true lists the CLI --show-stale rows by key (%d)" % len( cliKeys ) ), + ( "stale_by_kind" not in f and "stale_next" not in f, "show_stale=true carries no fold key" ), + ( pays == ( want == "fold" ), "the fixture's fold decision is the expected one (%s, pays=%s)" % ( want, pays ) ), + ( dT == ( folded if pays else fT ), "the default answer is exactly the predicted %s document" % ( "folded" if pays else "full" ) ) ): + if not cond: print( " " + msg ); ok = False sys.exit( 0 if ok else 1 ) PY + } + mcpfold "(2m) MCP quality_delta, 2 stale rows: the fold does not pay, the default is the show_stale=true document byte for byte" full "$TMP/targetgone_c" "$TMP/targetgone" + # twelve more acked symbols, then deleted: 14 stale rows — the fold pays + { cat "$REPO/src/lib.cpp"; for i in 1 2 3 4 5 6 7 8 9 10 11 12; do echo "int gone$i( int a ) { return a * $i; }"; done; } >"$TMP/lib_more.cpp" + cp "$ACKS" "$TMP/acks_saved" # restored below: the arms after this one read the two-row ledger + cp "$TMP/lib_more.cpp" "$REPO/src/lib.cpp" + run --quality-delta --quality-ack="fixture: twelve uncalled helpers" >/dev/null 2>&1 + printf 'int stable( int a ) { return a + 1; }\n' >"$REPO/src/lib.cpp" + run --quality-delta --show-stale >"$TMP/tg14" 2>/dev/null; run --quality-delta >"$TMP/tg14_c" 2>/dev/null + mcpfold "(2m) MCP quality_delta, more stale rows: the default folds to stale_by_kind + stale_next=show_stale=true, counts kept" fold "$TMP/tg14_c" "$TMP/tg14" + cp "$TMP/acks_saved" "$ACKS" else printf ' SKIP (2m) MCP fold (no python3)\n' fi From f961fa0068ec556d6557cfcc4ca3a383173861e7 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I <210578314+joyful-ii-V-I@users.noreply.github.com> Date: Sat, 24 Oct 2026 00:25:56 -0400 Subject: [PATCH 55/77] hop1 fix round 1: ring cycle-guard arm, slot_fns= forward width, bridge legend says what is capped B1: hop1check arm (X1) on a two-field cycle fixture; fails with the ring's d=1/self guard removed. B2: the bridge legend no longer claims every crossing fans out to at most 16: a slot crossing is bounded on its callers (slot_callers=), and slot_fns= now prints how many functions the field holds, uncapped (a cap would delete the gold hops). Full, compact and MCP legend text; arms X2 and L3. Nits: ring legend says no width bound (N3), next= claim reworded (N2), w17 next= arm (N4), over-decline limit documented (N5), defensive self-call skip commented. Co-Authored-By: Claude Sonnet 5.5 --- CHANGELOG.md | 9 +++---- src/backhops.h | 4 ++-- src/compactlegend.h | 5 ++-- src/pathgaps.h | 44 +++++++++++++++++++++++++--------- test/hop1check.sh | 58 +++++++++++++++++++++++++++++++++++++++++++-- 5 files changed, 99 insertions(+), 21 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3dcf61db0..996f370d2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,17 +22,18 @@ not published here — see `docs/EVALS.md` for the instruments behind the headli X). The answers that join hops now read the same rows, from one provider (`src/backhops.h`): - `--path`'s hedged `` crosses a slot hop (`via="slot"`, `via="slot-name"` for a field-name-only join) while the - join names at most 16 calling functions (`slot_callers=`); a chain whose every crossing is a slot may take three - crossings, any other chain keeps two. On tmux, `--path=cmdq_fire_command,window_copy_cmd_cancel` now prints the + join names at most 16 calling functions (`slot_callers=`), and `slot_fns=` says how many functions the field holds + (not capped: a wide table such as tmux's 92-entry `cmd_entry.exec` is still crossed, and the row says how wide it is); a + chain whose every crossing is a slot may take three crossings, any other chain keeps two. On tmux, `--path=cmdq_fire_command,window_copy_cmd_cancel` now prints the three-field-call chain (`cmd_entry.exec`, `window_mode.command`, the command table's `.f`); before, no chain. - A `reachable="0"` path answer whose search met gaps and whose `to=` has such a hop ends with `next="--callers="` (CLI and MCP `path_between`) instead of the gap rows' bodies. - `--impact` (XML, `--json`, columnar count, MCP `impact`) adds `reaches_hedged=N` beside `reaches=`: the functions that may reach the symbol through one or two calls through struct fields, with the two-hop ones listed as - `` (16 shown, `next=` the rest). Never counted in + `` (16 shown, no width bound on the d=1 set; `next=` pages the callers of the first unshown row's `calls=`, not every hidden row). Never counted in `reaches=`, `by_depth=` or the radius counts. -Every crossing stays a hedge: `proven="0"`, `via=` named, its width beside it, and never in `reachable=`/`hops=`/`reaches=`. +Every crossing stays a hedge: `proven="0"`, `via=` named, its width beside it (a slot crossing's `slot_callers=` is bounded at 16, its `slot_fns=` is printed, not capped), and never in `reachable=`/`hops=`/`reaches=`. ### Fixed — a declined call on a typed in-tree receiver is no longer read as the builtin method its name spells diff --git a/src/backhops.h b/src/backhops.h index 8af84192a..2db15d8fe 100644 --- a/src/backhops.h +++ b/src/backhops.h @@ -291,7 +291,7 @@ class BackwardHops for( const std::uint32_t c : calls ) { const NodeId f = m_ing.references[ c ].fromSymbol; - if( f != sink && f < m_ing.symbols.size() ) // a call made inside the sink itself is no way in + if( f != sink && f < m_ing.symbols.size() ) // a call made inside the sink itself is no way in (defensive: the ring's `known` and the bridge's layer test hold the seeds too) { slot.push_back( BackHop{ f, typed ? BackHopVia::Slot : BackHopVia::SlotName, std::uint32_t( calls.size() ), c } ); } @@ -482,7 +482,7 @@ inline std::string impactRingJson( const IngestResult& ing, const ImpactRing& ri inline constexpr std::string_view kImpactRingLegend = "reaches_hedged=N (absent when 0): N functions MAY reach of= through one or two calls through a struct FIELD (the IX-1 slot " "join): the rows' slot called_by= (one slot hop) and their own slot callers (two), not counted in reaches= and not an " - "answered definition. A hedge: never a proven caller, never in reaches=/by_depth=/radius_*. "; + "answered definition. The one-hop set has NO width bound (a wide field's callers are all counted). A hedge: never a proven caller, never in reaches=/by_depth=/radius_*. "; inline constexpr std::string_view kImpactRingRowsLegend = " lists the two-hop ones: = called_by may call " "calls= through a field at called_at=; first 16, next= the callers of the first unshown calls=. "; diff --git a/src/compactlegend.h b/src/compactlegend.h index 386be625b..ac767cbc0 100644 --- a/src/compactlegend.h +++ b/src/compactlegend.h @@ -362,7 +362,7 @@ inline constexpr CompactCompletenessTerm kCompactCompletenessTerms[] = { "called_at", "vr called_at= via=slot|slot-name: called_by='s calls through that struct field, file:line in the same order; slot = same declared struct type, slot-name = field name only (a type unknown); MAY call, not a proven call, in no count", false, "vr" }, { "slot_name_also", "slot_name_also=K: K name-only callers hidden behind the same-type ones", false, "vr" }, // HOP1 (src/backhops.h kImpactRingLegend): --impact's hedged slot ring, present-only - { "reaches_hedged", "reaches_hedged=N: N functions MAY reach of= through one or two calls through a struct field (the vr rows' slot callers and theirs); never in reaches= or any count" }, + { "reaches_hedged", "reaches_hedged=N: N functions MAY reach of= through one or two calls through a struct field (the vr rows' slot callers and theirs, no width bound); never in reaches= or any count" }, { "calls", "ring d=2 total= shown= next=; rh called_by= called_at= via= calls=: called_by may call calls= (a vr row's slot caller) through a struct field at called_at=; a hedge, in no count", false, "rh" }, { "called_more", "called_more=N: N more callers past the first 8", false, "vr" }, { "slot_calls", "slot_calls=N: N calls in this body go through a struct field holding a function; in no count" }, @@ -811,7 +811,8 @@ inline constexpr CompactCompletenessTerm kCompactAttributeReadings[] = // HEDGED BRIDGE and GLOB ENDPOINT (src/pathgaps.h kPathBridgeLegend / kPathGlobLegend), present-only on a reachable=0 answer { "crossings", "bridge proven=0 crossings= hops=: a HEDGED chain, NOT a path; the crossing into that row, the rest proven edges", true, "bridge", MapHeaderRead::No, {}, "path" }, // HOP1 (src/backhops.h, kPathBridgeLegend): a slot crossing's row, present-only; up to 3 crossings when every one is a slot - { "slot_callers", "s via=slot|slot-name slot_callers=N: the previous row calls through a struct field this row is stored in (slot-name: field name only); N functions that join names, at most 16; a hedge", true, {}, MapHeaderRead::No, {}, "path" }, + { "slot_callers", "s via=slot|slot-name slot_callers=N: the previous row calls through a struct field this row is stored in (slot-name: field name only); N calling functions that join names, at most 16 (a crossing wider is not made); a hedge", true, {}, MapHeaderRead::No, {}, "path" }, + { "slot_fns", "s slot_fns=K: K functions that struct field holds (the width the crossing picks from); NOT capped, a wide table is still crossed; a hedge", true, {}, MapHeaderRead::No, {}, "path" }, { "to_glob", "to_glob=1: to='s * matched every function or method definition fitting it (to_defs= counts them)", false, "path", MapHeaderRead::No, {}, "path" }, { "from_glob", "from_glob=1: from='s * matched every function or method definition fitting it (from_defs= counts them)", false, "path", MapHeaderRead::No, {}, "path" }, // SINK-SIDE FRONTIER (src/pathgaps.h kNearSinkLegend), present on a reachable=0 answer: the block and its rows. diff --git a/src/pathgaps.h b/src/pathgaps.h index 760fa3153..c42af66ce 100644 --- a/src/pathgaps.h +++ b/src/pathgaps.h @@ -309,7 +309,7 @@ inline constexpr std::string_view kPathGapsLegend = // root next= is the callers verb on to= instead — the call that crosses the gap, where the gap rows' bodies only show the text. inline constexpr std::string_view kPathGapsNextBodies = "next= reads the rows' bodies. "; inline constexpr std::string_view kPathGapsNextCallers = - "next= lists to='s callers: to= has a slot caller or a declined call naming it (at most 16 candidates), none a resolved edge. "; + "next= lists to='s callers: to= has a slot caller or a declined call naming it (at most 16 callers or candidates), none a resolved edge. "; static_assert( kPathGapsLegend.ends_with( kPathGapsNextBodies ), "the callers form replaces exactly the bodies sentence" ); inline std::string pathGapsLegend( bool on, bool callersNext = false ) { @@ -925,7 +925,7 @@ inline std::string pathGlobAttrXml( bool fromGlob, bool toGlob ) // build, whose callee is the sink. A human crosses exactly those hops. // // THE CONTRACT. Only on a reachable="0" answer, a second search may CROSS hops the graph holds no edge for — at most -// kBridgeCrossings of them, each fanning out to at most kBridgeFanOut candidates (a wider one is not followed): +// kBridgeCrossings of them, each fanning out to at most kBridgeFanOut candidates (a wider declined/value/namesake one is not followed; a slot one is bounded on its callers, below): // declined from a symbol to each candidate of a declined call it makes (the call's stored candidate list); // value from a symbol to each function it stores or passes as a value (ValueRefIndex::targetsOf); // closure from a function to each named function written in its body (src/closures.h); @@ -933,7 +933,9 @@ inline std::string pathGlobAttrXml( bool fromGlob, bool toGlob ) // same-kind definitions (the namesakes the proven search never walked); // slot (HOP1) from a function that calls through a struct field to each function stored in that field, read back // from the stored function's side (backhops.h BackwardHops: the --callers join, slot-name its -// field-name tier), only while that join names at most kBridgeFanOut calling functions (slot_callers=). +// field-name tier), only while that join names at most kBridgeFanOut calling functions (slot_callers=). That bound is +// on the BACKWARD width only; how many functions the field holds (slot_fns=, the forward width) is printed, never +// capped — a cap would delete the gold hop through a wide table (tmux's 92-entry cmd_entry.exec). // A chain whose EVERY crossing is a slot hop may take kBridgeSlotCrossings of them (tmux's send-keys chain is three field // calls deep: cmd_entry.exec, window_mode.command, the command table's .f); any other chain keeps kBridgeCrossings. // Layer by layer — every symbol reachable with k crossings over resolved edges before any with k+1 — so the chain printed @@ -962,6 +964,7 @@ struct PathBridge std::vector hops; // from= definition … to= definition std::vector via; // parallel: how the hop INTO each row was taken (Edge: a resolved call edge) std::vector fan; // parallel: a crossing's candidate count, or (slot) the calling functions the join names; 0 on an edge + std::vector fwd; // parallel: (slot) the functions the field holds, uncapped (slot_fns=); 0 on any other row std::size_t crossings = 0; bool found() const noexcept { return !hops.empty(); } @@ -969,13 +972,22 @@ struct PathBridge // SLOT crossings out of `u` (HOP1): each function a struct-field call in u may call, kept only when that function's own backward // hops name u (the --callers join and its tier rule, read from the provider: one copy) within the width bound. `cross( to, via, -// width )` decides whether the target is new. +// width, fieldFns )` decides whether the target is new; fieldFns = the distinct functions the call's field holds (never capped). template inline void bridgeSlotCrossings( const ValueRefIndex& values, const BackwardHops& backHops, NodeId u, Cross&& cross ) { for( const std::uint32_t call : values.slots().callsIn( u ) ) { - for( const SlotIndex::Hit& stored : values.slots().valuesFor( call ) ) + const std::vector held = values.slots().valuesFor( call ); + std::vector fns; // the distinct functions this call's field holds: the forward width, printed and never capped + for( const SlotIndex::Hit& stored : held ) + { + const std::vector t = values.targetsOf( stored.ref ); + fns.insert( fns.end(), t.begin(), t.end() ); + } + std::ranges::sort( fns ); + fns.erase( std::unique( fns.begin(), fns.end() ), fns.end() ); + for( const SlotIndex::Hit& stored : held ) { for( const NodeId f : values.targetsOf( stored.ref ) ) { @@ -983,7 +995,7 @@ inline void bridgeSlotCrossings( const ValueRefIndex& values, const BackwardHops const auto it = std::ranges::find_if( hops, [ & ]( const BackHop& h ) { return h.from == u && h.via != BackHopVia::Declined && h.width <= kBridgeFanOut; } ); if( it != hops.end() ) { - cross( f, it->via == BackHopVia::Slot ? BridgeVia::Slot : BridgeVia::SlotName, it->width ); + cross( f, it->via == BackHopVia::Slot ? BridgeVia::Slot : BridgeVia::SlotName, it->width, fns.size() ); } } } @@ -1072,8 +1084,11 @@ inline PathBridge pathBridge( const IngestResult& ing, const Graph& g, std::span std::vector parent( S, kNoNode ); std::vector viaOf( S, BridgeVia::Edge ); std::vector fanOf( S, 0 ); + std::vector fwdOf( S, 0 ); // HOP1: a slot crossing's forward width (slot_fns=) std::vector isDst( S, 0 ); std::vector slotOnly( S, 0 ); // HOP1: every crossing on the symbol's chain so far is a slot hop (layer 0: none) + // Known limit: within one layer a symbol is claimed by the FIRST chain to meet it (slot-only seeds sort first, but a mixed chain at a + // shallower BFS depth still wins), so a slot-only chain through that symbol may lose its third crossing: a bridge is lost, never a wrong one. // a chain may not route THROUGH a de-prioritized symbol — a test, bench, demo or generated file, a minified bundle, or (since // train 31's head round-robin lane) a .d.ts / .pyi declaration twin whose package ships its implementation (filter.h's rank // tiers: any multiplier below 1) — only start or end at one: the runtime path a reader asks for runs through the code itself @@ -1133,7 +1148,7 @@ inline PathBridge pathBridge( const IngestResult& ing, const Graph& g, std::span ownerReach.add( ing.symbols[u] ); } // one crossing out of this layer seeds the next - const auto cross = [ & ]( NodeId from, NodeId to, BridgeVia via, std::size_t fan ) + const auto cross = [ & ]( NodeId from, NodeId to, BridgeVia via, std::size_t fan, std::size_t fwd = 0 ) { if( to < S && layer[to] == kUnseen && routable( to ) ) { @@ -1141,6 +1156,7 @@ inline PathBridge pathBridge( const IngestResult& ing, const Graph& g, std::span parent[to] = from; viaOf[to] = via; fanOf[to] = std::uint32_t( fan ); + fwdOf[to] = std::uint32_t( fwd ); slotOnly[to] = char( slotOnly[from] != 0 && bridgeViaIsSlot( via ) ); frontier.push_back( to ); } @@ -1151,7 +1167,7 @@ inline PathBridge pathBridge( const IngestResult& ing, const Graph& g, std::span { continue; } - bridgeSlotCrossings( values, backHops, u, [ & ]( NodeId to, BridgeVia via, std::size_t fan ) { cross( u, to, via, fan ); } ); // HOP1 + bridgeSlotCrossings( values, backHops, u, [ & ]( NodeId to, BridgeVia via, std::size_t fan, std::size_t fwd ) { cross( u, to, via, fan, fwd ); } ); // HOP1 if( slotStepOnly ) { continue; @@ -1214,11 +1230,13 @@ inline PathBridge pathBridge( const IngestResult& ing, const Graph& g, std::span out.hops.push_back( n ); out.via.push_back( viaOf[n] ); out.fan.push_back( fanOf[n] ); + out.fwd.push_back( fwdOf[n] ); out.crossings += viaOf[n] != BridgeVia::Edge ? 1u : 0u; } std::ranges::reverse( out.hops ); std::ranges::reverse( out.via ); std::ranges::reverse( out.fan ); + std::ranges::reverse( out.fwd ); if( out.hops.size() > kBridgeMaxHops + 1 ) { return PathBridge{}; // past the hop bound: the legend says a bridge is shown only within it @@ -1233,15 +1251,15 @@ inline PathBridge pathBridge( const IngestResult& ing, const Graph& g, std::span inline constexpr std::string_view kPathBridgeLegend = ": a HEDGED chain from= to to= of at most 8 hops that crosses at most 2 the graph holds no " - "edge for (3 when every crossing is a slot), each fanning out to at most 16 candidates (a declined call spelled like a builtin type's method is not crossed, " + "edge for (3 when every crossing is a slot), each declined, value or namesake one fanning out to at most 16 candidates, each slot one bounded on its callers (a declined call spelled like a builtin type's method is not crossed, " "unless its receiver is a this/self field whose class the source states and the tree defines), and " "runs through no test, bench, demo, generated or minified symbol, nor a .d.ts/.pyi declaration twin whose package ships its implementation; its rows read like a path's, and via= on a row names the crossing INTO it: declined = " "one of cands= candidates of a declined call in the previous row, value = a function the previous row stores or passes as a value, " "closure = a function written in the previous row's body, namesake = another same-language, same-kind definition of a name the previous row " "calls by name alone, not spelled like a builtin type's method, whose class (or file, for a free function) owns something the " "chain's earlier layers reached, slot = the previous row calls through a struct field this row is stored in (same declared struct " - "type; slot-name = field name only), slot_callers=N the functions that field join names, at most 16 (via=name rows are proven edges " - "bound by name). Fewest crossings first. It is NOT a path: reachable= and " + "type; slot-name = field name only), slot_callers=N the functions that field join names, at most 16, and slot_fns=K the functions the field holds, NOT capped " + "(a wide table is still crossed: K says how wide) (via=name rows are proven edges bound by name). Fewest crossings first. It is NOT a path: reachable= and " "hops= on the root stay 0, no count includes it, and every crossing is a hedge. "; inline std::string_view pathBridgeLegend( bool on ) noexcept { @@ -1267,6 +1285,10 @@ inline std::string pathBridgeXml( const IngestResult& ing, const Graph& g, const // a slot crossing's width is the calling functions its field join names (HOP1), every other crossing's its candidates out += " via=\"" + std::string( kBridgeViaNames[ std::size_t( br.via[i] ) ] ) + ( bridgeViaIsSlot( br.via[i] ) ? "\" slot_callers=\"" : "\" cands=\"" ) + std::to_string( br.fan[i] ) + "\""; + if( bridgeViaIsSlot( br.via[i] ) ) + { + out += " slot_fns=\"" + std::to_string( br.fwd[i] ) + "\""; // the forward width, uncapped: how many functions the field holds + } } else if( i > 0 ) { diff --git a/test/hop1check.sh b/test/hop1check.sh index b4e0519fc..e7493aa79 100755 --- a/test/hop1check.sh +++ b/test/hop1check.sh @@ -8,7 +8,7 @@ # own slot row. hono's `this.router.match( … )` was never crossed: `match` is spelled like String.prototype.match. # # THE CONTRACT. Every crossing is a hedge: proven="0" on the bridge, via= named (slot | slot-name | declined), its width -# beside it (slot_callers= / cands=, at most 16), and never counted in reachable= / hops= / reaches=. A chain whose every +# beside it (slot_callers= at most 16, slot_fns= the field's forward width, uncapped / cands= at most 16), and never counted in reachable= / hops= / reaches=. A chain whose every # crossing is a slot may take 3 crossings; any other keeps 2. The ring is one or two slot hops back, reaches_hedged= beside # reaches=, rows after the rows. #11 lifts the builtin skip only for one field of this/self or of a # parameter whose class the source states, every same-named class typing it as an in-tree class, on a Ladder decline with a @@ -34,6 +34,8 @@ # (I1) --impact ring: reaches="0" reaches_hedged="2", one row; (I2) a ring member already in reaches= is not counted; # (I3) no slot row: no reaches_hedged=, no ; (I4) JSON twin, same names; (I5) columnar count; (I6) row cap 16 + next=. # (M1) MCP path_between: the same and next= as the CLI; (M2) MCP impact: the same reaches_hedged= and . +# (X1) ring cycle guard: a d=1 function is never reprinted as d=2 (ringcyc); (X2) slot_fns= prints the forward width, uncapped (fan); +# (X3) next= keeps --expand past 16 backward callers; (L3) the legends say exactly what is capped and define slot_fns=. # (L1) full legends carry the slot / ring / callers-next sentences and parse as XML; (L2) compact legends define the terms. # # Usage: test/hop1check.sh [BIN] (BIN defaults to RIPWIRE_BIN, then build/ripwire) @@ -47,7 +49,7 @@ command -v python3 >/dev/null 2>&1 || { echo " FAIL python3 required"; exit 2; TMP="$( mktemp -d )"; trap 'rm -rf "$TMP"' EXIT # ── fixtures ──────────────────────────────────────────────────────────────────────────────────────────────────────── -mkdir -p "$TMP/c3" "$TMP/mix" "$TMP/w16" "$TMP/w17" "$TMP/neg" "$TMP/known" "$TMP/cap" "$TMP/ts/routers" "$TMP/ts/app" \ +mkdir -p "$TMP/ringcyc" "$TMP/fan" "$TMP/c3" "$TMP/mix" "$TMP/w16" "$TMP/w17" "$TMP/neg" "$TMP/known" "$TMP/cap" "$TMP/ts/routers" "$TMP/ts/app" \ "$TMP/ts2/routers" "$TMP/ts2/app" "$TMP/ts2/other" "$TMP/wide/routers" "$TMP/wide/app" cat > "$TMP/c3/chain.c" <<'EOF' struct entry { const char *name; void (*exec)( void ); }; @@ -114,6 +116,35 @@ void send_keys_exec( void ) const struct entry send_keys_entry = { .exec = send_keys_exec }; void fire_command( struct entry *e ) { e->exec(); } EOF +# ring cycle guard (review B1): fa and fb both call sink through A.act (d=1); fb is stored in B.go, which fa calls through, so the +# d=2 walk meets fa again from fb. self_ref calls through the field it is stored in. +cat > "$TMP/ringcyc/r.c" <<'EOF' +struct A { void (*act)( void ); }; +struct B { void (*go)( void ); }; +const struct A *ga( void ); +const struct B *gb( void ); +void sink( void ) { } +const struct A SA = { .act = sink }; +void fb( void ) { const struct A *a = ga(); a->act(); } +const struct B SB = { .go = fb }; +void fa( void ) { const struct A *a = ga(); a->act(); const struct B *b = gb(); b->go(); } +void self_ref( void ); +struct C { void (*me)( void ); }; +const struct C *gc( void ); +const struct C SC = { .me = self_ref }; +void self_ref( void ) { const struct C *c = gc(); c->me(); } +EOF +# forward width (review B2): 20 functions stored in ops.run, one caller calls through it — the crossing is 1 caller of 20 functions +{ + echo 'struct ops { const char *name; void (*run)( void ); };' + i=0; while [ "$i" -lt 20 ]; do printf 'void h%d( void ) { }\n' "$i"; i=$(( i + 1 )); done + echo 'static const struct ops table[] = {' + i=0; while [ "$i" -lt 20 ]; do printf ' { "h%d", h%d }%s\n' "$i" "$i" "$( [ "$i" -lt 19 ] && echo , )"; i=$(( i + 1 )); done + echo '};' + echo 'const struct ops *pick( int i );' + echo 'void dispatch( int i ) { const struct ops *o = pick( i ); o->run(); }' + echo 'int main( void ) { dispatch( 3 ); return 0; }' +} > "$TMP/fan/fan.c" # width: a field the sink is stored in, called through by 16 (w16) or 17 (w17) functions for n in 16 17; do { @@ -322,6 +353,22 @@ check( "(D7) a typed receiver over 17 candidates is no way in (near_sink why=ext td = path( ts, "onString", "routers/a.ts:match" ) check( "(D8) near_sink counts exactly the two crossed sites (why=declined:2)", 'why="declined:2"' in body( td ), repr( re.findall( r']*>', body( td ) ) ) ) +# ── X: review fixes (fix round 1) ─────────────────────────────────────────────────────────────────────────────────── +trc = run( tmp + "/ringcyc", "--impact=sink" ) or ""; rrc = root( trc, "impact" ) +check( "(X1) ring cycle guard: fa and fb are both d=1, so reaches_hedged=2 and NO (a d=1 function is not printed again as d=2)", + attr( rrc, "reaches_hedged" ) == "2" and " Date: Sat, 24 Oct 2026 00:56:38 -0400 Subject: [PATCH 56/77] pathgaps fix 7: a re-export chain hops only through files that bind the name by one plain import (hop-wide rule) G1: pythonFileBindsOnlyByImport (one module-scope binding, a member import with no rename; no module rebind of any form, no star that could bind it, no dynamic write, a record exists) replaces pythonFileRenames at EVERY hop of pythonReexportChain; a hop that fails or is unknown ends the chain, so the row is withheld and never bridged (n03 n03b n03c n04 n04b n05 n19). n13: markPythonDynamicBinds files a `globals()` / `setattr(...)` call under the file's `__getattr__` pseudo-name in pythonModuleRebind, so the file is unknown for every name (hop test and pythonPackageMayBind); disclosed in CHANGELOG, `sys.modules[__name__].a = x` stays a floor. K23: 16 variants x (near_sink, bridge) in pathgapcheck. Co-Authored-By: Claude Sonnet 5.5 --- CHANGELOG.md | 9 +++- src/graph.h | 100 +++++++++++++++++++++++++++++-------------- src/pathgaps.h | 3 ++ test/pathgapcheck.sh | 61 ++++++++++++++++++++++++++ 4 files changed, 139 insertions(+), 34 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 71333ebcd..a61191824 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -49,8 +49,13 @@ not published here — see `docs/EVALS.md` for the instruments behind the headli reads `other` (the member's own name), not `a`; a dotted `import pkg.x` binds the root `pkg`, which no table names, so `pkg.Base` is withheld (this also over-hedges `import pkg.x as pkg`). `from pkg import a` (aliased or not) with `a.Base` reads the submodule `a` only when the package's `__init__` does not bind `a` itself (an import, def, class or assignment of - it, a star import that could bind it, or a module `__getattr__`), and a re-export hop that renames (`from pkg.x.a import - Other as Base` in the package or a middle module) is not followed. The import must be the name's one binding; + it, a star import that could bind it, or a module `__getattr__`). A re-export chain (`from pkg.x import Base`, where the + package or a middle module re-exports the name) is followed only through files that bind the name by ONE plain import, + at every hop: a hop that renames it (`from pkg.x.a import Other as Base`), reassigns it afterwards (`Base = Other`, `Base +=`, + a `for`, `with` or walrus target, a def or class of it, a `global Base` in any def), star-imports after it, or has no + record of it ends the chain, and the site is withheld. A hop file that calls `globals()` or `setattr(...)` anywhere, or + defines a module `__getattr__`, counts as unknown for every name (a dynamic write no table sees; this over-hedges any such + file, and `sys.modules[__name__].a = x`, which is not a call, stays a floor). The import must be the name's one binding; otherwise the site is withheld, never shown. Not read, and not detectable statically: a rebinding made from outside the file (`a.Base = Other` on an imported module). diff --git a/src/graph.h b/src/graph.h index ab714cad9..88d6064d4 100644 --- a/src/graph.h +++ b/src/graph.h @@ -2169,6 +2169,27 @@ inline bool isPythonSourcePath( std::string_view path ) noexcept return ext == ".py" || ext == ".pyi"; } +// the pseudo-name a file's DYNAMIC module-namespace writes are filed under in pythonModuleRebind ("#__getattr__"): a module +// `def __getattr__` (an ordinary top-level def, so buildPythonModuleRebindVetoes already records it) and, added by +// markPythonDynamicBinds, a `globals()` or `setattr(...)` call anywhere in the file (`globals()['a'] = x`, `setattr(sys.modules[__name__], ...)`): +// writes no static table can see. The file then binds ANY name it could be asked for; pythonFileBindsOnlyByImport and pythonPackageMayBind +// read it as unknown. `sys.modules[__name__].a = x` is not a call and stays a floor. +inline constexpr std::string_view kPythonDynamicBind = "__getattr__"; + +inline void markPythonDynamicBinds( const IngestResult& ing, HashMap& pythonModuleRebind ) +{ + std::string key; + for( const Reference& r : ing.references ) + { + if( r.role == RefRole::Call && ( r.calleeName == "globals" || r.calleeName == "setattr" ) && r.fileId < ing.files.size() + && isPythonSourcePath( rootRelPath( ing, r.fileId ) ) ) + { + key.clear(); Narrower::appendUint( key, r.fileId ); key.push_back( '#' ); key.append( kPythonDynamicBind ); + pythonModuleRebind.try_emplace( key, '\0' ); + } + } +} + // the first name of every Python class's base clause (the bare base, else its qualifier) inline HashMap pythonBaseClauseNames( const IngestResult& ing ) { @@ -2480,6 +2501,7 @@ inline ExternalVetoTables buildExternalVetoTables( const IngestResult& ing ) // WHOLE branch's delta against origin/main did not) — see their own doc comments for the WHY/semantics // of what each table means; this call site is purely the "where". t.pythonModuleRebind = buildPythonModuleRebindVetoes( ing ); + markPythonDynamicBinds( ing, t.pythonModuleRebind ); PythonImportVetoes iv = buildPythonImportVetoes( ing, fileIndex, moduleNames, t.pythonModuleRebind ); t.importBind = std::move( iv.importBind ); t.importBindFile = std::move( iv.importBindFile ); @@ -6406,11 +6428,49 @@ inline const std::vector* pythonModuleBindsOf( const ExternalVet return it == v.pythonScopes.nameBinds.end() ? nullptr : &it->second; } -// does file `f` import `name` under another name than the member's own (`from m import Other as name`)? -inline bool pythonFileRenames( const ExternalVetoTables& v, std::uint32_t f, std::string_view name ) +// can `from m import *` (any of file `f`'s) bind `name`? A star target outside the tree could bind anything; one inside can bind +// what its own module scope binds (a def, class, assignment or import of the name) or, when it star-imports itself, anything. +// Unknown answers yes (no walk past one level, no __all__ read). +inline bool pythonStarMayBind( const ExternalVetoTables& v, std::uint32_t f, std::string_view name ) +{ + if( f >= v.pythonScopes.starTargets.size() ) + { + return false; + } + std::string key; + for( std::uint32_t target : v.pythonScopes.starTargets[ f ] ) + { + if( target == kNoFile || ( target < v.pythonScopes.starTargets.size() && !v.pythonScopes.starTargets[ target ].empty() ) ) + { + return true; + } + const auto it = v.pythonScopes.nameBinds.find( fileNameKey( key, target, name ) ); + if( it != v.pythonScopes.nameBinds.end() && std::ranges::any_of( it->second, []( const PythonNameBind& b ) { return b.scope == kNoNode; } ) ) + { + return true; + } + } + return false; +} + +// Is `name` in Python file `f` bound by ONE thing only: a plain `from m import name`, where `name` keeps the member's own name? The +// default-withhold test a re-export chain applies at EVERY hop: exactly one module-scope binding, a member import with no rename +// (`import a as name` and `from m import x as name` bind something else); no later rebinding of any form (an assignment, augmented +// assignment, for/with/except target, walrus, def or class of the name, a `global` anywhere: pythonModuleRebind), no star import that could +// bind it, and no dynamic write (kPythonDynamicBind). Anything else, and anything unknown (no nameBinds row), is false: the chain stops +// there, so the row is withheld, never shown and never a bridge. +inline bool pythonFileBindsOnlyByImport( const ExternalVetoTables& v, std::uint32_t f, std::string_view name ) { const std::vector* binds = pythonModuleBindsOf( v, f, name ); - return binds != nullptr && std::ranges::any_of( *binds, []( const PythonNameBind& b ) { return b.scope == kNoNode && !b.member.empty(); } ); + if( binds == nullptr ) + { + return false; + } + std::string key; + return std::ranges::count_if( *binds, []( const PythonNameBind& b ) { return b.scope == kNoNode; } ) == 1 + && std::ranges::any_of( *binds, []( const PythonNameBind& b ) { return b.scope == kNoNode && b.isImport && !b.moduleForm && b.member.empty(); } ) + && !v.pythonModuleRebind.contains( fileNameKey( key, f, name ) ) && !v.pythonModuleRebind.contains( fileNameKey( key, f, kPythonDynamicBind ) ) + && !pythonStarMayBind( v, f, name ); } inline constexpr int kPythonReexportHops = 4; // member-import hops provenBasesOf follows (a package re-exporting a re-export) @@ -6428,9 +6488,10 @@ inline void pythonReexportChain( const BaseEvidenceTables& t, const std::vector< { return; } - // hop through `f` only when its import of `link` keeps the name: `from pkg.x.a import Other as Base` exports Other, and the - // candidates here are named Base (the import keys memberImportFile by the alias) - f = pythonFileRenames( t.extVeto, f, link ) ? kNoFile : pythonImportFileOf( t.extVeto.memberImportFile, f, link, key ); + // hop through `f` only when its import of `link` is the name's one binding there (pythonFileBindsOnlyByImport): `from pkg.x.a import + // Other as Base` exports Other, a star import or a later `Base = Other` or `global Base` exports whatever it left, and the candidates + // here are named Base (the import keys memberImportFile by the alias). A file that fails the test, or is unknown, ends the chain + f = pythonFileBindsOnlyByImport( t.extVeto, f, link ) ? pythonImportFileOf( t.extVeto.memberImportFile, f, link, key ) : kNoFile; } } @@ -6466,31 +6527,6 @@ inline std::vector pythonClassScopeChain( const IngestResult& ing, NodeI return chain; } -// can `from m import *` (any of file `f`'s) bind `name`? A star target outside the tree could bind anything; one inside can bind -// what its own module scope binds (a def, class, assignment or import of the name) or, when it star-imports itself, anything. -// Unknown answers yes (no walk past one level, no __all__ read). -inline bool pythonStarMayBind( const ExternalVetoTables& v, std::uint32_t f, std::string_view name ) -{ - if( f >= v.pythonScopes.starTargets.size() ) - { - return false; - } - std::string key; - for( std::uint32_t target : v.pythonScopes.starTargets[ f ] ) - { - if( target == kNoFile || ( target < v.pythonScopes.starTargets.size() && !v.pythonScopes.starTargets[ target ].empty() ) ) - { - return true; - } - const auto it = v.pythonScopes.nameBinds.find( fileNameKey( key, target, name ) ); - if( it != v.pythonScopes.nameBinds.end() && std::ranges::any_of( it->second, []( const PythonNameBind& b ) { return b.scope == kNoNode; } ) ) - { - return true; - } - } - return false; -} - // can package file `pkg` (its __init__.py) bind `name` itself? `from pkg import name` reads the package's attribute FIRST and falls back // to the submodule `name` only when there is none, so the submodule is the answer only when nothing in the package binds the name: no // import, def, class or assignment of it at module scope (a `global` anywhere counts, pythonModuleRebind), no module `__getattr__` @@ -6503,7 +6539,7 @@ inline bool pythonPackageMayBind( const ExternalVetoTables& v, std::uint32_t pkg { return true; } - return v.pythonModuleRebind.contains( fileNameKey( key, pkg, name ) ) || v.pythonModuleRebind.contains( fileNameKey( key, pkg, "__getattr__" ) ) + return v.pythonModuleRebind.contains( fileNameKey( key, pkg, name ) ) || v.pythonModuleRebind.contains( fileNameKey( key, pkg, kPythonDynamicBind ) ) || pythonStarMayBind( v, pkg, name ); } diff --git a/src/pathgaps.h b/src/pathgaps.h index 2ec5a897b..1daaf07c8 100644 --- a/src/pathgaps.h +++ b/src/pathgaps.h @@ -532,6 +532,9 @@ class NamesakeOwnerReach // does a Python base name with any binding but its import in the class statement's scope chain (pythonBaseNameProven: // `Base = Other` at module, class-body or function scope, a parameter, a match capture, a `type` statement, a second import, // a star import that could bind it; a qualifier also imported as a module outside the chain, where the file-wide tables decide). +// A re-export chain is followed only through files whose binding of the name is ONE plain import, at every hop +// (pythonFileBindsOnlyByImport): a hop that renames, rebinds, star-imports after it, writes the module namespace dynamically +// (`globals()`, `setattr`, `__getattr__`) or has no record of the name ends the chain, so the row is withheld. // Rule 2c's derivation (below): the classes that derive from a bound definition's class through PROVEN base edges only // (Graph::implementorBound == BaseBound::Proven), memoized per bound owner. The classes with a derived class are indexed by // "#" on first ask (one pass over the implementors map); the walk visits each class once. diff --git a/test/pathgapcheck.sh b/test/pathgapcheck.sh index 2082d5e80..499b510cc 100755 --- a/test/pathgapcheck.sh +++ b/test/pathgapcheck.sh @@ -89,6 +89,7 @@ # (K21-K22) fix 6: a package that BINDS the name (`from pkg.y import base2 as a` in pkg/x/__init__.py) is not its submodule a.py (shadowalias, # shadowplain), and a re-export hop that RENAMES (`from pkg.x.a import Other as Base` in the package or a middle module) exports Other, # not a.py's Base (reexpalias, reexpmod, reexpmid): all withheld; K22 re-exports under the SAME name and stays a way in. +# (K23) fix 7: a re-export hop file that rebinds the name any way but its one plain import (star, assignment, global, globals(), setattr, def ...) is withheld at every hop. # # Usage: test/pathgapcheck.sh [BIN] (BIN defaults to RIPWIRE_BIN, then build/ripwire) set -u @@ -640,6 +641,57 @@ for v in shadowalias shadowplain reexpalias reexpmod reexpmid reexpplain reexpmi printf "$out" > "$d/other/b.py" done # K21 end +# K23 fixtures (lane ind4b fix 7, the hop-wide rule): EVERY file a re-export chain hops through must bind the name by its one, unrenamed import. +# CPython (3.13) truth per variant: starmid/starinit/starqual/starqualmid: the hop's star import `from pkg.y.base2 import *` follows the import, so +# Base is pkg.y.base2.Base; assignmid/assigninit/assignqual/globalmid: `Base = Other` (or a `global Base; Base = Other` in a def that runs) makes it +# pkg.x.a.Other; globalsinit: `globals()['a'] = base2` in the package binds `a` to pkg.y.base2; setattrinit: setattr(sys.modules[__name__], 'a', ...) +# does the same; augmid/forinit/withinit/walrusinit/defmid/classmid: later rebinds of every other form. Controls (stay a way in): the K22 pair. +# K23 begin +for v in starmid starinit starqual starqualmid assignmid assigninit assignqual globalmid globalsinit setattrinit augmid forinit withinit walrusinit defmid classmid; do d="$TMP/c7_$v"; mkdir -p "$d/pkg/x" "$d/pkg/y" "$d/other" + printf 'class Base:\n def render_line(self):\n return 1\n\nclass Other:\n pass\n' > "$d/pkg/x/a.py" + printf 'from pkg.x.a import Base\ndef run(obj):\n return obj.render_line()\n' > "$d/pkg/x/c.py" + printf 'class Base:\n def other(self):\n return 3\n' > "$d/pkg/y/base2.py" + : > "$d/pkg/__init__.py"; : > "$d/pkg/x/__init__.py"; : > "$d/pkg/y/__init__.py" + tgt='def target():\n return 2\n\n' + sub() { printf 'class Sub(%s):\n def render_line(self):\n return target()\n' "$1"; } + hop='from pkg.x.a import Base, Other\n' + case "$v" in + starmid) printf 'from pkg.x.a import Base\nfrom pkg.y.base2 import *\n' > "$d/pkg/x/mid.py" + out="from pkg.x.mid import Base\n\n${tgt}$(sub Base)\n" ;; + starinit) printf 'from pkg.x.a import Base\nfrom pkg.y.base2 import *\n' > "$d/pkg/x/__init__.py" + out="from pkg.x import Base\n\n${tgt}$(sub Base)\n" ;; + starqual) printf 'from pkg.x.a import Base\nfrom pkg.y.base2 import *\n' > "$d/pkg/x/__init__.py" + out="import pkg.x as q\n\n${tgt}$(sub q.Base)\n" ;; + starqualmid) printf 'from pkg.x.a import Base\nfrom pkg.y.base2 import *\n' > "$d/pkg/x/mid.py" + out="import pkg.x.mid as q\n\n${tgt}$(sub q.Base)\n" ;; + assignmid) printf "${hop}Base = Other\n" > "$d/pkg/x/mid.py" + out="from pkg.x.mid import Base\n\n${tgt}$(sub Base)\n" ;; + assigninit) printf "${hop}Base = Other\n" > "$d/pkg/x/__init__.py" + out="from pkg.x import Base\n\n${tgt}$(sub Base)\n" ;; + assignqual) printf "${hop}Base = Other\n" > "$d/pkg/x/__init__.py" + out="import pkg.x as q\n\n${tgt}$(sub q.Base)\n" ;; + globalmid) printf "${hop}def f():\n global Base\n Base = Other\nf()\n" > "$d/pkg/x/mid.py" + out="from pkg.x.mid import Base\n\n${tgt}$(sub Base)\n" ;; + globalsinit) printf "from pkg.y import base2 as _b\nglobals()['a'] = _b\n" > "$d/pkg/x/__init__.py" + out="from pkg.x import a as q\n\n${tgt}$(sub q.Base)\n" ;; + setattrinit) printf "import sys\nfrom pkg.y import base2 as _b\nsetattr(sys.modules[__name__], 'a', _b)\n" > "$d/pkg/x/__init__.py" + out="from pkg.x import a as q\n\n${tgt}$(sub q.Base)\n" ;; + augmid) printf "${hop}Base += Other\n" > "$d/pkg/x/mid.py" + out="from pkg.x.mid import Base\n\n${tgt}$(sub Base)\n" ;; + forinit) printf "${hop}for Base in (Other,):\n pass\n" > "$d/pkg/x/__init__.py" + out="from pkg.x import Base\n\n${tgt}$(sub Base)\n" ;; + withinit) printf "import contextlib\n${hop}with contextlib.nullcontext(Other) as Base:\n pass\n" > "$d/pkg/x/__init__.py" + out="from pkg.x import Base\n\n${tgt}$(sub Base)\n" ;; + walrusinit) printf "${hop}(Base := Other)\n" > "$d/pkg/x/__init__.py" + out="from pkg.x import Base\n\n${tgt}$(sub Base)\n" ;; + defmid) printf "from pkg.x.a import Base\ndef Base():\n pass\n" > "$d/pkg/x/mid.py" + out="from pkg.x.mid import Base\n\n${tgt}$(sub Base)\n" ;; + classmid) printf "from pkg.x.a import Base\nclass Base:\n pass\n" > "$d/pkg/x/mid.py" + out="from pkg.x.mid import Base\n\n${tgt}$(sub Base)\n" ;; + esac + printf "$out" > "$d/other/b.py" +done +# K23 end nsblock(){ grep -o ']*>.*' "$1"; } nshead(){ grep -o ']*>' "$1"; } nsrows(){ grep -o ']*/>' "$1"; } @@ -1211,6 +1263,15 @@ for fx in reexpplain:7:3 reexpmidplain:7:3; do v=${fx%%:*}; rest=${fx#*:}; ln=${ "" done +# K23 (lane ind4b fix 7): a hop file whose binding of the name is not its one unrenamed import ends the chain - at EVERY hop, whatever the shape. +# Every arm is shown + bridged at fix 6's head (a02e8439); the K22 controls above stay a way in. +for v in starmid starinit starqual starqualmid assignmid assigninit assignqual globalmid globalsinit setattrinit augmid forinit withinit walrusinit defmid classmid; do + nsArm "(K23) 2c hop file rebinds the name ($v) - withheld" "$TMP/c7_$v" "run,target" \ + '' \ + "" + brArm "(K23b) 2c hop file rebinds the name ($v): no namesake bridge to it either" "$TMP/c7_$v" "run,target" '' +done + echo "(M) MCP path_between carries the same clause" # Two MCP legend postures (lean-answers): the unchanged assertions run on --mcp-legend=inline, the pre-posture spelling # where every answer carries its root facts on ; a TWIN runs the default session posture, whose answers after the From 96f6189da4fada4c458598d1c5bec0f04d5878da Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I <210578314+joyful-ii-V-I@users.noreply.github.com> Date: Sat, 24 Oct 2026 01:08:16 -0400 Subject: [PATCH 57/77] pathgaps fix 7: markPythonDynamicBinds wraps the rebind table (buildExternalVetoTables line count back to baseline) Co-Authored-By: Claude Sonnet 5.5 --- src/graph.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/graph.h b/src/graph.h index 88d6064d4..5997f83c6 100644 --- a/src/graph.h +++ b/src/graph.h @@ -2176,7 +2176,7 @@ inline bool isPythonSourcePath( std::string_view path ) noexcept // read it as unknown. `sys.modules[__name__].a = x` is not a call and stays a floor. inline constexpr std::string_view kPythonDynamicBind = "__getattr__"; -inline void markPythonDynamicBinds( const IngestResult& ing, HashMap& pythonModuleRebind ) +inline HashMap markPythonDynamicBinds( const IngestResult& ing, HashMap pythonModuleRebind ) { std::string key; for( const Reference& r : ing.references ) @@ -2188,6 +2188,7 @@ inline void markPythonDynamicBinds( const IngestResult& ing, HashMap Date: Sat, 24 Oct 2026 00:52:59 -0400 Subject: [PATCH 58/77] fix(grep): no pattern_meta on subscripts, semver, RxJS $ and footnotes; compact legend ties complete= to the literal reading N2: grepPatternMeta stays quiet for `arr[i-1]` / `a[0][1]` (a `[` right after a word byte or `]`), `^1.2.3` (^ before a digit), a bare trailing `user$` ($ counts only under a leading ^) and `[^1]` (footnote label), so the literal answer keeps its --at / --offset next=. Unambiguous regex (`a|b`, `.*` inside, escapes, `^word`, `^word$`, `[^h]`, `[0-9]`) keeps the meta. N1: the compact grep legend and the pattern_meta dict entry say complete=/hits= speak of the literal reading only. Co-Authored-By: Claude Sonnet 5.5 --- src/compactlegend.h | 4 +-- src/search.h | 61 ++++++++++++++++++++++++++++++++++++------ src/verbs_grep.h | 2 +- test/honestlexcheck.sh | 23 ++++++++++++++++ 4 files changed, 79 insertions(+), 11 deletions(-) diff --git a/src/compactlegend.h b/src/compactlegend.h index 27a3f3431..db532c017 100644 --- a/src/compactlegend.h +++ b/src/compactlegend.h @@ -111,7 +111,7 @@ inline constexpr CompactLegendSpec kCompactLegendSpecs[] = { "verify", "verify", "ONE structured claim, verdict=confirmed|refuted|unknown, its witness rows inline" }, { "query", "graph-query", "graph-query expression over the symbol graph: matching rows" }, { "match", "match", "tree-sitter structural query: captured nodes with their enclosing symbol; grammars=/eligible_files= the scope" }, - { "grep", "grep", "literal/regex scan grouped by file: CDATA text (/ context around it); complete=1 only for an exhaustive literal scan; = off-index" }, + { "grep", "grep", "literal/regex scan grouped by file: CDATA text (/ context around it); complete=1 only for an exhaustive literal scan (beside pattern_meta=, of the literal reading only); = off-index" }, { "exemplar", "exemplar", "the best-in-class instance of kind= for the task, chosen by role: n= p= in= ccx= tested=, to imitate" }, { "batch", "batch", "n= read sub-queries in one sweep (requested=, cap=): each wraps one sub-answer verbatim in CDATA" }, { "task-route", "help-task", "which verb answers this task: status=recommend|abstain confidence= score= margin=, the evidence" }, @@ -962,7 +962,7 @@ inline constexpr CompactCompletenessTerm kCompactAttributeReadings[] = { "string_hits", "string_hits=N: of hits=, string-tier; rows: files named like the pattern, source, then the rest", false, "grep", MapHeaderRead::No, {}, "grep" }, { "ref", "hit ref=: first code site (not a comment/string) naming the hit's callerless enclosing def (where it is registered; one site)", true, "hit", MapHeaderRead::No, {}, "grep" }, { "tier_budget", "tier_budget=: files|bytes cap hit after tier_parsed= of tier_files= hit files; tier counts floors, every row served", false, "grep", MapHeaderRead::No, {}, "grep" }, - { "pattern_meta", "pattern_meta=: regex-shaped pieces of this LITERAL pattern, matched as text; next= is then the regex verb on it", false, "grep", MapHeaderRead::No, {}, "grep" }, + { "pattern_meta", "pattern_meta=: regex-shaped pieces of this LITERAL pattern, matched as text; complete=1 and hits= speak of that literal reading only (a zero may mean a regex was meant); next= is then the regex verb on it", false, "grep", MapHeaderRead::No, {}, "grep" }, { "unindexed_files_scanned", "unindexed_files_scanned=N: off-index text files also scanned; outside complete=", false, "grep", MapHeaderRead::No, {}, "grep" }, { "unindexed_hits", "unindexed_hits=N: hits in off-index files, NOT in hits=/total=; listed in the trailing unindexed element", false, "grep", MapHeaderRead::No, {}, "grep" }, { "callers", "callers=N: 1-hop distinct callers of this name, all same-named defs (a FLOOR)", true, "enc", MapHeaderRead::No, {}, "grep" }, diff --git a/src/search.h b/src/search.h index adbf38cde..3168b714a 100644 --- a/src/search.h +++ b/src/search.h @@ -2539,18 +2539,64 @@ inline std::vector grepEnrichPage( const IngestResult& ing, const GrepC return hits; } +inline bool grepMetaWord( std::string_view pat, std::size_t i ) noexcept +{ + return i < pat.size() && ( std::isalnum( (unsigned char)pat[ i ] ) || pat[ i ] == '_' ); +} + +// Is the `[` at `i` the opening of a bracket CLASS (grepPatternMeta's "[]")? Not a subscript (`[` right after a word byte or +// `]`), and not a footnote label (`[^` + digits + `]`); `[^…` or a `a-z` range inside a closed bracket is. +inline bool grepMetaClassAt( std::string_view pat, std::size_t i ) noexcept +{ + if( pat[ i ] != '[' || i + 2 >= pat.size() || pat.find( ']', i + 2 ) == std::string_view::npos ) + { + return false; + } + if( i > 0 && ( grepMetaWord( pat, i - 1 ) || pat[ i - 1 ] == ']' ) ) + { + return false; + } + if( pat[ i + 1 ] != '^' ) + { + return grepMetaWord( pat, i + 1 ) && pat[ i + 2 ] == '-' && grepMetaWord( pat, i + 3 ); + } + std::size_t end = i + 2; + while( end < pat.size() && std::isdigit( (unsigned char)pat[ end ] ) ) + { + ++end; + } + return !( end > i + 2 && end < pat.size() && pat[ end ] == ']' ); +} + +// A leading ^ before a letter, `_` or # (never a digit: `^1.2.3` is a semver), or a trailing $ after a word byte when the +// pattern also opens with ^ (`^foo$`; a bare `user$` is an RxJS name). +inline bool grepMetaAnchored( std::string_view pat ) noexcept +{ + if( pat.size() < 2 || pat[ 0 ] != '^' ) + { + return false; + } + const bool lead = ( grepMetaWord( pat, 1 ) && !std::isdigit( (unsigned char)pat[ 1 ] ) ) || pat[ 1 ] == '#'; + return lead || ( pat.back() == '$' && grepMetaWord( pat, pat.size() - 2 ) ); +} + // honest-lex GREP-META: the pieces of a LITERAL pattern that read as regex syntax, space-separated in a fixed order, or // "" when none. A literal scan matches them as text, so `compose|HTTPException` answered a confident zero (hono: 31 // files hold either name) and `CHANGELOG\.md` matched only the one line spelling the backslash. The rule, each piece // chosen so a common literal stays quiet: // | between two word bytes (`a|b`, `x)|y`), never `||` or ` | `; \X a backslash before . | ( ) [ ] { } * + ? ^ $ // or b B w W d D s S; .* .+ followed by more pattern (`detail.*Budget`; a trailing `java.util.*` is a literal); -// ^ $ a leading ^ before a word byte or #, a trailing $ after a word byte; [] a bracket class opening `[^` or -// holding a range `a-z`. A lone '.' (`CHANGELOG.md`) is NOT a piece: it is the commonest literal dot. +// ^ $ a leading ^ before a letter, `_` or #, and a trailing $ after a word byte ONLY when the pattern also opens with ^ +// (`^foo$`); [] a bracket class opening `[^` or holding a range `a-z`. A lone '.' (`CHANGELOG.md`) is NOT a +// piece: it is the commonest literal dot. +// The rule is INVERTED toward "unambiguous regex": a meta that would replace next='s --at hop with a regex of a different +// answer is worse than silence, so these ordinary literals stay quiet: a subscript (`[` right after a word byte or `]`: +// `arr[i-1]`, `a[0][1]`), a semver (`^1.2.3`: ^ before a digit), an RxJS suffix (`user$`: a bare trailing $) and a +// markdown footnote (`[^1]`: `[^` + digits + `]`). inline std::string grepPatternMeta( std::string_view pat ) { - const auto word = [ & ]( std::size_t i ) { return i < pat.size() && ( std::isalnum( (unsigned char)pat[ i ] ) || pat[ i ] == '_' ); }; - bool alt = false, esc = false, star = false, anchor = false, cls = false; + const auto word = [ & ]( std::size_t i ) { return grepMetaWord( pat, i ); }; + bool alt = false, esc = false, star = false, cls = false; for( std::size_t i = 0; i < pat.size(); ++i ) { const char c = pat[ i ]; @@ -2567,13 +2613,12 @@ inline std::string grepPatternMeta( std::string_view pat ) { star = true; } - else if( c == '[' && i + 2 < pat.size() && pat.find( ']', i + 2 ) != std::string_view::npos - && ( pat[ i + 1 ] == '^' || ( word( i + 1 ) && pat[ i + 2 ] == '-' && word( i + 3 ) ) ) ) + else { - cls = true; + cls = cls || grepMetaClassAt( pat, i ); } } - anchor = ( pat.size() > 1 && pat[ 0 ] == '^' && ( word( 1 ) || pat[ 1 ] == '#' ) ) || ( pat.size() > 1 && pat.back() == '$' && word( pat.size() - 2 ) ); + const bool anchor = grepMetaAnchored( pat ); std::string out; for( const auto& [ on, piece ] : { std::pair{ alt, "|" }, std::pair{ esc, "\\" }, std::pair{ star, ".*" }, std::pair{ anchor, "^$" }, std::pair{ cls, "[]" } } ) { diff --git a/src/verbs_grep.h b/src/verbs_grep.h index 921cd7f68..6b22f280d 100644 --- a/src/verbs_grep.h +++ b/src/verbs_grep.h @@ -663,7 +663,7 @@ void emitCompactGrepLegend() { rw::emitTo( stdout, "" ); diff --git a/test/honestlexcheck.sh b/test/honestlexcheck.sh index 21be57d50..88e9d7875 100755 --- a/test/honestlexcheck.sh +++ b/test/honestlexcheck.sh @@ -191,6 +191,29 @@ o="$( g --grep='a[0]' | root )" if printf '%s' "$o" | grep -q 'pattern_meta='; then no "B13 an index a[0] read as a class: $o"; else ok "B13 a[0] (an index) is no bracket class"; fi o="$( g --grep='^hlxb' | root )" if printf '%s' "$o" | grep -q 'pattern_meta="^\$"'; then ok "B14 a leading ^ → pattern_meta=^\$"; else no "B14 $o"; fi +# FIX ROUND 2 (review N2): ordinary literals that LOOK like regex syntax must keep their --at hop; unambiguous regex keeps the meta. +printf 'y = hlxidx[i-1] + hlxidx[i-1];\nz = hlxgrid[0][1-2];\nv.hlxuser$.subscribe();\n"hlxver": "^1.2.3"\nsee [^1] for hlxnote\n' > "$R/n.c" +for pair in 'B23 a subscript hlxidx[i-1]|hlxidx[i-1]' 'B24 a chained subscript hlxgrid[0][1-2]|hlxgrid[0][1-2]' 'B25 an RxJS suffix hlxuser$|hlxuser$' \ + 'B26 a semver ^1.2.3|^1.2.3' 'B27 a footnote [^1]|[^1]'; do + lab="${pair%%|*}"; pat="${pair#*|}" + o="$( g --grep="$pat" --grep-in=any | root )" + if printf '%s' "$o" | grep -q 'pattern_meta='; then no "$lab carries a false pattern_meta: $o" + elif printf '%s' "$o" | grep -q 'next="--at='; then ok "$lab: no pattern_meta, keeps next= --at" + else no "$lab: no --at next=: $o"; fi +done +for pair in 'B28 ^\s*int (escape + anchor)|^\s*int ' 'B29 ^hlxb$ (both anchors)|^hlxb$' 'B30 [0-9] (a leading range class)|[0-9]' \ + 'B31 [^0-9] (a negated digit range)|[^0-9]' 'B32 a class right after a word (hlxa[a-z]) reads as a subscript: quiet (known false negative)|hlxa[a-z]'; do + lab="${pair%%|*}"; pat="${pair#*|}" + o="$( g --grep="$pat" --grep-in=any | root )" + case "$lab" in + B32*) if printf '%s' "$o" | grep -q 'pattern_meta='; then no "$lab: $o"; else ok "$lab"; fi ;; + *) if printf '%s' "$o" | grep -q 'pattern_meta='; then ok "$lab keeps pattern_meta"; else no "$lab lost its pattern_meta: $o"; fi ;; + esac +done +# (N1) the compact legend and the dict say complete=/hits= speak of the LITERAL reading only. +if g --grep='hlxa' --legend=compact | grep -q 'of the literal reading only'; then ok "B33 the compact grep legend ties complete= to the literal reading"; else no "B33 compact grep legend lacks the literal-reading clause"; fi +if ( cd "$R" && "$BIN" "$R" --legend-dict 2>/dev/null ) | grep 'pattern_meta=:' | grep -q 'literal reading only'; then ok "B34 the pattern_meta dict entry ties complete=/hits= to the literal reading"; else no "B34 dict entry lacks the clause"; fi + o="$( g --grep='hlxa|hlxb' --legend=full )" if printf '%s' "$o" | grep -q 'pattern_meta= (present only'; then ok "B15 the full legend defines pattern_meta= when it rides"; else no "B15 legend clause missing"; fi o="$( g --grep='hlxa' --legend=full )" From 23d03c6280af6e00cba9e17fa9b3fa313efdf2b6 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I <210578314+joyful-ii-V-I@users.noreply.github.com> Date: Sat, 24 Oct 2026 02:14:16 -0400 Subject: [PATCH 59/77] --lego Rust: a module or directory name of another crate puts no head in the tree; only a module of the using file's own crate does rustOutsideUses skipped a use head that was ANY .rs file stem or directory of the tree, and rustTreeCrate counted the same match as in-tree, so a registry crate named like another member's directory (crates/foo_core beside `foo-core = "1"`) or an unrelated module of another member (app/src/fc.rs beside `fc = { package = "foo-core", version = "1" }`) never reached the Cargo verdict and its implementors were listed with no floor. The crate-aware reader now reads the modules per crate (the nearest Cargo.toml above a file): only a module of the using file's own crate, crate/self/super or a Cargo name proven by a `path =` key puts a head in the tree; every other head goes through the Cargo verdict. FE-A's plain table keeps the tree-wide module set. Six xp/rsmod* arms (M1 and M3 red at c6049866, M2 T1 C5 L2 controls). Co-Authored-By: Claude Sonnet 5.5 --- src/graph.h | 103 ++++++++++++++++-- test/legotransitivecheck.sh | 11 ++ test/legotransitivefix/xp/rsmodc5/Cargo.toml | 2 + .../xp/rsmodc5/crates/app/Cargo.toml | 4 + .../xp/rsmodc5/crates/app/src/lib.rs | 3 + .../xp/rsmodc5/crates/core/Cargo.toml | 2 + .../xp/rsmodc5/crates/core/src/lib.rs | 1 + .../xp/rsmodc5/crates/other/Cargo.toml | 4 + .../xp/rsmodc5/crates/other/src/lib.rs | 3 + test/legotransitivefix/xp/rsmodl2/Cargo.toml | 2 + .../xp/rsmodl2/crates/app/Cargo.toml | 4 + .../xp/rsmodl2/crates/app/src/lib.rs | 3 + .../xp/rsmodl2/crates/core/Cargo.toml | 2 + .../xp/rsmodl2/crates/core/src/lib.rs | 1 + .../xp/rsmodl2/crates/other/Cargo.toml | 5 + .../xp/rsmodl2/crates/other/src/lib.rs | 3 + test/legotransitivefix/xp/rsmodm1/Cargo.toml | 2 + .../xp/rsmodm1/crates/app/Cargo.toml | 4 + .../xp/rsmodm1/crates/app/src/lib.rs | 3 + .../xp/rsmodm1/crates/other/Cargo.toml | 4 + .../xp/rsmodm1/crates/other/src/lib.rs | 3 + .../xp/rsmodm1/crates/xp_m1_core/Cargo.toml | 2 + .../xp/rsmodm1/crates/xp_m1_core/src/lib.rs | 1 + test/legotransitivefix/xp/rsmodm2/Cargo.toml | 2 + .../xp/rsmodm2/crates/app/Cargo.toml | 4 + .../xp/rsmodm2/crates/app/src/lib.rs | 3 + .../xp/rsmodm2/crates/other/Cargo.toml | 4 + .../xp/rsmodm2/crates/other/src/lib.rs | 3 + .../xp/rsmodm2/crates/xp-m2-core/Cargo.toml | 2 + .../xp/rsmodm2/crates/xp-m2-core/src/lib.rs | 1 + test/legotransitivefix/xp/rsmodm3/Cargo.toml | 2 + .../xp/rsmodm3/crates/app/Cargo.toml | 4 + .../xp/rsmodm3/crates/app/src/lib.rs | 3 + .../xp/rsmodm3/crates/app/src/xpm3fc.rs | 1 + .../xp/rsmodm3/crates/core/Cargo.toml | 2 + .../xp/rsmodm3/crates/core/src/lib.rs | 1 + .../xp/rsmodm3/crates/other/Cargo.toml | 4 + .../xp/rsmodm3/crates/other/src/lib.rs | 3 + test/legotransitivefix/xp/rsmodt1/Cargo.toml | 2 + .../xp/rsmodt1/crates/app/Cargo.toml | 4 + .../xp/rsmodt1/crates/app/src/lib.rs | 3 + .../xp/rsmodt1/crates/core/Cargo.toml | 2 + .../xp/rsmodt1/crates/core/src/lib.rs | 1 + .../xp/rsmodt1/crates/other/Cargo.toml | 6 + .../xp/rsmodt1/crates/other/src/lib.rs | 3 + 45 files changed, 221 insertions(+), 11 deletions(-) create mode 100644 test/legotransitivefix/xp/rsmodc5/Cargo.toml create mode 100644 test/legotransitivefix/xp/rsmodc5/crates/app/Cargo.toml create mode 100644 test/legotransitivefix/xp/rsmodc5/crates/app/src/lib.rs create mode 100644 test/legotransitivefix/xp/rsmodc5/crates/core/Cargo.toml create mode 100644 test/legotransitivefix/xp/rsmodc5/crates/core/src/lib.rs create mode 100644 test/legotransitivefix/xp/rsmodc5/crates/other/Cargo.toml create mode 100644 test/legotransitivefix/xp/rsmodc5/crates/other/src/lib.rs create mode 100644 test/legotransitivefix/xp/rsmodl2/Cargo.toml create mode 100644 test/legotransitivefix/xp/rsmodl2/crates/app/Cargo.toml create mode 100644 test/legotransitivefix/xp/rsmodl2/crates/app/src/lib.rs create mode 100644 test/legotransitivefix/xp/rsmodl2/crates/core/Cargo.toml create mode 100644 test/legotransitivefix/xp/rsmodl2/crates/core/src/lib.rs create mode 100644 test/legotransitivefix/xp/rsmodl2/crates/other/Cargo.toml create mode 100644 test/legotransitivefix/xp/rsmodl2/crates/other/src/lib.rs create mode 100644 test/legotransitivefix/xp/rsmodm1/Cargo.toml create mode 100644 test/legotransitivefix/xp/rsmodm1/crates/app/Cargo.toml create mode 100644 test/legotransitivefix/xp/rsmodm1/crates/app/src/lib.rs create mode 100644 test/legotransitivefix/xp/rsmodm1/crates/other/Cargo.toml create mode 100644 test/legotransitivefix/xp/rsmodm1/crates/other/src/lib.rs create mode 100644 test/legotransitivefix/xp/rsmodm1/crates/xp_m1_core/Cargo.toml create mode 100644 test/legotransitivefix/xp/rsmodm1/crates/xp_m1_core/src/lib.rs create mode 100644 test/legotransitivefix/xp/rsmodm2/Cargo.toml create mode 100644 test/legotransitivefix/xp/rsmodm2/crates/app/Cargo.toml create mode 100644 test/legotransitivefix/xp/rsmodm2/crates/app/src/lib.rs create mode 100644 test/legotransitivefix/xp/rsmodm2/crates/other/Cargo.toml create mode 100644 test/legotransitivefix/xp/rsmodm2/crates/other/src/lib.rs create mode 100644 test/legotransitivefix/xp/rsmodm2/crates/xp-m2-core/Cargo.toml create mode 100644 test/legotransitivefix/xp/rsmodm2/crates/xp-m2-core/src/lib.rs create mode 100644 test/legotransitivefix/xp/rsmodm3/Cargo.toml create mode 100644 test/legotransitivefix/xp/rsmodm3/crates/app/Cargo.toml create mode 100644 test/legotransitivefix/xp/rsmodm3/crates/app/src/lib.rs create mode 100644 test/legotransitivefix/xp/rsmodm3/crates/app/src/xpm3fc.rs create mode 100644 test/legotransitivefix/xp/rsmodm3/crates/core/Cargo.toml create mode 100644 test/legotransitivefix/xp/rsmodm3/crates/core/src/lib.rs create mode 100644 test/legotransitivefix/xp/rsmodm3/crates/other/Cargo.toml create mode 100644 test/legotransitivefix/xp/rsmodm3/crates/other/src/lib.rs create mode 100644 test/legotransitivefix/xp/rsmodt1/Cargo.toml create mode 100644 test/legotransitivefix/xp/rsmodt1/crates/app/Cargo.toml create mode 100644 test/legotransitivefix/xp/rsmodt1/crates/app/src/lib.rs create mode 100644 test/legotransitivefix/xp/rsmodt1/crates/core/Cargo.toml create mode 100644 test/legotransitivefix/xp/rsmodt1/crates/core/src/lib.rs create mode 100644 test/legotransitivefix/xp/rsmodt1/crates/other/Cargo.toml create mode 100644 test/legotransitivefix/xp/rsmodt1/crates/other/src/lib.rs diff --git a/src/graph.h b/src/graph.h index ebbe17fc6..73d789af4 100644 --- a/src/graph.h +++ b/src/graph.h @@ -6637,16 +6637,96 @@ inline void rustCrateAliases( std::string_view text, std::vector& modules, const HashMap& cargoNames, std::string_view name ) +// The modules of each CRATE of the tree (a crate = the .rs files whose nearest Cargo.toml above them is the same; a tree with no manifest above +// a file is one crate): every .rs file stem and directory name a .rs path passes through BELOW its crate's directory. A name that is a module +// of ANOTHER crate (or only a directory somewhere else in the tree) is no module of the file's own crate: a `use` of it names a crate, and only +// Cargo can say which (rustTreeCrate, rustOutsideUses). FE-A's plain table keeps rustModuleNames (tree-wide), so its call edges stay as they were. +struct RustCrateModules +{ + std::vector crateOf; // file -> index into `modules` + std::vector> modules; // per crate + + // The modules of the crate file `f` belongs to (none for a file the table never saw). + [[nodiscard]] const HashMap& of( std::uint32_t f ) const + { + static const HashMap none; + return f < crateOf.size() ? modules[ crateOf[ f ] ] : none; + } +}; + +inline RustCrateModules rustCrateModuleNames( const IngestResult& ing ) +{ + RustCrateModules out; + HashMap crateAt; // manifest directory under the root -> crate index + crateAt.try_emplace( std::string(), 0 ); + for( std::uint32_t f = 0; f < ing.files.size(); ++f ) + { + const std::string_view rel = rootRelPath( ing, f ); + if( rel == "Cargo.toml" || rel.ends_with( "/Cargo.toml" ) ) + { + crateAt.try_emplace( std::string( rel.substr( 0, rel.size() == 10 ? 0 : rel.size() - 11 ) ), std::uint32_t( crateAt.size() ) ); + } + } + out.modules.resize( crateAt.size() ); + out.crateOf.assign( ing.files.size(), 0 ); + for( std::uint32_t f = 0; f < ing.files.size(); ++f ) + { + std::string_view path = rootRelPath( ing, f ); + if( !path.ends_with( ".rs" ) ) + { + continue; + } + path.remove_suffix( 3 ); + std::size_t below = 0; // where the crate's own directory ends: the deepest manifest directory above the file + for( std::size_t slash = path.rfind( '/' ); slash != std::string_view::npos && slash > 0; slash = path.rfind( '/', slash - 1 ) ) + { + if( const auto hit = crateAt.find( std::string( path.substr( 0, slash ) ) ); hit != crateAt.end() ) + { + out.crateOf[ f ] = hit->second; + below = slash + 1; + break; + } + } + path.remove_prefix( below ); + HashMap& names = out.modules[ out.crateOf[ f ] ]; + for( std::size_t at = 0; at <= path.size(); ) + { + const std::size_t slash = std::min( path.find( '/', at ), path.size() ); + if( slash > at ) + { + names.try_emplace( std::string( path.substr( at, slash - at ) ), '\0' ); + } + at = slash + 1; + } + } + return out; +} + +// Is `name` a crate of this tree: a module of the using file's OWN crate (`ownModules`: a name that is only some other crate's module or an +// unrelated directory proves nothing, a registry crate of that name is still a registry crate), a Cargo name or dependency key some manifest +// puts in the tree (`path =` proven, cargoCrateNames), or a workspace crate directory of that crate spelled with `-` for the `_` of the code? +inline bool rustTreeCrate( const HashMap& ownModules, const HashMap& cargoNames, std::string_view name ) { std::string spelled( name ); - if( modules.contains( spelled ) || cargoNames.contains( spelled ) ) + if( ownModules.contains( spelled ) || cargoNames.contains( spelled ) ) { return true; } std::replace( spelled.begin(), spelled.end(), '_', '-' ); - return modules.contains( spelled ); + return ownModules.contains( spelled ); +} + +// Does the `use` of file `f` stay inside its own scope, so that it names no outside item and needs no verdict: no path to read, `crate` / +// `self` / `super`, or a head that is a module? The plain table (FE-A) reads a module of ANY crate of the tree; the crate-aware one only a module of +// the file's own crate — a head that is merely another member's directory or module goes through the Cargo verdict like any other. +inline bool rustUseIsLocal( std::string_view path, std::string_view head, bool crateDirAware, std::uint32_t f, const HashMap& treeModules, + const RustCrateModules& crateMods ) +{ + if( path.empty() || head == "crate" || head == "self" || head == "super" ) + { + return true; + } + return ( crateDirAware ? crateMods.of( f ) : treeModules ).contains( std::string( head ) ); } using RustAliasCache = HashMap>>; // per file, read once and only when asked @@ -6691,7 +6771,7 @@ inline std::string_view rustUsePath( const IngestResult& ing, const Include& inc inline char rustHeadVerdict( const IngestResult& ing, std::uint32_t f, std::string_view head, const CargoNames& cargo, RustAliasCache& aliases, const auto& treeCrate ) { - if( treeCrate( head ) ) + if( treeCrate( f, head ) ) { return 'i'; // a crate of this tree under its own name or a Cargo key } @@ -6700,7 +6780,7 @@ inline char rustHeadVerdict( const IngestResult& ing, std::uint32_t f, std::stri return '\0'; } const std::string_view target = rustFileAlias( ing, f, head, aliases ); // `extern crate X as head;` / `use X as head;` - if( !target.empty() && treeCrate( target ) ) + if( !target.empty() && treeCrate( f, target ) ) { return 'i'; } @@ -6709,7 +6789,7 @@ inline char rustHeadVerdict( const IngestResult& ing, std::uint32_t f, std::stri // Rust: `use a::b::c;` whose first segment is not crate/self/super/a module of this tree names an outside item `c`, into // `out` ("#c"). `crateDirAware` (lane fc2-lego-extparent's base clause reader) decides it differently and in three ways: a head that is a -// crate of this tree (a module, a workspace crate directory spelled with `_` for `-`, a Cargo package or [lib] name or a +// crate of this tree (a module of the using file's OWN crate, a workspace crate directory of it spelled with `_` for `-`, a Cargo package or [lib] name or a // dependency key naming one, a leading `::` stripped, or a head the file itself aliases to such a crate) is in the tree and has no // row; a head that is std/core/alloc/proc_macro/test or declared as a registry or git dependency (rustDecidedOutside, also through a // file's own alias of one) is DECIDED outside ('\0'); every other head is UNDECIDED (kRustUseUndecided): it is not read as in or out. @@ -6721,15 +6801,16 @@ inline void rustOutsideUses( const IngestResult& ing, bool crateDirAware, HashMa { return; // no Rust in the tree } - const CargoNames cargo = crateDirAware ? cargoCrateNames( ing ) : CargoNames{}; - const auto treeCrate = [ & ]( std::string_view name ) - { return name == "crate" || name == "self" || name == "super" || rustTreeCrate( modules, cargo.inTree, name ); }; + const CargoNames cargo = crateDirAware ? cargoCrateNames( ing ) : CargoNames{}; + const RustCrateModules crateMods = crateDirAware ? rustCrateModuleNames( ing ) : RustCrateModules{}; + const auto treeCrate = [ & ]( std::uint32_t f, std::string_view name ) + { return name == "crate" || name == "self" || name == "super" || rustTreeCrate( crateMods.of( f ), cargo.inTree, name ); }; RustAliasCache aliases; for( const Include& inc : ing.includes ) { const std::string_view path = rustUsePath( ing, inc, crateDirAware ); const std::string_view head = path.substr( 0, path.find( "::" ) ); - if( path.empty() || head == "crate" || head == "self" || head == "super" || modules.contains( std::string( head ) ) ) + if( rustUseIsLocal( path, head, crateDirAware, inc.fileId, modules, crateMods ) ) { continue; } diff --git a/test/legotransitivecheck.sh b/test/legotransitivecheck.sh index 92a3217d5..bc548f586 100755 --- a/test/legotransitivecheck.sh +++ b/test/legotransitivecheck.sh @@ -434,6 +434,17 @@ xpfloor XpPkgBase "XpPkgReal XpPkgOutReg" xpfloor XpNpBase "XpNpOutReg XpNpOutEntry" xpfloor XpMlBase "XpMlOutReg" xpfloor XpTtBase "XpTtOutReg" +# fix round 7 (review rv-fc2-delta6 E1: no head is in the tree by a MODULE or DIRECTORY name match either — only a module of the using file's OWN +# crate, `crate`/`self`/`super`, or a `path =` key): a registry crate whose name is only some other member's directory (M1: crates/xp_m1_core +# beside `xp-m1-core = "1"`) or an unrelated module of another member (M3: app's xpm3fc.rs beside `xpm3fc = { package = "xp-m3-core", version = "1" }`) +# is floored like the same shape without the name match. Controls: M2 (a dashed directory + a registry rename), T1 (one manifest, two tables), C5 +# (no name match), L2 (a two-line rename) keep their floor (M2, L2 and M3 also their Real implementor). +for k in m1 m2 m3 t1 c5 l2 +do + K="$( printf '%s' "$k" | tr 'a-z' 'A-Z' )" + xpfloor "XpMd${K}Base" "XpMd${K}OutReg" + case "$k" in m2|m3|l2) xp "XpMd${K}Base" "" "XpMd${K}Real" ;; esac # m1/t1/c5: the key is two-meaning for the app member too, so its Real row floors with the rest +done xp XpKeyBothBase "" "XpKeyBothA XpKeyBothB" xp XpKeyCtlBase "XpKeyCtlOut" "" if lego XpKeyCtlBase && iface | grep -q 'implementors_floor'; then no "XpKeyCtlBase carries a floor for a key every member declares as a registry crate: $( iface )"; else ok "XpKeyCtlBase: a key registry in every member stays decided (no floor)"; fi diff --git a/test/legotransitivefix/xp/rsmodc5/Cargo.toml b/test/legotransitivefix/xp/rsmodc5/Cargo.toml new file mode 100644 index 000000000..e7165ae28 --- /dev/null +++ b/test/legotransitivefix/xp/rsmodc5/Cargo.toml @@ -0,0 +1,2 @@ +[workspace] +members = ["crates/core", "crates/app", "crates/other"] diff --git a/test/legotransitivefix/xp/rsmodc5/crates/app/Cargo.toml b/test/legotransitivefix/xp/rsmodc5/crates/app/Cargo.toml new file mode 100644 index 000000000..6c99aeae5 --- /dev/null +++ b/test/legotransitivefix/xp/rsmodc5/crates/app/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "app" +[dependencies] +xp-c5-core = { path = "../core" } diff --git a/test/legotransitivefix/xp/rsmodc5/crates/app/src/lib.rs b/test/legotransitivefix/xp/rsmodc5/crates/app/src/lib.rs new file mode 100644 index 000000000..ac0838f31 --- /dev/null +++ b/test/legotransitivefix/xp/rsmodc5/crates/app/src/lib.rs @@ -0,0 +1,3 @@ +use xp_c5_core::XpMdC5Base; +pub struct XpMdC5Real; +impl XpMdC5Base for XpMdC5Real {} diff --git a/test/legotransitivefix/xp/rsmodc5/crates/core/Cargo.toml b/test/legotransitivefix/xp/rsmodc5/crates/core/Cargo.toml new file mode 100644 index 000000000..5f06d4b7b --- /dev/null +++ b/test/legotransitivefix/xp/rsmodc5/crates/core/Cargo.toml @@ -0,0 +1,2 @@ +[package] +name = "xp-c5-core" diff --git a/test/legotransitivefix/xp/rsmodc5/crates/core/src/lib.rs b/test/legotransitivefix/xp/rsmodc5/crates/core/src/lib.rs new file mode 100644 index 000000000..5d08ff5d9 --- /dev/null +++ b/test/legotransitivefix/xp/rsmodc5/crates/core/src/lib.rs @@ -0,0 +1 @@ +pub trait XpMdC5Base {} diff --git a/test/legotransitivefix/xp/rsmodc5/crates/other/Cargo.toml b/test/legotransitivefix/xp/rsmodc5/crates/other/Cargo.toml new file mode 100644 index 000000000..938cb6b9a --- /dev/null +++ b/test/legotransitivefix/xp/rsmodc5/crates/other/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "other" +[dependencies] +xp-c5-core = "0.9" diff --git a/test/legotransitivefix/xp/rsmodc5/crates/other/src/lib.rs b/test/legotransitivefix/xp/rsmodc5/crates/other/src/lib.rs new file mode 100644 index 000000000..8828f3abe --- /dev/null +++ b/test/legotransitivefix/xp/rsmodc5/crates/other/src/lib.rs @@ -0,0 +1,3 @@ +use xp_c5_core::XpMdC5Base; +pub struct XpMdC5OutReg; +impl XpMdC5Base for XpMdC5OutReg {} diff --git a/test/legotransitivefix/xp/rsmodl2/Cargo.toml b/test/legotransitivefix/xp/rsmodl2/Cargo.toml new file mode 100644 index 000000000..e7165ae28 --- /dev/null +++ b/test/legotransitivefix/xp/rsmodl2/Cargo.toml @@ -0,0 +1,2 @@ +[workspace] +members = ["crates/core", "crates/app", "crates/other"] diff --git a/test/legotransitivefix/xp/rsmodl2/crates/app/Cargo.toml b/test/legotransitivefix/xp/rsmodl2/crates/app/Cargo.toml new file mode 100644 index 000000000..47693d4a0 --- /dev/null +++ b/test/legotransitivefix/xp/rsmodl2/crates/app/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "app" +[dependencies] +xp-l2-core = { path = "../core" } diff --git a/test/legotransitivefix/xp/rsmodl2/crates/app/src/lib.rs b/test/legotransitivefix/xp/rsmodl2/crates/app/src/lib.rs new file mode 100644 index 000000000..c70f4fbb6 --- /dev/null +++ b/test/legotransitivefix/xp/rsmodl2/crates/app/src/lib.rs @@ -0,0 +1,3 @@ +use xp_l2_core::XpMdL2Base; +pub struct XpMdL2Real; +impl XpMdL2Base for XpMdL2Real {} diff --git a/test/legotransitivefix/xp/rsmodl2/crates/core/Cargo.toml b/test/legotransitivefix/xp/rsmodl2/crates/core/Cargo.toml new file mode 100644 index 000000000..04dc18db7 --- /dev/null +++ b/test/legotransitivefix/xp/rsmodl2/crates/core/Cargo.toml @@ -0,0 +1,2 @@ +[package] +name = "xp-l2-core" diff --git a/test/legotransitivefix/xp/rsmodl2/crates/core/src/lib.rs b/test/legotransitivefix/xp/rsmodl2/crates/core/src/lib.rs new file mode 100644 index 000000000..e182d61f2 --- /dev/null +++ b/test/legotransitivefix/xp/rsmodl2/crates/core/src/lib.rs @@ -0,0 +1 @@ +pub trait XpMdL2Base {} diff --git a/test/legotransitivefix/xp/rsmodl2/crates/other/Cargo.toml b/test/legotransitivefix/xp/rsmodl2/crates/other/Cargo.toml new file mode 100644 index 000000000..e9e93495b --- /dev/null +++ b/test/legotransitivefix/xp/rsmodl2/crates/other/Cargo.toml @@ -0,0 +1,5 @@ +[package] +name = "other" +[dependencies] +xpl2fc = { package = "xp-l2-core", + version = "1" } diff --git a/test/legotransitivefix/xp/rsmodl2/crates/other/src/lib.rs b/test/legotransitivefix/xp/rsmodl2/crates/other/src/lib.rs new file mode 100644 index 000000000..f12e3fadb --- /dev/null +++ b/test/legotransitivefix/xp/rsmodl2/crates/other/src/lib.rs @@ -0,0 +1,3 @@ +use xpl2fc::XpMdL2Base; +pub struct XpMdL2OutReg; +impl XpMdL2Base for XpMdL2OutReg {} diff --git a/test/legotransitivefix/xp/rsmodm1/Cargo.toml b/test/legotransitivefix/xp/rsmodm1/Cargo.toml new file mode 100644 index 000000000..683ca6aec --- /dev/null +++ b/test/legotransitivefix/xp/rsmodm1/Cargo.toml @@ -0,0 +1,2 @@ +[workspace] +members = ["crates/xp_m1_core", "crates/app", "crates/other"] diff --git a/test/legotransitivefix/xp/rsmodm1/crates/app/Cargo.toml b/test/legotransitivefix/xp/rsmodm1/crates/app/Cargo.toml new file mode 100644 index 000000000..cd227f344 --- /dev/null +++ b/test/legotransitivefix/xp/rsmodm1/crates/app/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "app" +[dependencies] +xp-m1-core = { path = "../xp_m1_core" } diff --git a/test/legotransitivefix/xp/rsmodm1/crates/app/src/lib.rs b/test/legotransitivefix/xp/rsmodm1/crates/app/src/lib.rs new file mode 100644 index 000000000..6d4dcb9c6 --- /dev/null +++ b/test/legotransitivefix/xp/rsmodm1/crates/app/src/lib.rs @@ -0,0 +1,3 @@ +use xp_m1_core::XpMdM1Base; +pub struct XpMdM1Real; +impl XpMdM1Base for XpMdM1Real {} diff --git a/test/legotransitivefix/xp/rsmodm1/crates/other/Cargo.toml b/test/legotransitivefix/xp/rsmodm1/crates/other/Cargo.toml new file mode 100644 index 000000000..9f80b2e1c --- /dev/null +++ b/test/legotransitivefix/xp/rsmodm1/crates/other/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "other" +[dependencies] +xp-m1-core = "1" diff --git a/test/legotransitivefix/xp/rsmodm1/crates/other/src/lib.rs b/test/legotransitivefix/xp/rsmodm1/crates/other/src/lib.rs new file mode 100644 index 000000000..8953758ac --- /dev/null +++ b/test/legotransitivefix/xp/rsmodm1/crates/other/src/lib.rs @@ -0,0 +1,3 @@ +use xp_m1_core::XpMdM1Base; +pub struct XpMdM1OutReg; +impl XpMdM1Base for XpMdM1OutReg {} diff --git a/test/legotransitivefix/xp/rsmodm1/crates/xp_m1_core/Cargo.toml b/test/legotransitivefix/xp/rsmodm1/crates/xp_m1_core/Cargo.toml new file mode 100644 index 000000000..33be777a6 --- /dev/null +++ b/test/legotransitivefix/xp/rsmodm1/crates/xp_m1_core/Cargo.toml @@ -0,0 +1,2 @@ +[package] +name = "xp-m1-core" diff --git a/test/legotransitivefix/xp/rsmodm1/crates/xp_m1_core/src/lib.rs b/test/legotransitivefix/xp/rsmodm1/crates/xp_m1_core/src/lib.rs new file mode 100644 index 000000000..92895cb50 --- /dev/null +++ b/test/legotransitivefix/xp/rsmodm1/crates/xp_m1_core/src/lib.rs @@ -0,0 +1 @@ +pub trait XpMdM1Base {} diff --git a/test/legotransitivefix/xp/rsmodm2/Cargo.toml b/test/legotransitivefix/xp/rsmodm2/Cargo.toml new file mode 100644 index 000000000..cb4367cc2 --- /dev/null +++ b/test/legotransitivefix/xp/rsmodm2/Cargo.toml @@ -0,0 +1,2 @@ +[workspace] +members = ["crates/xp-m2-core", "crates/app", "crates/other"] diff --git a/test/legotransitivefix/xp/rsmodm2/crates/app/Cargo.toml b/test/legotransitivefix/xp/rsmodm2/crates/app/Cargo.toml new file mode 100644 index 000000000..fd32b7816 --- /dev/null +++ b/test/legotransitivefix/xp/rsmodm2/crates/app/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "app" +[dependencies] +xp-m2-core = { path = "../xp-m2-core" } diff --git a/test/legotransitivefix/xp/rsmodm2/crates/app/src/lib.rs b/test/legotransitivefix/xp/rsmodm2/crates/app/src/lib.rs new file mode 100644 index 000000000..c96445710 --- /dev/null +++ b/test/legotransitivefix/xp/rsmodm2/crates/app/src/lib.rs @@ -0,0 +1,3 @@ +use xp_m2_core::XpMdM2Base; +pub struct XpMdM2Real; +impl XpMdM2Base for XpMdM2Real {} diff --git a/test/legotransitivefix/xp/rsmodm2/crates/other/Cargo.toml b/test/legotransitivefix/xp/rsmodm2/crates/other/Cargo.toml new file mode 100644 index 000000000..e525ccaf5 --- /dev/null +++ b/test/legotransitivefix/xp/rsmodm2/crates/other/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "other" +[dependencies] +xpm2fc = { package = "xp-m2-core", version = "1" } diff --git a/test/legotransitivefix/xp/rsmodm2/crates/other/src/lib.rs b/test/legotransitivefix/xp/rsmodm2/crates/other/src/lib.rs new file mode 100644 index 000000000..520f2e3cf --- /dev/null +++ b/test/legotransitivefix/xp/rsmodm2/crates/other/src/lib.rs @@ -0,0 +1,3 @@ +use xpm2fc::XpMdM2Base; +pub struct XpMdM2OutReg; +impl XpMdM2Base for XpMdM2OutReg {} diff --git a/test/legotransitivefix/xp/rsmodm2/crates/xp-m2-core/Cargo.toml b/test/legotransitivefix/xp/rsmodm2/crates/xp-m2-core/Cargo.toml new file mode 100644 index 000000000..4473cb9b3 --- /dev/null +++ b/test/legotransitivefix/xp/rsmodm2/crates/xp-m2-core/Cargo.toml @@ -0,0 +1,2 @@ +[package] +name = "xp-m2-core" diff --git a/test/legotransitivefix/xp/rsmodm2/crates/xp-m2-core/src/lib.rs b/test/legotransitivefix/xp/rsmodm2/crates/xp-m2-core/src/lib.rs new file mode 100644 index 000000000..32b82076a --- /dev/null +++ b/test/legotransitivefix/xp/rsmodm2/crates/xp-m2-core/src/lib.rs @@ -0,0 +1 @@ +pub trait XpMdM2Base {} diff --git a/test/legotransitivefix/xp/rsmodm3/Cargo.toml b/test/legotransitivefix/xp/rsmodm3/Cargo.toml new file mode 100644 index 000000000..e7165ae28 --- /dev/null +++ b/test/legotransitivefix/xp/rsmodm3/Cargo.toml @@ -0,0 +1,2 @@ +[workspace] +members = ["crates/core", "crates/app", "crates/other"] diff --git a/test/legotransitivefix/xp/rsmodm3/crates/app/Cargo.toml b/test/legotransitivefix/xp/rsmodm3/crates/app/Cargo.toml new file mode 100644 index 000000000..ea60ecee8 --- /dev/null +++ b/test/legotransitivefix/xp/rsmodm3/crates/app/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "app" +[dependencies] +xp-m3-core = { path = "../core" } diff --git a/test/legotransitivefix/xp/rsmodm3/crates/app/src/lib.rs b/test/legotransitivefix/xp/rsmodm3/crates/app/src/lib.rs new file mode 100644 index 000000000..e7474c58a --- /dev/null +++ b/test/legotransitivefix/xp/rsmodm3/crates/app/src/lib.rs @@ -0,0 +1,3 @@ +use xp_m3_core::XpMdM3Base; +pub struct XpMdM3Real; +impl XpMdM3Base for XpMdM3Real {} diff --git a/test/legotransitivefix/xp/rsmodm3/crates/app/src/xpm3fc.rs b/test/legotransitivefix/xp/rsmodm3/crates/app/src/xpm3fc.rs new file mode 100644 index 000000000..4f11053d1 --- /dev/null +++ b/test/legotransitivefix/xp/rsmodm3/crates/app/src/xpm3fc.rs @@ -0,0 +1 @@ +pub fn f(){} diff --git a/test/legotransitivefix/xp/rsmodm3/crates/core/Cargo.toml b/test/legotransitivefix/xp/rsmodm3/crates/core/Cargo.toml new file mode 100644 index 000000000..6ac98e4a4 --- /dev/null +++ b/test/legotransitivefix/xp/rsmodm3/crates/core/Cargo.toml @@ -0,0 +1,2 @@ +[package] +name = "xp-m3-core" diff --git a/test/legotransitivefix/xp/rsmodm3/crates/core/src/lib.rs b/test/legotransitivefix/xp/rsmodm3/crates/core/src/lib.rs new file mode 100644 index 000000000..e9786006d --- /dev/null +++ b/test/legotransitivefix/xp/rsmodm3/crates/core/src/lib.rs @@ -0,0 +1 @@ +pub trait XpMdM3Base {} diff --git a/test/legotransitivefix/xp/rsmodm3/crates/other/Cargo.toml b/test/legotransitivefix/xp/rsmodm3/crates/other/Cargo.toml new file mode 100644 index 000000000..bce20f62d --- /dev/null +++ b/test/legotransitivefix/xp/rsmodm3/crates/other/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "other" +[dependencies] +xpm3fc = { package = "xp-m3-core", version = "1" } diff --git a/test/legotransitivefix/xp/rsmodm3/crates/other/src/lib.rs b/test/legotransitivefix/xp/rsmodm3/crates/other/src/lib.rs new file mode 100644 index 000000000..d0ae19032 --- /dev/null +++ b/test/legotransitivefix/xp/rsmodm3/crates/other/src/lib.rs @@ -0,0 +1,3 @@ +use xpm3fc::XpMdM3Base; +pub struct XpMdM3OutReg; +impl XpMdM3Base for XpMdM3OutReg {} diff --git a/test/legotransitivefix/xp/rsmodt1/Cargo.toml b/test/legotransitivefix/xp/rsmodt1/Cargo.toml new file mode 100644 index 000000000..e7165ae28 --- /dev/null +++ b/test/legotransitivefix/xp/rsmodt1/Cargo.toml @@ -0,0 +1,2 @@ +[workspace] +members = ["crates/core", "crates/app", "crates/other"] diff --git a/test/legotransitivefix/xp/rsmodt1/crates/app/Cargo.toml b/test/legotransitivefix/xp/rsmodt1/crates/app/Cargo.toml new file mode 100644 index 000000000..8ede808c0 --- /dev/null +++ b/test/legotransitivefix/xp/rsmodt1/crates/app/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "app" +[dependencies] +xp-t1-core = { path = "../core" } diff --git a/test/legotransitivefix/xp/rsmodt1/crates/app/src/lib.rs b/test/legotransitivefix/xp/rsmodt1/crates/app/src/lib.rs new file mode 100644 index 000000000..78a29ba5d --- /dev/null +++ b/test/legotransitivefix/xp/rsmodt1/crates/app/src/lib.rs @@ -0,0 +1,3 @@ +use xp_t1_core::XpMdT1Base; +pub struct XpMdT1Real; +impl XpMdT1Base for XpMdT1Real {} diff --git a/test/legotransitivefix/xp/rsmodt1/crates/core/Cargo.toml b/test/legotransitivefix/xp/rsmodt1/crates/core/Cargo.toml new file mode 100644 index 000000000..a43e8ea57 --- /dev/null +++ b/test/legotransitivefix/xp/rsmodt1/crates/core/Cargo.toml @@ -0,0 +1,2 @@ +[package] +name = "xp-t1-core" diff --git a/test/legotransitivefix/xp/rsmodt1/crates/core/src/lib.rs b/test/legotransitivefix/xp/rsmodt1/crates/core/src/lib.rs new file mode 100644 index 000000000..188d59355 --- /dev/null +++ b/test/legotransitivefix/xp/rsmodt1/crates/core/src/lib.rs @@ -0,0 +1 @@ +pub trait XpMdT1Base {} diff --git a/test/legotransitivefix/xp/rsmodt1/crates/other/Cargo.toml b/test/legotransitivefix/xp/rsmodt1/crates/other/Cargo.toml new file mode 100644 index 000000000..458240ce1 --- /dev/null +++ b/test/legotransitivefix/xp/rsmodt1/crates/other/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "other" +[dependencies] +xp-t1-core = "1" +[target.'cfg(unix)'.dependencies] +xp-t1-core = { path = "../core" } diff --git a/test/legotransitivefix/xp/rsmodt1/crates/other/src/lib.rs b/test/legotransitivefix/xp/rsmodt1/crates/other/src/lib.rs new file mode 100644 index 000000000..a01abfa10 --- /dev/null +++ b/test/legotransitivefix/xp/rsmodt1/crates/other/src/lib.rs @@ -0,0 +1,3 @@ +use xp_t1_core::XpMdT1Base; +pub struct XpMdT1OutReg; +impl XpMdT1Base for XpMdT1OutReg {} From 111655a299e4c99e8c58a02cec258e0a6eac470b Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I <210578314+joyful-ii-V-I@users.noreply.github.com> Date: Sat, 24 Oct 2026 06:05:30 -0400 Subject: [PATCH 60/77] --lego Rust: build the crate-aware use table from the tree, not only when FE-A's plain table is non-empty FE-A's plain table skips every head that is a module anywhere in the tree, so a tree whose every outside-looking head matches another member's directory (a standalone M1: crates/foo_core beside a registry `foo-core = "1"`) had an empty plain table and the lego reader never ran: no Cargo verdict, no floor. rustOutsideUses(crateDirAware) now always runs (it returns at once for a tree with no Rust). Standalone arms for the six rsmod* fixtures; CHANGELOG sentence for the own-crate module rule. Co-Authored-By: Claude Sonnet 5.5 --- CHANGELOG.md | 5 +++-- src/graph.h | 10 +++++----- test/legotransitivecheck.sh | 14 ++++++++++++++ 3 files changed, 22 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bb7f4a844..1cf1fc763 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,8 +25,9 @@ not published here — see `docs/EVALS.md` for the instruments behind the headli a registry or git dependency that some Cargo.toml of the tree declares (any dependency table, `[workspace.dependencies]` included; the key and its `package =` name; a `path =` crate or a key `[patch]`/`[replace]` names is not one), also through an alias the file itself spells (`use serde as sd;`, `use std::io::{Write as _};` moves no other head). A head that is a - crate of the tree keeps its row: a Cargo `[package]`/`[lib]` name from its tests/, examples/ and main.rs, a workspace crate - spelled with `_` for its `-` directory, a dependency key with `path =` under the indexed root (only a path puts a key in the tree) + crate of the tree keeps its row: a Cargo `[package]`/`[lib]` name from its tests/, examples/ and main.rs, a module of the + using file's OWN crate (a workspace crate directory of it spelled with `_` for its `-` directory; a name that is only another + member's directory or module, `crates/foo_core/` beside a registry `foo-core = "1"`, is no module of this file's crate), a dependency key with `path =` under the indexed root (only a path puts a key in the tree) (`core2 = { package = "foo-core", path = "../core" }`, also a `[dependencies.core2]` table or a dotted key), a leading `::`, an `extern crate foo_core as fc;` or `use foo_core as fc;` the file spells. EVERY other head — an unknown crate, an alias or re-export this scan did not see (`pub extern crate foo_core as fc;` in lib.rs, `use fc::T` from another file), a rename only diff --git a/src/graph.h b/src/graph.h index 73d789af4..e305365bf 100644 --- a/src/graph.h +++ b/src/graph.h @@ -10705,11 +10705,11 @@ inline Graph buildGraph( const IngestResult& ing, const ScipOverlay* scip = null std::vector baseCand; // one inheritance reference's base candidates, reused across references std::vector baseProven; // … and the one of them the language's lookup proves (provenBasesOf), reused std::vector boundEdges; // every edge drawn below with how it was bound; merged after the dedup - HashMap rustOutsideBase; // fc2-lego-extparent: built only when FE-A saw an outside `use` at all - if( !falseEdges.rustOutsideUse.empty() ) - { - rustOutsideUses( ing, /*crateDirAware=*/true, rustOutsideBase ); - } + // fc2-lego-extparent: the crate-aware table, read from the tree itself (it returns at once for a tree with no Rust). NOT gated on FE-A's + // plain table, which skips every head that is a module ANYWHERE in the tree: a tree whose every outside-looking head matches some other + // member's directory would otherwise have no table at all, and no Cargo verdict would ever run. + HashMap rustOutsideBase; + rustOutsideUses( ing, /*crateDirAware=*/true, rustOutsideBase ); HashMap selfImport = jsImports.targets.empty() ? HashMap{} : jsSelfPackageImports( ing ); for( auto& [ k, v ] : pythonRootPackageImports( ing ) ) { diff --git a/test/legotransitivecheck.sh b/test/legotransitivecheck.sh index bc548f586..a87cd09e2 100755 --- a/test/legotransitivecheck.sh +++ b/test/legotransitivecheck.sh @@ -445,6 +445,20 @@ do xpfloor "XpMd${K}Base" "XpMd${K}OutReg" case "$k" in m2|m3|l2) xp "XpMd${K}Base" "" "XpMd${K}Real" ;; esac # m1/t1/c5: the key is two-meaning for the app member too, so its Real row floors with the rest done +# the same six shapes as their OWN root (the review's fixtures are standalone): M1's every outside-looking head matches another member's directory, +# so the plain FE-A table is empty there and the crate-aware reader must still run +for k in m1 m2 m3 t1 c5 l2 +do + K="$( printf '%s' "$k" | tr 'a-z' 'A-Z' )" + if "$BIN" "$FIX/xp/rsmod$k" --no-cache --lego="XpMd${K}Base" >"$DIR/out" 2>"$DIR/err" + then + OUT="$( sed 's/>\n Date: Sat, 24 Oct 2026 07:10:06 -0400 Subject: [PATCH 61/77] test(compactlegendcheck): re-pin ripwire.grep/v1 1380 -> 1432 (measured 1420) The compact grep verb row now says complete=/hits= speak of the literal reading only (+52 B, honest-lex review N1); the (U) universe arm measured 1420 B. Co-Authored-By: Claude Sonnet 5.5 --- test/compactlegendcheck.sh | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/compactlegendcheck.sh b/test/compactlegendcheck.sh index 50b9d3491..71000034f 100755 --- a/test/compactlegendcheck.sh +++ b/test/compactlegendcheck.sh @@ -689,7 +689,7 @@ ripwire.affected/v1 840 826 ripwire.verify/v1 440 421 ripwire.help-task/v1 440 429 ripwire.query/v1 790 775 -ripwire.grep/v1 1380 1368 +ripwire.grep/v1 1432 1420 ripwire.match/v1 370 355 ripwire.lego/v1 290 275 ripwire.exemplar/v1 440 422 @@ -1715,6 +1715,8 @@ grep -q '^__ERROR__' "$TMP/m.bad" && ok "(M) MCP edit_check legend:\"terse\" is # RE-PINNED 2026-09-19 (the L1 fix round, rv-r1-L1 HIGH-1: every emitted attribute defined — the (U) pins' note): uses 290 -> 510 # (measured 500), path_between 280 -> 430 (419), exemplar 260 -> 350 (331); impact and lego did not cross their pins. # Fix round 2: path_between 430 -> 540 (measured 535 — the no-path hint= reading). +# RE-PINNED 2026-10-24 (honest-lex fix 2): grep/v1 1380 -> 1432 (measured 1420; 1368 on the base binary) -- the +52 B clause +# "(beside pattern_meta=, of the literal reading only)" that says complete=/hits= speak of the literal reading (review N1). # RE-PINNED 2026-10-18 (lane ix2-slice): path_between 540 -> 860 — the same / readings the (U) table's # ripwire.path/v1 row states (one emitter, src/pathgaps.h pathNearSinkXml); the (U-twin-path) arm keeps the old 550 B CLI pin. # RE-PINNED 2026-09-23 (cut-fix C): impact 780 -> 810 (measured 797; 777 on the base binary) — the same +20 B From 52cafcb7f79f63aacbeb692cfe2cf225540fcaa1 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I <210578314+joyful-ii-V-I@users.noreply.github.com> Date: Sat, 24 Oct 2026 07:43:23 -0400 Subject: [PATCH 62/77] lean2b fix round 1: gateexitcheck spelling, tools/list ceiling, MCP rows_next spelling - staleackcheck mcpfold: if/else verdict form (gateexitcheck G2) - show_stale schema row shortened and quality_delta description trimmed: tools/list 47,539 B -> 47,420 B (ceiling unchanged) - MCP edit_check rows_next= names the MCP call (edit_check symbol=SYM limit=N); CLI unchanged - changelog fragment: measured 567 B Co-Authored-By: Claude Sonnet 5.5 --- changelog.d/mcp-fold.md | 4 ++-- src/editcheck.h | 6 ++++-- src/mcp.h | 2 +- src/mcprefusal.h | 2 +- src/mcpverbs.h | 4 ++-- test/editcheckcheck.sh | 6 +++--- test/staleackcheck.sh | 3 ++- 7 files changed, 15 insertions(+), 12 deletions(-) diff --git a/changelog.d/mcp-fold.md b/changelog.d/mcp-fold.md index c0e8d30ab..6c5a71b3c 100644 --- a/changelog.d/mcp-fold.md +++ b/changelog.d/mcp-fold.md @@ -3,10 +3,10 @@ - MCP `quality_delta`: a regression-free answer with stale acks now drops its `sa` rows and carries `stale_by_kind` (the stale count split by ack kind, the same string the CLI prints) and `stale_next` (`show_stale=true`), when that saves at least a quarter of the bytes. Every count stays on the answer. The new boolean argument `show_stale=true` (the CLI - `--show-stale` twin) returns the full document unchanged. Measured on ripwire's own tree: 15,015 B to 580 B; on the eight + `--show-stale` twin) returns the full document unchanged. Measured on ripwire's own tree: 15,015 B to 567 B; on the eight other sweep repos the answer has no stale acks and is byte-identical. - MCP `edit_check`: an unchanged contract at git HEAD with no flagged caller answers the verdict and `rows_next=` (the - `limit` that lists the callers), as `--edit-check` already does; `callers=` keeps the count. Median 1.72x smaller on the + MCP call, `edit_check symbol=SYM limit=N`, that lists the callers), as `--edit-check` already does; `callers=` keeps the count. Median 1.72x smaller on the repos where it applies (1.62x to 3.46x). A dirty tree, a flagged caller, `new_body` previews and an explicit `limit` or `offset` still list every row. - It does NOT drop a regression row, a flagged caller or any count: the folded rows are one argument away. diff --git a/src/editcheck.h b/src/editcheck.h index b31b373e3..01901db3d 100644 --- a/src/editcheck.h +++ b/src/editcheck.h @@ -1288,6 +1288,7 @@ struct EditCheckPage int offset = 0; bool verdictOnly = false; std::string_view selector; + bool mcpSpelling = false; // lean2b fix 1 (B3): rows_next= names the MCP call (symbol=SYM limit=N), not the CLI flags }; // idea #11: the facts the VERDICT-ONLY answer is decided on, gathered by editCheckBundleText once every verdict number is @@ -1351,8 +1352,9 @@ inline std::string editCheckVerdictOnlyText( const EditCheckVerdictFacts& f, con } std::vector esc; const auto ex = [ & ]( std::string_view s ) -> std::string { return std::string( escapeXml( s, esc ) ); }; - const std::string rowsNext = nextFlag( "--edit-check=", f.page.selector.empty() ? std::string_view( fsym.name ) : f.page.selector ) - + " --limit=" + std::to_string( f.callers ); + const std::string_view rowsSel = f.page.selector.empty() ? std::string_view( fsym.name ) : f.page.selector; + const std::string rowsNext = f.page.mcpSpelling ? "edit_check " + nextFlag( "symbol=", rowsSel ) + " limit=" + std::to_string( f.callers ) + : nextFlag( "--edit-check=", rowsSel ) + " --limit=" + std::to_string( f.callers ); std::string out = ")+', doc, re.S ) + return m.group( 0 ) if m else "" +def defined( a, legend ): return re.search( r'(?', '', re.sub( r'', '', doc, flags = re.S ), flags = re.S ) + keys = set() + for m in re.finditer( r'<([a-zA-Z][\w-]*)((?:\s+[\w:.-]+="[^"]*")*)\s*/?>', body ): + for a in re.findall( r'\s([\w:.-]+)="', m.group( 2 ) ): + if a not in { "p", "n", "t", "id", "l", "k", "c" }: keys.add( m.group( 1 ) + "@" + a ) + return sorted( k for k in keys if not defined( k.split( "@", 1 )[ 1 ], legend ) ) +docs = { k: rd( "s_" + k ) for k in ( "pdef", "pcmp", "idef", "icmp" ) } +slot = re.compile( r']*via="slot"[^>]*slot_fns="\d+"' ) +ring = re.compile( r']*via="slot"' ) +print( "S0 %d %d %d %d" % ( bool( slot.search( docs[ "pdef" ] ) ), bool( slot.search( docs[ "pcmp" ] ) ), + bool( ring.search( docs[ "idef" ] ) ), bool( ring.search( docs[ "icmp" ] ) ) ) ) +for k in ( "pdef", "pcmp", "idef", "icmp" ): print( "S-" + k + " " + " ".join( gaps( docs[ k ] ) ) ) +cut = re.sub( r'(? ring, default and compact" +else + no "(S0) no slot row (path def/cmp, impact def/cmp = $( sline S0 )) — (S1)-(S5) prove nothing: $( head -c 300 "$TMP/s_out" )" +fi +for k in "pdef:1:--path default" "pcmp:2:--path --legend=compact" "idef:3:--impact default" "icmp:4:--impact --legend=compact"; do + key=${k%%:*}; rest=${k#*:}; n=${rest%%:*}; what=${rest#*:} + if [ -z "$( sline "S-$key" )" ]; then + ok "(S$n) the $what slot answer defines every attribute it emits (via= slot_callers= slot_fns= reaches_hedged= ring)" + else + no "(S$n) the $what slot answer's legend does not define: $( sline "S-$key" )" + fi +done +case " $( sline S5 ) " in + *" s@slot_fns "*) ok "(S5) mutation control: the probe reports s@slot_fns once the legend loses its slot_fns= definition" ;; + *) no "(S5) mutation control: a legend without slot_fns= still read as covered ($( sline S5 ))" ;; +esac + [ "$fail" -eq 0 ] && echo "ALL PASS" || echo "legendcoveragecheck: FAILURES ABOVE" exit "$fail" From 0b53bae34d140962db35aae78cadf97c0fc963a1 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I <210578314+joyful-ii-V-I@users.noreply.github.com> Date: Sat, 24 Oct 2026 12:10:07 -0400 Subject: [PATCH 69/77] =?UTF-8?q?docs:=20ARCHITECTURE=20=C2=A76=20seeds=20?= =?UTF-8?q?->=20hops=20->=20walks,=20and=20a=20CONTRIBUTING=20map=20line?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The three layers every graph question is built from: seed resolvers (every printed spelling is an accepted one; the hunk-to-symbol producer with its whole-file fallback, contributed by @llvm-x86 in #423), hop providers (one certainty label per edge: proven / hedged / declined, with a width), and walk policies (depth, width bound, hedged crossings, printed rows). The single-focus pick is resolveFocus's order (code definition first, lowest id, a bodyless C/C++ declaration yields to its same-scope body); the terminal scorer uses it as the tie-break. Co-Authored-By: Claude Opus 5.5 --- CONTRIBUTING.md | 2 ++ docs/ARCHITECTURE.md | 58 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index db6bb20bb..fa53613af 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -12,6 +12,8 @@ If you are about to write C++ here, §3 is the authoritative style-rules checkli - **What the tool does / which flag answers which question** → `README.md`, `docs/COMMANDS.md`, or `./build/ripwire --help` (the binary self-documents and is always current). - **How it is built internally** → `docs/ARCHITECTURE.md`. +- **Where code for a graph question goes** (a new input spelling, a new indirection shape, a new verb) → the + seeds → hops → walks layers, `docs/ARCHITECTURE.md` §6. - **How its claims are measured** → `docs/EVALS.md`. --- diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index f421bda02..a7aa2b8c7 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -733,3 +733,61 @@ The same lesson generalizes, and it is the reason the gate discipline in `CONTRI the way it is: **a gate that cannot observe what it asserts is worse than no gate**, because it reports confidence. Guard your probes — assert the thing you are about to search for actually exists, then assert the property. + +--- + +## 6. Seeds → hops → walks + +Every graph question ripwire answers about code (`--callers`, `--callees`, `--impact`, `--path`, `--connect`, +`--situ`, `--test-gate`, and the caller rows of `--for`) is built from three layers. A change to one layer should +not need a change to the other two. + +**1. Seeds: where a walk starts.** A seed resolver turns what the user typed into an ordered list of candidate +definitions and says how it got there. Input spellings: a bare name, `file:name`, `Scope::name`, `Class.method`, +the canonical id the tool prints, `@FILE:LINE`, a `*` glob, and the changed symbols of a diff. All of them go through +`resolveAllByNameQualified` (`graph.h`) or one of its tiers. The diff-hunk producer (`diffhunks.h`, `situ.h`) maps +hunks to the symbols whose bodies they touch, and falls back to the whole file when no body holds a hunk +(contributed by @llvm-x86 in #423). Matches come back in index order (ascending id); a verb that needs ONE +focus takes it from `resolveFocus` (`graph.h`): a code definition before a docs heading or a config key, the lowest id +among those, and a bodyless C/C++ declaration yields to the same-scope definition with a body. A seed resolver never +picks silently. `defs=`, `unproven_defs=`, `from_defs=`/`to_defs=` and the `` table disclose the candidates. +The rule: **any spelling the output prints is a spelling the input accepts.** + +**2. Hops: which edges leave a node, and how sure each one is.** A hop provider takes a node, a direction and a +set of hop kinds. It returns edges, and each edge carries exactly one certainty label: +- *proven*: the index pins the edge (a uniquely resolved call, an ES import binding, a SCIP pin, an FFI binding, + a single-implementor interface call `via="impl"`); +- *hedged*: a walk may follow the edge but must print its kind and width. Examples: a split arm `prov="split"`, a + name-only bind `via="name"`, a struct-field slot `via="slot"`/`"slot-name"` with its caller count, a stored + function value, a closure, a declined call whose candidate list is short enough to name; +- *declined*: a candidate set the walk counts but does not cross (too wide, gated as a builtin or standard member, + or stored without a list). +Proven edges live in the graph CSR (`outProv`, `outNameOnly`). Hedged and declined hops come from side indexes: +the value-ref and slot index (`valuerefindex.h`), declined call sites (`declinedsites.h`), interface dispatch +(`ifacedispatch.h`), and the backward hop provider (`backhops.h`). The provider reports a width. The walk decides +the bound. + +**3. Walks: how far to go and what to print.** A walk is a policy over seeds and hops. It sets a depth, a width +bound, how many hedged crossings it allows, and the rows it prints: +- `--callers`/`--callees` go one hop; +- `--impact` is the proven caller ring plus a hedged ring (`reaches_hedged=`); +- `--path` runs a shortest proven chain, then a hedged bridge, then gaps and `near_sink`, then a `next=` + continuation; +- `--connect` joins several terminals; +- `--situ`/`--test-gate` walk callers from the changed symbols. + +A walk never relabels a hop. A hedged hop is never counted in `reaches=`, `reachable=` or `hops=`. A bound that +cut something is printed (`capped=`, `total=`, `next=`). It is never reported as "not reached". + +**One coupling, stated on purpose.** When a name has several definitions, the *pick* can depend on the walk: +the right `Loop` is the one that reaches the other endpoint. The seed layer therefore returns the ordered +candidates. A walk that must choose uses the one shared terminal scorer (`scoreTerminalCandidates`, `graph.h`), +with `resolveFocus`'s order as the tie-break, and it prints the alternatives it did not take. + +**Adding to a layer.** +- A new spelling goes in the seed resolver. The verbs pick it up from there. +- A new indirection shape (a function table, a registry, a factory) is a new hop kind in the provider, with + one label and one width. Every walk then sees it. +- A new question is a new walk policy over the existing seeds and hops. +- Two copies of one rule in two verbs (two endpoint pickers, two hop vocabularies) are a fold candidate, the same + as the ingest feature families in CONTRIBUTING. From c97f6f91c588c5ccfe70aa46eb40d8beb69f31fe Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I <210578314+joyful-ii-V-I@users.noreply.github.com> Date: Sat, 24 Oct 2026 12:13:54 -0400 Subject: [PATCH 70/77] train 34: regenerate counts, printf_parity grep pin, versions fragment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Gate count 711 (8 sites); LIMITS 278 caps / 113 silent / 7 params / 140 classified; TUNING 37; README cap sentence 276 -> 278. printf_parity.manifest: only the grep row moves — its legend clause now reads 'complete=1 only for an exhaustive literal scan (beside pattern_meta=, of the literal reading only)' (honest-lex N1); 42 verbs ALL PASS. changelog.d/versions.md: kParserVer 206 -> 226 (file tag 227), kCacheVersion 36 -> 40, dictv=2259c5025ac90271 entries=930. Co-Authored-By: Claude Opus 5.5 --- README.md | 6 +++--- changelog.d/versions.md | 13 +++++++------ docs/EVALS.md | 6 +++--- docs/LIMITS.md | 10 +++++----- docs/TUNING.md | 20 ++++++++++---------- present/deck5_ripwire_build.js | 6 +++--- test/printf_parity.manifest | 2 +- 7 files changed, 32 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index da2b819ae..f3eacfb76 100644 --- a/README.md +++ b/README.md @@ -2120,9 +2120,9 @@ wrong, and it has. These are the results that say so, all in-tree, all published ### In the tests
-709 gate scripts, five contracts no unit test can hold, and the house rule: write the gate before the code it measures +711 gate scripts, five contracts no unit test can hold, and the house rule: write the gate before the code it measures -`test/regression.sh` names **709 gate scripts** and is the authoritative list; +`test/regression.sh` names **711 gate scripts** and is the authoritative list; `python3 test/pargates.py . ./build/ripwire -j 6` runs the same set in parallel. On top of them sit the contracts that do not fit a unit test: two runs byte-identical, warm output identical to cold, output that pipes clean through `xmllint --noout`, a sanitizer build with `-fno-sanitize-recover=all`, and a @@ -2878,7 +2878,7 @@ python3 test/pargates.py . ./build/ripwire -j 6 A new gate script must be added to `test/regression.sh` in the same change. The gate `test/manifestcheck.sh` enforces this rule. -Another gate derives the cap inventory. The tool has 276 compile-time caps and 7 ranking parameters. +Another gate derives the cap inventory. The tool has 278 compile-time caps and 7 ranking parameters. `docs/LIMITS.md` lists each cap, its value, and whether the file discloses a truncation when the cap fires, and `python3 docs/limits_build.py --check` proves that list against `src/`. `docs/TUNING.md` lists the measured cost of each cap. diff --git a/changelog.d/versions.md b/changelog.d/versions.md index 889f90c2f..da0136758 100644 --- a/changelog.d/versions.md +++ b/changelog.d/versions.md @@ -1,9 +1,10 @@ ### Changed — the versions this change moves, stated once -`kParserVer` 206 → 224 (file tag 225): the Rust written-type receivers carried 202 (203) on their branch and the struct-field +`kParserVer` 206 → 226 (file tag 227): the Rust written-type receivers carried 202 (203) on their branch and the struct-field slot join 204 (205), and its fix rounds changed what 204 extracts, so a cache from that branch would replay the older slot -rows; the interface-dispatch records carried 210 and 212 on theirs; 224 sits above every live branch and pull-request number -(212 to 223). A cache written by 0.6.8 is re-parsed once. `kCacheVersion` 36 → 39: a Rust binding typed only through a trait -(`&dyn T`, `impl T`, a bound) and a Rust `impl T for X` block are new binding records, so a cache written before them is -rejected and rebuilt (the branch that added them used 37 and 38). The quality-snapshot scheme stays 22. The session legend -dictionary is `dictv=a612d2cfd7867548 entries=923`. +rows; the interface-dispatch records carried 210 and 212 on theirs; the JS/TS written-binding records carried 218 and the +Python rebind-withholding records a 224 placeholder on theirs; 226 sits above every live branch and pull-request number +(212 to 225). A cache written by 0.6.8 is re-parsed once. `kCacheVersion` 36 → 40: a Rust binding typed only through a trait +(`&dyn T`, `impl T`, a bound), a Rust `impl T for X` block and a JS/TS binding written after its declaration are new binding +records, so a cache written before them is rejected and rebuilt (the branches that added them used 37, 38 and 39). The +quality-snapshot scheme stays 22. The session legend dictionary is `dictv=2259c5025ac90271 entries=930`. diff --git a/docs/EVALS.md b/docs/EVALS.md index 950ba8fea..387272231 100644 --- a/docs/EVALS.md +++ b/docs/EVALS.md @@ -21,7 +21,7 @@ section, and it is not an afterthought. | **Co-change / known-item evals** | `--eval`, `--eval-retrieval` (see `bench/ANSWERQUALITY.md`) | Whether the tool surfaces the other files a real historical commit touched; and known-item retrieval across four rankers. | | **Ensemble calibration harness** | `bench/ensemblecal/` | Whether `--ensemble`'s four evidence families are actually orthogonal, how often each fires, how stable each is across commits — and the preset ladder derived from that (§9). | | **Differential argv harness** | `test/argvdiffcheck.sh` | That a refactor changed *nothing observable*: two binaries, every argv vector, stdout + stderr + exit code byte-identical. | -| **The gate suite** | `test/regression.sh`, `test/pargates.py` | 709 gate scripts plus the determinism, cache-transparency and golden contracts. | +| **The gate suite** | `test/regression.sh`, `test/pargates.py` | 711 gate scripts plus the determinism, cache-transparency and golden contracts. | | **`--quality-delta`** | `src/quality.h` | Ten measured code-quality failure modes, reported only where a change made them worse. | ### The labeling protocol (why the held-out eval is allowed to disagree with the ranker) @@ -5888,7 +5888,7 @@ copy here would be exactly the dialect divergence that gate exists to catch. Com tags, wrap, stable-order defaults), seven individually invoked standalone gates (`g1freshcheck`, `skillscan`, `htmlexport`, `compresscheck`, `handoffcheck`, `releaseinstallcheck`, `taskroutecheck`), and a single loop -naming **709 gate scripts**, all of which exist on disk. +naming **711 gate scripts**, all of which exist on disk. `python3 test/pargates.py . ./build/ripwire -j 6` runs the same scripts in parallel so a full verification fits in one sitting. It does not modify `regression.sh`. @@ -7005,7 +7005,7 @@ Listed because the reason is more useful than the silence. shipped**. See `bench/locbench/anchorhop_calib.json`. The mention anchor's reproducible numbers are the ablations in §4. - **A single round gate-count.** Two in-tree numbers disagree (`test/pargates.py`'s docstring says - ~210; `test/argvdiffcheck.sh` says 200+), while the loop in `test/regression.sh` names 709. The + ~210; `test/argvdiffcheck.sh` says 200+), while the loop in `test/regression.sh` names 711. The loop is the authority; the stale docstrings are a known drift. Since 2026-09-10 the number is not written by hand anywhere: `docs/gatecount_build.py` derives it from the loop and rewrites every published site, `test/gatecountcheck.sh` fails if any of them drifts, and `test/manifestcheck.sh` diff --git a/docs/LIMITS.md b/docs/LIMITS.md index 3e2662b78..652fa1904 100644 --- a/docs/LIMITS.md +++ b/docs/LIMITS.md @@ -30,10 +30,10 @@ a small explicit bound" is safe only when nothing inside the group repeats; it i | total caps | files | caps whose file discloses | caps whose file discloses NOTHING | | --- | --- | --- | --- | -| 276 | 108 | 165 | **111** | +| 278 | 109 | 165 | **113** | Plus 7 ranking and apportionment parameters, in their own table below: they are not caps, they -are not counted as caps, and 276 + 7 is the 283 constants this generator parses out of `src/`. +are not counted as caps, and 278 + 7 is the 285 constants this generator parses out of `src/`. ## INDEXING, OUTPUT or BOUNDARY — which half of the answer a cap bounds @@ -51,8 +51,8 @@ None of them truncates anything, so none can be judged by `shown=`/`total=` and a disclosure — labelling them OUTPUT would ask for a `capped="1"` that could never honestly fire. The distinction was named in review on #108 and the rows below now carry it. -The `class` column below carries that answer where it is known. **140 of 276 caps are classified -(50 INDEXING, 46 OUTPUT, 44 BOUNDARY); the remaining 136 render `—`, which means NOT YET +The `class` column below carries that answer where it is known. **140 of 278 caps are classified +(50 INDEXING, 46 OUTPUT, 44 BOUNDARY); the remaining 138 render `—`, which means NOT YET CLASSIFIED — never "neither".** Classifications live in `docs/limits_classes.tsv`, a sidecar with a known expiry: the tag belongs on the declaration itself, and this file exists only because the round that @@ -102,7 +102,7 @@ refuse to write, so the column cannot be satisfied by pointing at nothing. ## Caps, by file -One table for each of the 108 files that declare a cap — the 276 caps counted above, and no parameter. +One table for each of the 109 files that declare a cap — the 278 caps counted above, and no parameter. ### `src/abicheck.h` diff --git a/docs/TUNING.md b/docs/TUNING.md index 2c6425736..cb9c6afff 100644 --- a/docs/TUNING.md +++ b/docs/TUNING.md @@ -14,25 +14,25 @@ to production at defaults; that control is what makes these numbers mean anythin | cap declarations | distinct names | tunable | must stay `constexpr` | move >= 1 invocation | move nothing measurable | | --- | --- | --- | --- | --- | --- | -| 184 | 183 | 112 | 12 | **37** | 75 | +| 186 | 185 | 112 | 12 | **37** | 75 | -The first two columns are not the same number, and the gap is not a rounding: `src/` holds **184 cap -declarations** under **183 distinct names** (`kRowCap` declared in more than one file). The sweep +The first two columns are not the same number, and the gap is not a rounding: `src/` holds **186 cap +declarations** under **185 distinct names** (`kRowCap` declared in more than one file). The sweep patches by NAME, so its own population is NAMES and not declarations — and the two columns beside those are frozen at the commit named under Provenance while this census is re-read from `src/` on -every run, which makes them a third population again. Enumerated over the 183 names `src/` declares -today: **111 tunable**, **12 must stay `constexpr`**, and **60 declared since the sweep was -prepared, which no measurement has touched** (`kBaseScopeScanCap`, `kBridgeMaxHops`, -`kCallSitesPerRowCap`, `kCallersBandMaxBytes`, `kChurnMergeBombMaxFiles`, `kClassNewRowCap`, +54 -more). 111 + 12 + 60 = 183, and `emit` refuses to render a partition that does not add up. The +every run, which makes them a third population again. Enumerated over the 185 names `src/` declares +today: **111 tunable**, **12 must stay `constexpr`**, and **62 declared since the sweep was +prepared, which no measurement has touched** (`kBackHopWidth`, `kBaseScopeScanCap`, +`kBridgeMaxHops`, `kCallSitesPerRowCap`, `kCallersBandMaxBytes`, `kChurnMergeBombMaxFiles`, +56 +more). 111 + 12 + 62 = 185, and `emit` refuses to render a partition that does not add up. The `tunable` column above reads 112 rather than 111 because one name the sweep classified is no longer -declared in `src/` at all: `kSituTestRowsShown`. Quoting "113 of 184" would be wrong in both halves +declared in `src/` at all: `kSituTestRowsShown`. Quoting "113 of 186" would be wrong in both halves at once, which is the shape of error a generated table exists to prevent. ## Read this ratio before the tables **37 of 112 tunable caps move any invocation at all. 75 move nothing measurable.** That is the -finding, and it says what NOT to do: this is not a 184-cap audit. Most of these constants are +finding, and it says what NOT to do: this is not a 186-cap audit. Most of these constants are inert on real invocations and should be left alone. The work worth doing is the small set below, plus the caps that fire SILENTLY — a cap that bites without disclosing is a defect independent of whether its value is right, and that fix is both cheaper and larger than any retuning. diff --git a/present/deck5_ripwire_build.js b/present/deck5_ripwire_build.js index 31bfa9ec9..1b2d43e4c 100644 --- a/present/deck5_ripwire_build.js +++ b/present/deck5_ripwire_build.js @@ -1214,7 +1214,7 @@ function storyCards(s, { kick, head, stories, footText }){ kicker(s, "// how it stays true", AMBER); title(s, "Proven, not promised"); const cards = [ - ["709 gate scripts", "the suite runs on every push — plus determinism, cache-transparency and golden contracts; the gate count itself is gated against the runner's own loop"], // gatecount + ["711 gate scripts", "the suite runs on every push — plus determinism, cache-transparency and golden contracts; the gate count itself is gated against the runner's own loop"], // gatecount ["byte-identical, always", "two runs over the same tree produce the same bytes; warm equals cold. Enforced in CI, twice — Release AND a plain flavour, because NDEBUG once blinded a whole class of checks"], ["differential refactoring", "a refactor must prove it changed nothing observable: two binaries, hundreds of argv vectors, stdout + stderr + exit codes byte-identical"], ["held-out labels, authored blind", "eval labels were written by reading source before the ranker ever ran on them — so the eval is allowed to say the ranker is wrong. It has."], @@ -1238,7 +1238,7 @@ function storyCards(s, { kick, head, stories, footText }){ title(s, "Claims you can trust, because we publish what failed", { size: 32 }); card(s, MX, 1.72, 3.86, 1.72); - stat(s, "709", "gate scripts named by test/regression.sh — and the COUNT itself is gated against the runner's own loop, so it cannot go stale quietly", // gatecount + stat(s, "711", "gate scripts named by test/regression.sh — and the COUNT itself is gated against the runner's own loop, so it cannot go stale quietly", // gatecount MX+0.15, 1.86, 3.56, CYAN, { bsize: 42, bh: 0.66, lsize: 9.5 }); card(s, 4.68, 1.72, 3.86, 1.72, CARD2); stat(s, "8", "registered NEGATIVES — changes built, gated green, measured against a band written before the code, and reverted rather than tuned", @@ -1540,7 +1540,7 @@ function storyCards(s, { kick, head, stories, footText }){ ["192 long flags · 36 slides", "bash test/deckclaimcheck.sh"], ["every --flag named here exists", "bash test/deckcheck.sh"], ["74.7% fewer element bytes", "bash test/showcasecapturecheck.sh"], - ["709 gate scripts", "bash test/manifestcheck.sh"], // gatecount + ["711 gate scripts", "bash test/manifestcheck.sh"], // gatecount ["49 repos · 71 papers · 237 surveyed","bash test/readmedriftcheck.sh"], ["the ten moments, any row", "ripwire . --callers=SYM | wc -c"], ["the head-to-head table", "bench/headtohead/r4-2026-08-06/"], diff --git a/test/printf_parity.manifest b/test/printf_parity.manifest index 00be1e534..17d07a5c0 100644 --- a/test/printf_parity.manifest +++ b/test/printf_parity.manifest @@ -16,7 +16,7 @@ around 0 827b569b350640938954e34497f0430f757b4baac8f0daa2194306113cb46a36 e3b0c4 uses 0 5442767b638c8cac9eb0327cbfd5fde73f92fde5b85e2a995670a3982d903711 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 path 0 976a050ece8954e7e0b245df5d61b693bb10dec1f3867918ec519c72f83f4aee e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 connect 0 6ceb99d6c5ecb9f57a1b7426da2ed2118705cb7cc6b3d0aa4456b84c0ff715f4 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 -grep 0 ea5c66f3062d3ba805f271d18b146ee411f3b132f75d0c77062ecf2eda13f36e e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 +grep 0 5494a9c4aa9739ae3d1dfd5f7776029f049efa6ee2cb936d2ea0f0c5e8a6dfdb e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 pack_signatures 0 66de39992132b15fdc91e088e5301976e2beb972cb95802441331350983fab25 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 pack_task 0 f3de4fc3a28342cf515fedb8a02de0fea7f149ce8e948d39a7037c955a9eafd1 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 arch 2 45b12ab02e3e9f2e32bf92fe0e1cef7b489e70711386e0749d50281743aeeec5 e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855 From 1fa0886a7150213ba807b26a9e23c5a215f179c0 Mon Sep 17 00:00:00 2001 From: joyful-ii-V-I <210578314+joyful-ii-V-I@users.noreply.github.com> Date: Sat, 24 Oct 2026 12:36:23 -0400 Subject: [PATCH 71/77] train 34: showcase capture 2026-10-24 and COMMANDS.md regenerated once docs/captures/COMMANDS_showcase_2026-10-24.md: 265 commands (219 rc 0; the non-zero set is the refusal / timeout demos), from build/ripwire built_from=b58af84eb (src == HEAD). Seed src/graph.h:11749 -> rankGraphTeleport (main's 10408; FC2/FC3/IND4b/HOP1 graph.h lines). COMMANDS.md: 188 flags, 172 samples. Co-Authored-By: Claude Opus 5.5 --- docs/COMMANDS.md | 962 +- docs/captures/COMMANDS_showcase_2026-10-24.md | 7806 +++++++++++++++++ 2 files changed, 8281 insertions(+), 487 deletions(-) create mode 100644 docs/captures/COMMANDS_showcase_2026-10-24.md diff --git a/docs/COMMANDS.md b/docs/COMMANDS.md index b3c432042..84e4711cd 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-23.md`), trimmed to the first few lines and +Sample output is lifted from a real recorded run (`docs/captures/COMMANDS_showcase_2026-10-24.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` @@ -195,7 +195,7 @@ _The honest half of the contract: a task with no ripwire-shaped evidence ABSTAIN $ ./build/ripwire . --help-task="write a cheerful release announcement" - + ``` @@ -215,14 +215,14 @@ _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` +#### The convergence disclosure contract +### Wave-2 adversarial verification (2026-08-19) — six probes against `aa97c9e` @@ -252,14 +252,14 @@ _Restore the / sections the --for bundle collapses to a counted s ``` $ ./build/ripwire . --for="tree-sitter parse of a source file" --sections=lego,compose - + 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 ) +TSTree* parseTree( TSParser* parser, std::string_view src ) Exercise every registered grammar and its embedded query, reporting loaded and expected totalsinline DoctorGrammarProbe doctorProbeGrammars() ... [21 more line(s); run it to see the whole thing] @@ -279,14 +279,14 @@ _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` +#### The convergence disclosure contract +### Wave-2 adversarial verification (2026-08-19) — six probes against `aa97c9e` @@ -314,14 +314,14 @@ _Opt OUT of compact conceptual serving: restore the rank-first auto wal ``` $ ./build/ripwire . --for="tree-sitter parse of a source file" --auto-bodies - + 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 ) +TSTree* parseTree( TSParser* parser, std::string_view src ) Exercise every registered grammar and its embedded query, reporting loaded and expected totalsinline DoctorGrammarProbe doctorProbeGrammars() ... [21 more line(s); run it to see the whole thing] @@ -346,17 +346,18 @@ _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 ) - -... [21 more line(s); run it to see the whole thing] + +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] ``` **Caveats (stated by the binary):** @@ -377,14 +378,14 @@ _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 - + 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 ) +TSTree* parseTree( TSParser* parser, std::string_view src ) Exercise every registered grammar and its embedded query, reporting loaded and expected totalsinline DoctorGrammarProbe doctorProbeGrammars() ... [21 more line(s); run it to see the whole thing] @@ -408,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 - @@ -438,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 std::string staleByKindValue( const std::vector<StaleAck>& staleAcks ) +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] ``` @@ -468,7 +469,7 @@ _Interface -> implementors view: every existing impl of the named interface; the $ ./build/ripwire . --lego=Vehicle - + @@ -499,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" - + /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=2041 est_tokens=957 +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 -━━ commands.md (relevance 9.990) ━━ [sections: 1 of 164 selected (192 in doc), section-granular; whole doc 587368 B; lines="3824-3828"; dropped_by_budget=163] [truncated: 1865 of 7738 bytes] -### `--field-affinity[=STRUCT]` +━━ commands.md (relevance 10.880) ━━ [sections: 1 of 165 selected (192 in doc), section-granular; whole doc 594792 B; lines="555-580"; dropped_by_budget=164] [truncated: 1854 of 4256 bytes] +#### Pattern: a directory of dumped tool output as a knowledge base -**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. +**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? -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] +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: -(capped: 1 of 2 relevant document files omitted — raise --max-tokens/budget_tokens or narrow the query for 1 more (~2548-byte budget)) +... [17 more line(s); run it to see the whole thing] ``` ### `--tree` @@ -622,7 +628,7 @@ _File-by-file orientation map (top symbols per file)._ ``` $ ./build/ripwire . --tree - + @@ -726,8 +732,8 @@ _--no-stable outside --mcp: what the flag does (or says) when there is no stable $ ./build/ripwire . --no-stable --top-k=3 - - + + @@ -760,7 +766,7 @@ $ ./build/ripwire . --around=rankGraphTeleport - + @@ -812,18 +818,18 @@ _What SYM calls (1-hop out-edges)._ ``` $ ./build/ripwire . --callees=rankGraphTeleport - - + + - - - - - - + + + + + + ``` @@ -847,10 +853,10 @@ _The resolvable use-sites (call/read/write/import/extends) with file:line; count ``` $ ./build/ripwire . --uses=rankGraphTeleport - + - - + + @@ -881,16 +887,16 @@ _Composable node-set query: functions within 2 caller-hops of rankGraphTeleport. ``` $ ./build/ripwire . --graph-query='and(callers(name("rankGraphTeleport"),2),kind(all,fn))' - + - + - + - + @@ -918,19 +924,19 @@ _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] ``` @@ -953,12 +959,12 @@ _Shortest directed call-path SRC -> DST. CHANGED: now reports from_p/to_p/from_d ``` $ ./build/ripwire . --path=main,rankGraphTeleport - + - + ``` @@ -975,10 +981,10 @@ _Minimal connecting subgraph over 3 symbols (finds shared-caller joins)._ ``` $ ./build/ripwire . --connect=rankGraphTeleport,runEval,getIndex - + - + @@ -1001,7 +1007,7 @@ _Transitive blast radius — everything that reaches SYM. NOW carries shown/capp ``` $ ./build/ripwire . --impact=rankGraphTeleport - @@ -1085,7 +1091,7 @@ _Test files that transitively reach the changed file._ ``` $ ./build/ripwire . --affected=src/graph.h - @@ -1136,25 +1142,14 @@ Backtested on two histories (EVALS.md): a named partner is edited within the nex **Try it** -_Mid-task situational report for the current git diff — recorded against a DIRTY tree (contrast with the sandbox run below)._ +_Mid-task situational report for the current git diff — recorded against a CLEAN tree (contrast with the sandbox run below)._ ``` $ ./build/ripwire . --situ -ripwire situational-awareness — 1 changed file(s), 115 symbols in them +ripwire situational-awareness — 0 changed file(s), 0 symbols in them root: . -at: f96bd7bdd+dirty - decl/def partners (5) not_dependents=1 (showing 4 of 5 files — shown=4 total=5 capped=1) — symbols declared there and defined here, or the reverse (header/impl, stub, partial class); NOT transitive dependents, so they are absent from the list below: - test/archmetricsfix/src/core/math.h (1 shared symbols) - test/builtinbindfix/mono/rb/c/report.rb (1 shared symbols) - test/rubyargfix/lib/script.rb (1 shared symbols) - test/rubyrecvfix/lib/script.rb (1 shared symbols) - [1] blast radius: 3 symbols across 1 files transitively depend on these changes - counts_floor=1 graph_ambiguous=2882 graph_unresolved=14926 graph_unindexed=269 (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] - bench/routing_ab_report.py (3 dependent symbols) - [2] tests to run (1) 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/showcase_capture.py [changed] (run: not derivable) - script_gates_unmodelled=775 — test/*.sh gates never appear above: script-to-binary edges are not call edges (a path count) -... [10 more line(s); run it to see the whole thing] +at: c97f6f91c + (0 changed files — working tree is clean, nothing to analyze) ``` **Shaped by:** `--affected`, `--test-gate`, `--legend`, `--limit` @@ -1167,25 +1162,25 @@ Empty diff is fine — the packet still carries branch/sha + heuristics. Compose **Try it** -_The continuation packet for the NEXT session: disk truth (branch/sha, changed symbols, blast radius, tests) + labeled suggestions. Recorded against a DIRTY tree._ +_The continuation packet for the NEXT session: disk truth (branch/sha, changed symbols, blast radius, tests) + labeled suggestions. Recorded against a CLEAN tree._ ``` $ ./build/ripwire . --handoff - - - - - - - - - - - - - - -... [17 more line(s); run it to see the whole thing] + + + + + + + + + + + + +... [1 more line(s); run it to see the whole thing] ``` **Caveats (stated by the binary):** @@ -1202,18 +1197,12 @@ exit 4 if either is non-empty agent self-check before a PR (pair with --quality- **Try it** -_Pre-PR gate recorded against a DIRTY tree, so the obligations below are the working copy's real ones — the recorded exit code says which way it went._ +_Pre-PR gate on a CLEAN tree: no obligations, exit 0._ ``` $ ./build/ripwire . --test-gate - - - - - - - + + ``` @@ -1237,12 +1226,11 @@ _Literal trigram-indexed search. Each hit carries its MATCHED line as the ``` $ ./build/ripwire . --grep=DISCLOSE - + - + - + @@ -1540,7 +1529,7 @@ _Importance-weighted detail: FULL bodies for top-2, signatures for the rest._ ``` $ ./build/ripwire . --for="pagerank power iteration" --detail=2 - + @@ -1575,8 +1564,8 @@ $ ./build/ripwire . --pack-signatures --top-k=10 - - + + @@ -1606,7 +1595,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)" ); @@ -1633,11 +1622,11 @@ _NEW since the last capture: --top-k=0 means PAYLOAD-ONLY — no ranked map ride ``` $ ./build/ripwire . --top-k=0 --expand=rankGraphTeleport - + - + + - - + + - + @@ -1809,19 +1798,19 @@ _File->file dependency graph (god-files, cycles)._ ``` $ ./build/ripwire . --deps - - - - - + + + + + - - - - + + + + + - - + ... [17 more line(s); run it to see the whole thing] ``` @@ -1846,18 +1835,18 @@ _Complexity x recent git churn (maintenance pain)._ $ ./build/ripwire . --hotspots - - - - + + + + + - + - - - - + + + ... [17 more line(s); run it to see the whole thing] ``` @@ -1874,17 +1863,17 @@ _Token-normalized duplicate bodies._ ``` $ ./build/ripwire . --clones - + - + - + ... [17 more line(s); run it to see the whole thing] @@ -1905,10 +1894,10 @@ _Per-function size ranking, largest Halstead volume/token entropy/lines first ``` $ ./build/ripwire . --biggest-first --limit=8 - - - - + + + + @@ -1938,7 +1927,7 @@ _Per function, the non-local MUTABLE state it can reach (transitively), most wri ``` $ ./build/ripwire . --nonlocal-state --limit=8 - @@ -1973,7 +1962,7 @@ _The family join: per function, which of four orthogonal evidence families fire, ``` $ ./build/ripwire . --ensemble --limit=8 - @@ -1984,7 +1973,7 @@ $ ./build/ripwire . --ensemble --limit=8 - + ... [17 more line(s); run it to see the whole thing] ``` @@ -2004,17 +1993,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] @@ -2041,13 +2030,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] ``` @@ -2072,7 +2061,7 @@ _Score the naming-* rules against this repo's own rename history: proxy=old/(old ``` $ ./build/ripwire . --naming-calibration - + @@ -2107,10 +2096,10 @@ _The corpus's OWN case-convention vote per (language, kind) group; off-conventio ``` $ ./build/ripwire . --naming-consistency --limit=8 - - - - + + + + @@ -2144,8 +2133,8 @@ _The opt-in --lint modifier: naming predicates over LOCAL variable names too, C/ ``` $ ./build/ripwire . --lint --naming-locals - - + + @@ -2153,10 +2142,10 @@ $ ./build/ripwire . --lint --naming-locals - + - + ... [17 more line(s); run it to see the whole thing] ``` @@ -2179,11 +2168,11 @@ _Functions WITH a doc comment, most name-restating first: c_coeff (high = the co ``` $ ./build/ripwire . --comment-coherence --limit=8 - + - + @@ -2212,16 +2201,16 @@ _Files that change together in git (hidden coupling)._ ``` $ ./build/ripwire . --cochange - + + - - - + + + - + - ... [22 more line(s); run it to see the whole thing] ``` @@ -2244,16 +2233,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] ``` @@ -2270,10 +2259,10 @@ _Modularity-violation GROUPS instead of pairs: "X co-changes with {A,B,C}, none ``` $ ./build/ripwire . --cochange --cochange-groups - + - + @@ -2299,19 +2288,19 @@ _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] ``` @@ -2337,7 +2326,7 @@ _Enforce layering rules (exit 2 on violation) — run against the repo's own tes $ ./build/ripwire . --arch=test/archfix/rules.txt - @@ -2367,7 +2356,7 @@ _Enforce layering rules (exit 2 on violation) — run against the repo's own tes $ ./build/ripwire . --arch=test/archfix/rules.txt - @@ -2393,7 +2382,7 @@ _Enforce layering rules (exit 2 on violation) — run against the repo's own tes $ ./build/ripwire . --arch=test/archfix/rules.txt - @@ -2420,8 +2409,8 @@ _Built-in AST checks (c-cast, goto, unsafe-c-fn, ...)._ ``` $ ./build/ripwire . --lint - - + + @@ -2429,10 +2418,10 @@ $ ./build/ripwire . --lint - + - + ... [17 more line(s); run it to see the whole thing] ``` @@ -2529,8 +2518,8 @@ _DROP two families, applied after selection; the raw select=/ignore= you passed ``` $ ./build/ripwire . --lint --lint-ignore=naming-,cache- - + @@ -2538,10 +2527,10 @@ $ ./build/ripwire . --lint --lint-ignore=naming-,cache- - + - + ... [17 more line(s); run it to see the whole thing] ``` @@ -2643,15 +2632,15 @@ _Cluster the call graph into cohesive modules._ ``` $ ./build/ripwire . --communities - + - + - + @@ -2674,7 +2663,7 @@ _Drill into ONE call-graph community by id — the drill= the --communities outp ``` $ ./build/ripwire . --community=0 - ``` @@ -2699,19 +2688,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] ``` @@ -2736,16 +2725,16 @@ $ ./build/ripwire . --report # ripwire architecture report -3315 files · 30881 symbols · 29413 edges · 3553 modules (14301 call-graph isolated) +3589 files · 31640 symbols · 29882 edges · 3588 modules (14830 call-graph isolated) Root: `.` -Call-graph isolate provenance: 3822 declaration, 2412 header, 3226 source, 4841 document; 0 connected Louvain singletons +Call-graph isolate provenance: 3951 declaration, 2444 header, 3330 source, 5105 document; 0 connected Louvain singletons -## Modules (call-graph clusters; showing 12 of 3553) -- **src::emitTo@infra/emit.h:53:2760 [write,run,emit]** — 1577 symbols -- **src::kindIs@infra/nodekind.h:46:3271 [push,emit,collect]** — 530 symbols -- **src::DISCLOSE@infra/Diagnostics.h:616:43369 [read,compute,resolve]** — 333 symbols +## Modules (call-graph clusters; showing 12 of 3588) +- **src::emitTo@infra/emit.h:53:2760 [write,run,emit]** — 1617 symbols +- **src::kindIs@infra/nodekind.h:46:3271 [push,emit,collect]** — 547 symbols +- **src::DISCLOSE@infra/Diagnostics.h:616:43369 [read,compute,resolve]** — 335 symbols ... [17 more line(s); run it to see the whole thing] ``` @@ -2762,12 +2751,12 @@ _Cross-module call seams no test reaches. NOW carries seam_pairs/shown/capped._ ``` $ ./build/ripwire . --seams - - + + - - + + @@ -2793,15 +2782,15 @@ $ ./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"] - n102["src
9191"] + n102["src
9384"] n103["src/infra
1301"] end subgraph sg1 ["test"] - n104["test
5465"] - n556["test/legochargefix
546"] - n398["test/fixtures/recallpassage
314"] - n799["test/slotjoinfix
175"] - n641["test/ownerhopfix/py/pkg
158"] + n104["test
5501"] + n559["test/legochargefix
546"] + n400["test/fixtures/recallpassage
314"] + n944["test/slotjoinfix
175"] + n786["test/ownerhopfix/py/pkg
158"] n342["test/expandmodefix
151"] n344["test/expandsibsfix
149"] ... [17 more line(s); run it to see the whole thing] @@ -2822,15 +2811,15 @@ _Bus-factor: recency-weighted author ownership per file._ ``` $ ./build/ripwire . --owners - - + + - + - + @@ -2853,7 +2842,7 @@ _Internal functions with no caller found in the index — a name-based graph rea ``` $ ./build/ripwire . --dead-code=src - + ``` @@ -2913,12 +2902,12 @@ either fold only when it saves a quarter of the bytes. Refused alone. **Try it** -_The same verdict with every stale acknowledgement printed as an row and the full header — recorded against a DIRTY tree, so the rows above it are the working copy's. Refused without --quality-delta._ +_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._ ``` $ ./build/ripwire . --quality-delta --show-stale - -, acked. value-ref-excluded=N: internal functions kept off because a table, field or argument in another function or at file scope holds them (matched by name, not a proven call); a floor. at=: commit+dirty+shallow. stale=N: ack ledger rows whose target no longer applies (sa rows); never gating. preexisting-worse=N: regressions on symbols that existed at baseline; they gate when major. new-symbol=N: findings on NEW code; new-symbol rows never gate, except defect-shape format-arity. register-macro-excluded=N: symbols kept out of dead-code as self-registering test/bench macros; a floor. declined-call-excluded=N: symbols kept out of dead-code only because a declined call may mean them; a floor. api-new-surface=N: new PUBLIC symbols; a count, never gates, not in regressions=. renames=/rename_window_commits=: git rename pairs read over that many commits, to re-file baseline and acks. acked_by_rename=/acked_by_content=: acked= suppressions matched via git renames / an equal body hash. renames_window_truncated=1: history is deeper than the rename window, older renames unread. acked=N: findings suppressed by the ack ledger, listed as sa rows; never gating. sa key=/why=: the stale ack's ledger hash / target-gone (names nothing now) or finding-gone (no longer fires). head_basis=identity: the floor is this tree's own snapshot; archived-index-hidden: refused, a tracked path is skip-worktree/assume-unchanged; absent: the archived HEAD tree. --> + @@ -2942,12 +2931,12 @@ Each kind is measured against the baseline (complexity/verbosity/nesting/params/ **Try it** -_Recorded against a DIRTY tree, so any row below is a real regression in the working copy — the recorded exit code says whether anything gated. The sandbox section below shows the same gating shape on a known, deliberate edit._ +_On a CLEAN tree: nothing got worse, exit 0. The gating shape is in the sandbox section below._ ``` $ ./build/ripwire . --quality-delta - -, acked. at=: commit+dirty+shallow. stale=N: ack ledger rows whose target no longer applies (sa rows); never gating. stale_by_kind=: stale= split by ack kind (kind:N,...); the sa rows are collapsed. stale_next=: the call printing the sa rows and the full header. head_basis=identity: the floor is this tree's own snapshot; archived-index-hidden: refused, a tracked path is skip-worktree/assume-unchanged; absent: the archived HEAD tree. --> + ``` @@ -2967,12 +2956,12 @@ $ ./build/ripwire . --quality-delta **Try it** -_Recorded against a DIRTY tree, so any row below is a real regression in the working copy — the recorded exit code says whether anything gated. The sandbox section below shows the same gating shape on a known, deliberate edit._ +_On a CLEAN tree: nothing got worse, exit 0. The gating shape is in the sandbox section below._ ``` $ ./build/ripwire . --quality-delta - -, acked. at=: commit+dirty+shallow. stale=N: ack ledger rows whose target no longer applies (sa rows); never gating. stale_by_kind=: stale= split by ack kind (kind:N,...); the sa rows are collapsed. stale_next=: the call printing the sa rows and the full header. head_basis=identity: the floor is this tree's own snapshot; archived-index-hidden: refused, a tracked path is skip-worktree/assume-unchanged; absent: the archived HEAD tree. --> + ``` @@ -2997,7 +2986,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 -

--callers=SYM --json) +````` + +## `./build/ripwire . --hotspots --limit=3 --offset=3` + +*Pagination: 3 items, skipping the first 3 (deterministic seams).* + +**wall time: 2.38s** + +````` + + + + + + + +````` + +## `./build/ripwire . --ignore-tests --top-k=5` + +*Drop test paths from the corpus before ranking.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [67 more display lines; full output is 8115 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --exclude=present --exclude=bench --top-k=5` + +*Drop matching paths (repeatable) before ranking.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [65 more display lines; full output is 8095 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: 1.48s** + +````` + + + + + + + + + + + + + + + + + + + + + + +````` + +## `./build/ripwire . --no-cache --top-k=3` + +*Force a cold parse (bypass the warm TMPDIR cache) — shows the cold-vs-warm cost.* + +**wall time: 3.15s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [63 more display lines; full output is 8117 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.17s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [63 more display lines; full output is 8117 bytes on 1 raw line(s)] +````` + +Artifact written: + +````` + 28364081 /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).* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [18 more display lines; full output is 4246 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: 2.02s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + +````` + +## `./build/ripwire . --eval` + +*Self-eval: co-change recall vs BM25.* + +**wall time: 19.78s** + +````` +ripwire --eval (co-change recovery, averaged over 80 historical commits) + ranker recall@5 recall@10 recall@20 + ripwire 4.9% 6.4% 15.5% + BM25 10.1% 15.0% 15.9% + BM25sub 11.4% 22.7% 29.0% + BM25body 17.7% 23.7% 32.4% + fused 12.8% 14.3% 26.0% + anchored 17.7% 24.0% 32.7% + same-dir 0.9% 5.0% 8.8% + random 0.1% 0.3% 0.6% <- floor (random ranking over F=3589 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: 151.62s** + +````` +ripwire --eval-retrieval (known-item, 4000 doc-commented symbols; gold is in-corpus by construction) + sample: population=7969 scored=4000 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.5% 84.8% 90.0% + subtoken doc-phrase 0.903 88.3% 92.2% 93.4% + name-exact name 0.898 82.2% 95.8% 97.4% + name-exact doc-phrase 0.013 0.5% 1.9% 2.6% + anchored name 0.687 56.2% 84.1% 88.7% + anchored doc-phrase 0.899 87.5% 92.2% 92.9% + routed name 0.899 82.4% 96.0% 97.6% + routed doc-phrase 0.902 88.2% 92.0% 93.2% + note: routing chose name-exact on 3989/4000 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: 40.06s** + +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=b58af84eb) +````` + + +--- + +# navigate — seeds, claims, slices, shapes + +## `./build/ripwire . --at=src/graph.h:11751` + +*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 . --callers=@src/graph.h:11751` + +*The same seed in a SELECTOR position: @FILE:LINE resolves to the innermost enclosing definition, then --callers runs on it.* + +````` + + + + + + + + + + +````` + +## `./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 16107 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.* + +````` + + + + + +````` + +## `./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** + +````` +(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: 1.10s** + +````` + + + + + + + + + + + + + + + + + +````` + +## `./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: 1.05s** + +````` + + + + + + + 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: 1.14s** + +````` + + + + + + + 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.22s** + +````` + + + + + + + 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.* + +````` + + + + + + + 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.75s** + +````` + +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.85s** + +````` + +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 17778 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 +inline bool hasMainGuard( std::string_view source ) +constexpr std::uint32_t kMaxYamlNestDepth = 64u +SpanTierBatch spanTiersOfFiles( std::span<const std::string> diskPaths, bool useMemo ) +enum class DisclosureWhy : std::uint8_t +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 ) +struct LintRule +inline FileHealth measureHealthAdoptingMemberMacroReparse( TSParser* parser, Lang lang, std::string_view bytes, TreeGuard& tree, MemberMacroRepa … [line truncated: 19 more bytes on this line] +inline std::string buildMemoryStopAttr( const IngestResult& ing, bool json ) +… [138 more display lines; full output is 19865 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: 1.88s** + +````` + + + + +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 +inline bool hasMainGuard( std::string_view source ) +constexpr std::uint32_t kMaxYamlNestDepth = 64u +SpanTierBatch spanTiersOfFiles( std::span<const std::string> diskPaths, bool useMemo ) +enum class DisclosureWhy : std::uint8_t +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 ) +struct LintRule +inline FileHealth measureHealthAdoptingMemberMacroReparse( TSParser* parser, Lang lang, std::string_view bytes, TreeGuard& tree, MemberMacroRepa … [line truncated: 19 more bytes on this line] +inline std::string buildMemoryStopAttr( const IngestResult& ing, bool json ) +… [149 more display lines; full output is 20198 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: 1.89s** + +````` + + + + +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 +inline bool hasMainGuard( std::string_view source ) +constexpr std::uint32_t kMaxYamlNestDepth = 64u +SpanTierBatch spanTiersOfFiles( std::span<const std::string> diskPaths, bool useMemo ) +enum class DisclosureWhy : std::uint8_t +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 ) +struct LintRule +inline FileHealth measureHealthAdoptingMemberMacroReparse( TSParser* parser, Lang lang, std::string_view bytes, TreeGuard& tree, MemberMacroRepa … [line truncated: 19 more bytes on this line] +inline std::string buildMemoryStopAttr( const IngestResult& ing, bool json ) +… [134 more display lines; full output is 20417 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: 1.96s** + +````` + + + + + +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 std::string staleByKindValue( const std::vector<StaleAck>& staleAcks ) +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& wo… +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] +inline const char* kAcksFile = ".ripwire_quality_acks" +DirtyPinVerdict inspectDirtyBaselinePin( const MainDispatch& d, const std::string& acksFile ) +struct IdentityHealing +inline std::pair<std::string, std::string> qualityDeltaJson( const std::string& root, bool showStale = false ) +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 ) +… [193 more display lines; full output is 22980 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: 1.84s** + +````` + + + + + +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 std::string staleByKindValue( const std::vector<StaleAck>& staleAcks ) +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& wo… +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] +inline const char* kAcksFile = ".ripwire_quality_acks" +DirtyPinVerdict inspectDirtyBaselinePin( const MainDispatch& d, const std::string& acksFile ) +struct IdentityHealing +inline std::pair<std::string, std::string> qualityDeltaJson( const std::string& root, bool showStale = false ) +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 ) +… [193 more display lines; full output is 22799 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: 2.92s** + +````` + + + + + + + + + + + + + +````` + +## `./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: 4.10s** + +````` + + + + + + + + + + +````` + +## `./build/ripwire . --skipped` + +*WHY a file is not in the index (oversize / excluded / unsupported-ext / gitignored) and which indexed files it cannot vouch for (degraded-parse, minified-suspect), plus the per-language census.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [441 more display lines; full output is 46965 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).* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [63 more display lines; full output is 8117 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.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [63 more display lines; full output is 8117 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** + +````` + + + + + + const&, float) src/graph.h:11751 + #1 0x102f3e884 in rw::rankGraph(Graph const&, float) src/graph.h:11792 + #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.* + +````` + + + + +````` + +## `./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.73s** + +````` + + + + +````` + +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.34s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [188 more display lines; full output is 15675 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.08s** + +````` + + + + + + + + + + + +````` + +## `./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).* + +**wall time: 1.09s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [21 more display lines; full output is 11151 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.92s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [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: 3.72s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [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: 2.13s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [510 more display lines; full output is 45635 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.93s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + +````` + +## `./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 4430 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: 13.67s** + +````` + + + + + + + + + + + +

+

+

+

+

+

+

+

+

+

+

+

+

+

+

+

+… [153 more display lines; full output is 15975 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: 3.15s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [641 more display lines; full output is 72667 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=2259c5025ac90271 entries=930 +: 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, +… [901 more display lines; full output is 97483 bytes on 931 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: 2.55s** + +````` + + + + + + + + + + +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] ] +… [607 more display lines; full output is 73533 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: 3.11s** + +````` + + + + + + + + + + + + + + + + + + + + + + + +0.5 +1023u +main (90 lines) +20 +delta < 0.02 && delta > -0.02 +0.02 +… [724 more display lines; full output is 66341 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: 2.60s** + +````` + + + +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: 2.82s** + +````` + + + +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: 2.63s** + +````` +(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: 2.94s** + +````` +(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: 2.65s** + +````` +{"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}}, +… [13391 more display lines; full output is 1895351 bytes on 1 raw line(s)] +````` + +Parsed summary of the same SARIF (past the display cut): + +````` +sarif 2.1.0 rules= 39 results= 6691 +````` + +## `./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: 7.21s** + +````` + + +

+

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

+

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

+

+ +````` + +## `./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: 4.12s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [847 more display lines; full output is 56290 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: 4.19s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +````` + +## `./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: 2.41s** + +````` +(empty) +````` + +Artifact written: + +````` + 156158 /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: 6.43s** + +````` +(empty) +````` + +stderr: + +````` +ripwire: --index-out wrote /aux/ci_index.lean.ripwirecache (28364081 bytes, lean family) +ripwire: --index-out wrote /aux/ci_index.rich.ripwirecache (58137965 bytes, rich family) +````` + +Artifact written: + +````` + 28364081 /aux/ci_index.lean.ripwirecache + 58137965 /aux/ci_index.rich.ripwirecache + 86502046 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.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [63 more display lines; full output is 8117 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.* + +**wall time: 1.07s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [63 more display lines; full output is 8117 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. + 128906 /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** — **wall time: 1.01s** + +````` + + + + + + + + + + + + + +````` + +## `./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** — **wall time: 1.19s** + +````` + + + + + + + +````` + +## `./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: 9.30s** + +````` + + + + + + + + + + + + + +````` + +## `./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: 7.58s** + +````` +ripwire situational-awareness — 1 changed file(s), 13 symbols in them +root: . +at: c97f6f91c+dirty + change_set: granularity=file changed_symbols=13 file_symbols=13 files_narrowed=0 files_file_granular=1 diff_hunks=7 diff_hunks_located=6 diff_hunks_unlocated=1 — seeds = symbols a diff -U0-vs-HEAD hunk overlaps (function/method body spans only); an unlocated hunk (module-level, deletion, rename) … [line truncated: 34 more bytes on this line] + [1] blast radius: 246 symbols across 58 files transitively depend on these changes (showing 8 of 58 files — shown=8 total=58 capped=1 prcontext_cap=20 (--pr-context's own per-file list is cut at 20 too); next: --situ --limit=58) + counts_floor=1 graph_ambiguous=2925 graph_unresolved=15128 graph_unindexed=269 (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 (13 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=779 — 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="5228": + (none — 5228 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: 5.90s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +````` + +## `./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: 38.60s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [148 more display lines; full output is 17984 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: 16.52s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [148 more display lines; full output is 22976 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: 17.50s** + +````` +{"baseline":"git-HEAD","regressions":9,"minor":1,"acked":0,"stale":166,"preexisting-worse":6,"new-symbol":3,"gating":5,"register-macro-excluded":62,"api-new-surface":2,"at":"c97f6f91c+dirty","renames":62,"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"}, +… [145 more display lines; full output is 16575 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: 13.34s** + +````` +(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 written +````` + +## `./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: 11.86s** + +````` +(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: 16.29s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [147 more display lines; full output is 17687 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: 2.01s** + +````` + + + + + + +````` + +## `./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: 2.29s** + +````` + + + + + + +````` + +## `./build/ripwire . --pr-context` + +*The review-evidence bundle with an actual changed file.* + +**wall time: 5.02s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [80 more display lines; full output is 8063 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.75s** + +````` + + + + + + + + + + + + + + + + + + + + + + +````` + +## `./build/ripwire . --clones` + +*The duplicated helper the sandbox edit introduced shows up as a clone group.* + +**wall time: 3.01s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [308 more display lines; full output is 16222 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.52s** + +````` + + + + + +````` + +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.41s** + +````` + + + + +````` + +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: 7.33s** + +````` + + +

+

+ +````` + +## `./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: 17.42s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [150 more display lines; full output is 17954 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: 12.37s** + +````` +(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: 6.14s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +````` + +## `./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.* + +**wall time: 1.63s** + +````` +src/infra/sortutil.h::rw::sortutil::lessByScoreDescId 2026-10-24 keep this branch-free — it sits inside the PageRank sort comparator c97f6f91c588c5ccfe70aa46eb40d8beb69f31fe integration/train-34 +````` + +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.)* + +**wall time: 1.14s** + +````` + + + + + + + + + + + + + + + + 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.* + +**wall time: 1.09s** + +````` +src/infra/sortutil.h::rw::sortutil::lessByScoreDescId 2026-10-24 chose the flat two-branch compare over the nested ladder because the comparator sits inside the PageRank sort c97f6f91c588c5ccfe70aa46eb40d8beb69f31fe integration/train-34 +````` + +## `./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: 4.12s** + +````` +{"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":2925,"graph_unresolved":15128,"graph_unindexed":269,"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":779,"graph_ambiguous":2925,"graph_unresolved":15128,"graph_unindexed":269,"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.92s** + +````` +{"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.88s** + +````` +{"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":2925,"graph_unresolved":15128,"graph_unindexed":269,"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":779,"graph_ambiguous":2925,"graph_unresolved":15128,"graph_unindexed":269,"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: 2.09s** + +````` +(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: 2.03s** + +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":2925,"graph_unresolved":15128,"graph_unindexed":269,"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: 4.29s** + +````` +{"schema":"ripwire.edit-plan/v1","mode":"apply","edits":1,"files":1,"callers_union":4,"graph_ambiguous":2925,"graph_unresolved":15128,"graph_unindexed":269,"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":2925,"graph_unresolved":15128,"graph_unindexed":269,"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: 18.42s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [144 more display lines; full output is 17051 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: 11.81s** + +````` +(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: 15.10s** + +````` +(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 30704 per-symbol rows, 31642 indexed symbols total) +````` + +Artifact written: + +````` + 5197963 .ripwire_quality_baseline +# ripwire quality baseline v6 — regenerate with --quality-baseline; do not hand-edit +producer 819bbf9fd6fd06a8db334bbe702e4325902726c09c1613707e652f98ed144539 +head c97f6f91c588c5ccfe70aa46eb40d8beb69f31fe +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: 18.22s** + +````` + + +````` + + +--- + +# 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: 862 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= 47445 (~tokens at 4 bytes/token: 11861 ) +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= 742 schema_bytes= 279 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: 862 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= 14186 (~tokens at 4 bytes/token: 3546 ) +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= 742 schema_bytes= 279 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: 2.84s** + +````` +{"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: 6553 more bytes on this line] +"_index":"[index: files=3589 symbols=31640 hash=57665a56]","_fresh":"ok"}} +{"jsonrpc":"2.0","id":3,"result":{"content":[{"type":"text","text":"

@@ -3090,7 +3079,7 @@ _OWNERSHIP partition for a shared tree: every regression here lives in src/infra ``` $ ./build/ripwire . --quality-delta --scope=src/graph.h - @@ -3115,15 +3104,15 @@ A contract is PER DEFINITION, so a SYM matching several definition sites REFUSES **Try it** -_Fast per-symbol post-edit contract check vs git HEAD — recorded against a DIRTY tree, so the verdict describes the working copy, not HEAD alone, and the recorded exit code is the working copy's._ +_Fast per-symbol post-edit contract check vs git HEAD (unchanged on a clean tree)._ ``` $ ./build/ripwire . --edit-check=rankGraphTeleport - - - + + @@ -3174,12 +3163,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":2882,"graph_unresolved":14926,"graph_unindexed":269,"counts_floor":true},"tests_to_run":[{"p":"test/verify_radix.cpp","hops":1,"run":"bash test/greptiercheck.sh"}, +"graph_ambiguous":2925,"graph_unresolved":15128,"graph_unindexed":269,"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":775,"graph_ambiguous":2882,"graph_unresolved":14926,"graph_unindexed":269,"counts_floor":true,"next":"--uses=src/infra/sortutil.h:nonNegativeFloatDescKey"} +"order":"evidence","partners":0,"run_first":1,"tests":5,"script_gates_unmodelled":779,"graph_ambiguous":2925,"graph_unresolved":15128,"graph_unindexed":269,"counts_floor":true,"next":"--uses=src/infra/sortutil.h:nonNegativeFloatDescKey"} ``` ### `--insert-after-symbol=TARGET` @@ -3195,12 +3184,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":2882,"graph_unresolved":14926,"graph_unindexed":269,"counts_floor":true},"tests_to_run":[{"p":"test/verify_radix.cpp","hops":1,"run":"bash test/greptiercheck.sh"}, +"graph_ambiguous":2925,"graph_unresolved":15128,"graph_unindexed":269,"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":775,"graph_ambiguous":2882,"graph_unresolved":14926,"graph_unindexed":269,"counts_floor":true,"next":"bash test/greptiercheck.sh"} +"order":"evidence","partners":0,"run_first":1,"tests":5,"script_gates_unmodelled":779,"graph_ambiguous":2925,"graph_unresolved":15128,"graph_unindexed":269,"counts_floor":true,"next":"bash test/greptiercheck.sh"} ``` **Caveats (stated by the binary):** @@ -3246,12 +3235,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":2882,"graph_unresolved":14926,"graph_unindexed":269,"counts_floor":true},"tests_to_run":[{"p":"test/verify_radix.cpp","hops":1,"run":"bash test/greptiercheck.sh"}, +"graph_ambiguous":2925,"graph_unresolved":15128,"graph_unindexed":269,"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":775,"graph_ambiguous":2882,"graph_unresolved":14926,"graph_unindexed":269,"counts_floor":true,"next":"--uses=src/infra/sortutil.h:nonNegativeFloatDescKey"} +"order":"evidence","partners":0,"run_first":1,"tests":5,"script_gates_unmodelled":779,"graph_ambiguous":2925,"graph_unresolved":15128,"graph_unindexed":269,"counts_floor":true,"next":"--uses=src/infra/sortutil.h:nonNegativeFloatDescKey"} ``` **Shaped by:** `--insert-after-symbol` @@ -3310,11 +3299,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":2882,"graph_unresolved":14926,"graph_unindexed":269,"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: 521 more bytes on this line] +{"schema":"ripwire.edit-plan/v1","mode":"apply","edits":1,"files":1,"callers_union":4,"graph_ambiguous":2925,"graph_unresolved":15128,"graph_unindexed":269,"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":2882,"graph_unresolved":14926,"graph_unindexed":269,"counts_floor":true}}]} +"graph_ambiguous":2925,"graph_unresolved":15128,"graph_unindexed":269,"counts_floor":true}}]} ``` **Shaped by:** `--edit-check` @@ -3360,19 +3349,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] ``` @@ -3397,17 +3386,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] @@ -3451,10 +3440,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:10410 +$ ./build/ripwire . --at=src/graph.h:11751 - - + + ``` @@ -3473,25 +3462,14 @@ The bundle is BUDGETED by default (8000 tokens, budget_default="1" on the root; **Try it** -_No-LLM review-evidence bundle for the working-tree diff — recorded against a DIRTY tree, so it is populated rather than empty._ +_No-LLM review-evidence bundle for the working-tree diff (clean tree = empty)._ ``` $ ./build/ripwire . --pr-context - - - - - - - - - - - - - -... [17 more line(s); run it to see the whole thing] + + + ``` **Shaped by:** `--test-gate`, `--from-trace`, `--limit`, `--map-diff`, `--index-out` @@ -3515,20 +3493,16 @@ _Pairwise cross-arm conflict sites + suggested landing order (any committish sha ``` $ ./build/ripwire . --merge-scout=HEAD~2,HEAD~1 - - + + - + - - - - - -... [2 more line(s); run it to see the whole thing] + + ``` **Shaped by:** `--plan-lanes`, `--plan` @@ -3598,7 +3572,7 @@ _Which refs of the `lane/` ref family still hold divergent authored work vs HEAD ``` $ ./build/ripwire . --stray-content=lane/ - + @@ -3632,14 +3606,14 @@ _Select the genuinely-unmerged refs of the `lane/` ref family and feed them to m ``` $ ./build/ripwire . --stray-content=lane/ --plan - + - + @@ -3667,7 +3641,21 @@ _Cross-branch ABI-break gate over the `lane/` ref family: struct byte-contract d ``` $ ./build/ripwire . --stray-content=lane/ --abi -(empty) + + + + + + + + + + + + + +... [2 more line(s); run it to see the whole thing] ``` **Caveats (stated by the binary):** @@ -3688,13 +3676,13 @@ _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] ``` @@ -3718,11 +3706,11 @@ _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] @@ -3746,12 +3734,12 @@ _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] @@ -3779,7 +3767,7 @@ _The dark-content dashboard: gates BUILT but OFF. CHANGED: no longer invents gat $ ./build/ripwire . --flags - + @@ -3860,7 +3848,7 @@ _The cache-locality lens over every aggregate: fields READ TOGETHER but declared ``` $ ./build/ripwire . --field-affinity - @@ -3895,7 +3883,7 @@ _Which of this repo's doc claims are now false. CHANGED: row attribute at= renam ``` $ ./build/ripwire . --doc-drift - + @@ -3929,7 +3917,7 @@ _Which of this repo's doc claims are now false. CHANGED: row attribute at= renam ``` $ ./build/ripwire . --doc-drift - + @@ -3962,8 +3950,8 @@ _Same report, with git history splitting stale mentions into deleted-by-commit v ``` $ ./build/ripwire . --doc-drift --with-history - - + + @@ -3996,7 +3984,7 @@ _The house PLAN/DESIGN format's STRUCTURE check — never semantics; exit 2 when ``` $ ./build/ripwire . --plan-lint=test/planlintfix/wave.md - + @@ -4031,8 +4019,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:10410 - #1 0x102f3e884 in rw::rankGraph(Graph const&, float) src/graph.h:10451 + #0 0x102f4a1c8 in rw::rankGraphTeleport(Graph const&, std::vector const&, float) src/graph.h:11751 + #1 0x102f3e884 in rw::rankGraph(Graph const&, float) src/graph.h:11792 #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) @@ -4061,7 +4049,7 @@ _A command that exits 0: a minimal success record (exit, measured duration, disc $ ./build/ripwire . --run-trace="true" - + ``` @@ -4151,7 +4139,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 @@ -4188,11 +4176,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 @@ -4221,7 +4209,7 @@ _Task lens + a compact Mermaid flowchart of the top anchors' 1-hop edges._ ``` $ ./build/ripwire . --for="pagerank power iteration" --with-graph - @@ -4294,14 +4282,14 @@ _Environment self-check: binary staleness, grammars, cache dir, git, tracked-bin ``` $ ./build/ripwire . --doctor - - + - - + + - - + @@ -4328,14 +4316,14 @@ _--doctor plus a LIVE integration inspection for one agent: PATH binary, install ``` $ ./build/ripwire . --doctor --agent=claude - - + - - + + - - + @@ -4363,7 +4351,7 @@ $ ./build/ripwire . --skipped - @@ -4451,13 +4439,13 @@ _Rank by git change-frequency prior instead of PageRank._ $ ./build/ripwire . --rank-by=churn --top-k=5 - - + + - + - + @@ -4487,19 +4475,19 @@ _Scope the recent-changes answer to ONE directory. The global block sta $ ./build/ripwire . --rank-by=churn-decay --since=HEAD~6 --exclude=test --exclude=docs --exclude=skills --exclude=changelog.d --in=src --limit=3 - - - - - - + + + + + + + + + + + - - - - - -... [15 more line(s); run it to see the whole thing] +... [17 more line(s); run it to see the whole thing] ``` **Shaped by:** `--external-surface`, `--doctor`, `--legend`, `--max-memory` @@ -4561,7 +4549,7 @@ _The same gating report under --legend=full — same rows, same exit 2; the defa ``` $ ./build/ripwire . --quality-delta --legend=full - @@ -4589,7 +4577,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=a612d2cfd7867548 entries=923 +ripwire legend dictionary ripwire.dict/v1 dictv=2259c5025ac90271 entries=930 : 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=) @@ -4641,7 +4629,7 @@ _The family join: per function, which of four orthogonal evidence families fire, ``` $ ./build/ripwire . --ensemble --limit=8 - @@ -4652,7 +4640,7 @@ $ ./build/ripwire . --ensemble --limit=8 - + ... [17 more line(s); run it to see the whole thing] ``` @@ -4677,14 +4665,14 @@ _Drop matching paths (repeatable) before ranking._ $ ./build/ripwire . --exclude=present --exclude=bench --top-k=5 - - + + - + - + @@ -4702,25 +4690,25 @@ this is NOT a filter to only-changed symbols. changed="N" in the header names th **Try it** -_Full map re-ranked with teleport toward git-changed files — recorded against a DIRTY tree, so changed= counts the working copy's files and the teleport is live._ +_Full map re-ranked with teleport toward git-changed files — clean tree, so changed=0 and it degrades to the plain map._ ``` $ ./build/ripwire . --map-diff --top-k=5 - + - - - - - - - + + + + - + + + + + - - -... [5 more line(s); run it to see the whole thing] + +... [8 more line(s); run it to see the whole thing] ``` **Shaped by:** `--json`, `--limit` @@ -4742,8 +4730,8 @@ _Explicit incremental cache at a path OUTSIDE the repo (first call writes it)._ $ ./build/ripwire . --cache=/aux/warm2.ripwirecache --top-k=3 - - + + @@ -4792,8 +4780,8 @@ _Force a cold parse (bypass the warm TMPDIR cache) — shows the cold-vs-warm co $ ./build/ripwire . --no-cache --top-k=3 - - + + @@ -4823,8 +4811,8 @@ _Crawl paths the repo's own .gitignore covers (default honours it and discloses $ ./build/ripwire . --no-ignore --top-k=3 - - + + @@ -4859,14 +4847,14 @@ _Skip files above a size bound before parsing (note the corpus shrink in the hea $ ./build/ripwire . --max-file-size=8K --top-k=3 - - + + - + - + @@ -4939,8 +4927,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 - - + + @@ -4993,10 +4981,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":" + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [735 more display lines; full output is 23597 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --top-k=5` + +*Same map, capped to the 5 highest-ranked symbols.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [67 more display lines; full output is 8256 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.* + +````` + + + + + + + + + + + + + + + + + + + + + + +````` + +## `./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.* + +````` + + + + + + + + + + +````` + +## `./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** + +````` + + +````` + +stderr: + +````` +ripwire: --token-budget exceeded: withheld_est_tokens=9507 > 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: 3.22s** + +````` + + + + +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 +inline McpRebuildBaseline mcpRebuildBaseline( const McpIndex& ix, bool isIncrementalPass ) +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] + +persistent in-memory index (parse once, reuse across MCP calls) ---- The MCP server is long-live…struct McpIndex +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 CachePathKeys buildCachePathKeys( const std::vector<std::string>& files, std::string_view rootDir ) +inline std::string reAbsolutize( std::string_view rel, std::string_view root ) +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 std::uint64_t byteHash( const char* data, std::size_t n ) noexcept +inline void collectExtractPartials( const IngestFileScan& scan, IngestResult& result ) +void emitGrepHandleLegend( bool enabled ) +… [127 more display lines; full output is 20072 bytes on 8 raw line(s)] +````` + +## `./build/ripwire . --for="rankGraphTeleport"` + +*Name-shaped query: the router picks name-exact BM25 (header says which/why).* + +**wall time: 1.66s** + +````` + + + + + +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 ) +… [28 more display lines; full output is 5868 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: 1.64s** + +````` + + + + + +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 +### Fixed — `rank_by` over MCP no longer answers a different ranking than `--rank-by` on … [line truncated: 36 more bytes on this line] +### 2. Gates for A +inline std::vector<float> anchoredLexicalRank( const Graph& g, const std::vector<float>& lex ) +inline int runEval( const std::string& root, const IngestResult& ing, const Graph& g, const std::vector<char>& currentDiff ) +#### The convergence disclosure contract +### Wave-2 adversarial verification (2026-08-19) — six probes against `aa97c9e` +… [155 more display lines; full output is 24115 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: 1.68s** + +````` + + + + + +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: 1.45s** + +````` + + + + +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 +inline bool hasMainGuard( std::string_view source ) +constexpr std::uint32_t kMaxYamlNestDepth = 64u +SpanTierBatch spanTiersOfFiles( std::span<const std::string> diskPaths, bool useMemo ) +enum class DisclosureWhy : std::uint8_t +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 ) +struct LintRule +inline FileHealth measureHealthAdoptingMemberMacroReparse( TSParser* parser, Lang lang, std::string_view bytes, TreeGuard& tree, MemberMacroRepa … [line truncated: 19 more bytes on this line] +inline std::string buildMemoryStopAttr( const IngestResult& ing, bool json ) +… [138 more display lines; full output is 19980 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: 1.77s** + +````` + + + + +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 double bm25ImpactBound( double idf, double T, const Bm25Params& p ) noexcept +inline int runEval( const std::string& root, const IngestResult& ing, const Graph& g, const std::vector<char>& currentDiff ) +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 +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 ) +std::optional<int> runEvalViews( const MainDispatch& d ) +struct RecallSectionPick +inline double binomialChoose( std::size_t n, std::size_t k ) +… [146 more display lines; full output is 21540 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.03s** + +````` + + + + +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 double bm25ImpactBound( double idf, double T, const Bm25Params& p ) noexcept +inline int runEval( const std::string& root, const IngestResult& ing, const Graph& g, const std::vector<char>& currentDiff ) +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 +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 ) +std::optional<int> runEvalViews( const MainDispatch& d ) +struct RecallSectionPick +inline double binomialChoose( std::size_t n, std::size_t k ) +… [155 more display lines; full output is 21617 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: 1.18s** + +````` + + + + + +````` + +## `./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.10s** + +````` + + + + +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: 1.02s** + +````` + + + + +````` + +## `./build/ripwire . --recall="quality delta gating exit codes"` + +*Most relevant DOCS' full bodies (markdown only) — recall what is already written down.* + +````` +ripwire recall — "quality delta gating exit codes" — 121 relevant of 264 document files, best-first — total=121 shown=8 capped=1 truncated=6 generated_demoted=3 max_tokens=8000 share_bytes=2468 est_tokens=4165 + +━━ README.md (relevance 8.094) ━━ [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.527) ━━ [sections: 1 of 109 selected (451 in doc), section-granular; whole doc 998064 B; lines="558-579"; dropped_by_budget=108] [truncated: 2264 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 +… [114 more lines, 10620 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 1355363-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=952 + +━━ commands.md (relevance 10.880) ━━ [sections: 1 of 165 selected (192 in doc), section-granular; whole doc 594792 B; lines="555-580"; dropped_by_budget=164] [truncated: 1854 of 4256 bytes] +#### Pattern: a directory of dumped tool output as a knowledge base + +**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? + +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: + +1. **Dump to `.md`.** `--recall` ranks DOCUMENT files: `.md`, plus the docparse'd + `.ipynb`/`.html`/`.csv` (and Office/PDF through the optional markitdown bridge). `.txt`, + `.log`, `.json` and extensionless files are **not** documents to it. A directory of those + answers `0 relevant of 0 document files` and exits 0 — which reads like "nothing matched + your terms" when what happened is "nothing was indexed at all". Redirect to `notes.md`, + never `notes.txt`. The recorded run below is that rule's own demonstration: its scratch dir + holds five dumps, and the header says `2 relevant of 2 document files` because only the two + `.md` ones are documents — the `git log`, the `--help` text and the JSON access log are not + in the population at all. + +2. **Keep `##` headings in the dump.** A headed document is served as whole ranked SECTIONS, so + an answer buried mid-file arrives at a small `--max-tokens`, and — while the served document + SET stays fixed — a larger ceiling returns a strict superset of it; the `[sections: S of R + selected (N in doc) … lines="…"; dropped_by_budget=D]` note names the ranges you actually got + and what the ceiling cost. … + +(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 11197 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --html=/aux/map2.html` + +*Self-contained HTML force-directed call graph.* + +**wall time: 2.35s** + +````` +(empty) +````` + +Artifact written: + +````` + 156153 /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.00s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [67 more display lines; full output is 8274 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.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [164 more display lines; full output is 7789 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.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [1947 more display lines; full output is 53159 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: 1.67s** + +````` + + +````` + +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.02s** + +````` + + + + + + + + + + + + +````` + +## `./build/ripwire . --graph-query='and(callers(name("rankGraphTeleport"),2),kind(all,fn))'` + +*Composable node-set query: functions within 2 caller-hops of rankGraphTeleport.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [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 5604 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 5478 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").* + +````` + + + + + + + + +````` + +## `./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 6423 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --mentions=rankGraphTeleport` + +*Markdown docs that name SYM in a backtick (doc<->code edges).* + +````` + + + + + + + + + + +````` + +## `./build/ripwire . --affected=src/graph.h` + +*Test files that transitively reach the changed file.* + +````` + + + + + + + + + +````` + +## `./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: 1.29s** + +````` +ripwire situational-awareness — 0 changed file(s), 0 symbols in them +root: . +at: c97f6f91c + (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: 3.81s** + +````` + + +````` + +## `./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.* + +````` + +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.91s** + +````` + +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.11s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [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: 1.88s** + +````` + + + + + +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 +inline std::string powerShellSingleQuote( std::string_view s ) noexcept +inline std::string path_prepend_hint( std::string_view dir ) +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] +handle_t leftmost_leaf() const +inline void discloseUniformChurnFallback( bool hasChurnEvidence, bool stubbed, const char* verbLabel, const std::string& windowStamp ) +inline std::string doctorPathPrependLine( const std::string& selfPath ) +inline std::string doctorWhichRipwire( rw::os::stat_t& st ) +inline const char* rankByLegendFor( const char* label ) noexcept +std::uint32_t testIterationCeiling() noexcept +… [156 more display lines; full output is 22215 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.07s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [148 more display lines; full output is 14953 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.* + +````` + + + + +& 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).* + +````` + + + + +& 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.* + +````` + + + + +: 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=). --> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [97 more display lines; full output is 10931 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --deps` + +*File->file dependency graph (god-files, cycles).* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [899 more display lines; full output is 24018 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --hotspots` + +*Complexity x recent git churn (maintenance pain).* + +**wall time: 2.24s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [14 more display lines; full output is 5767 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --clones` + +*Token-normalized duplicate bodies.* + +**wall time: 2.41s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [308 more display lines; full output is 16222 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --cochange` + +*Files that change together in git (hidden coupling).* + +**wall time: 2.45s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +````` + +## `./build/ripwire . --hotspots --since="2 weeks ago"` + +*Hotspots scoped to RECENT churn (the regression lens).* + +**wall time: 2.42s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [13 more display lines; full output is 5151 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.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [1029 more display lines; full output is 152096 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --lint` + +*Built-in AST checks (c-cast, goto, unsafe-c-fn, ...).* + +**wall time: 2.63s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [685 more display lines; full output is 70581 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.09s** + +````` + + + + +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 16896 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 7291 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.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [271 more display lines; full output is 12259 bytes on 1 raw line(s)] +````` + +## `./build/ripwire . --report` + +*Architecture summary (modules, god-files, cycles) as markdown.* + +````` + + +# ripwire architecture report + +3589 files · 31640 symbols · 29882 edges · 3588 modules (14830 call-graph isolated) + +Root: `.` + +Call-graph isolate provenance: 3951 declaration, 2444 header, 3330 source, 5105 document; 0 connected Louvain singletons + +## Modules (call-graph clusters; showing 12 of 3588) +- **src::emitTo@infra/emit.h:53:2760 [write,run,emit]** — 1617 symbols +- **src::kindIs@infra/nodekind.h:46:3271 [push,emit,collect]** — 547 symbols +- **src::DISCLOSE@infra/Diagnostics.h:616:43369 [read,compute,resolve]** — 335 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::child@ingest_valuerefs.h:656:33874 [emit,resolve,open]** — 62 symbols +- **src::rowOffsets@infra/sparseCsr.h:248:8990 [connect,compute,expand]** — 54 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::tokText@defectshape.h:128:5308 [scan,find,join]** — 41 symbols + +## God files (most depended-on; showing 10 of 228) +- `src/model.h` — 103 dependents +- `src/infra/emit.h` — 87 dependents +- `src/infra/Diagnostics.h` — 65 dependents +- `src/graph.h` — 46 dependents +- `src/serialize.h` — 45 dependents +… [35 more lines, 3965 bytes total] +````` + +## `./build/ripwire . --seams` + +*Cross-module call seams no test reaches. NOW carries seam_pairs/shown/capped.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [57 more display lines; full output is 9051 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"] + n102["src
9384"] + n103["src/infra
1301"] + end + subgraph sg1 ["test"] + n104["test
5501"] + n559["test/legochargefix
546"] + n400["test/fixtures/recallpassage
314"] + n944["test/slotjoinfix
175"] + n786["test/ownerhopfix/py/pkg
158"] + n342["test/expandmodefix
151"] + n344["test/expandsibsfix
149"] + n927["test/rubyschemafix
99"] + n419["test/forbodyfoldfix/py
98"] + n909["test/rubyattrsfix
85"] + n722["test/lib
81"] + end + n74["docs
858"] + n2[".github
625"] + 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"] +… [20 more lines, 1592 bytes total] +````` + +## `./build/ripwire . --owners` + +*Bus-factor: recency-weighted author ownership per file.* + +**wall time: 2.34s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [971 more display lines; full output is 106097 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: 26.04s** + +````` + + +````` + +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.68s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [139 more display lines; full output is 16104 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: 14.43s** + +````` + + + + + + + + + +````` + +## `./build/ripwire . --pr-context` + +*No-LLM review-evidence bundle for the working-tree diff (clean tree = empty).* + +**wall time: 2.16s** + +````` + + + +````` + +## `./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: 3.76s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [211 more display lines; full output is 14974 bytes on 1 raw line(s)] +````` + +## `./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: 19.11s** + +````` + + + + + + + + + + + +````` + +## `./build/ripwire . --stray-content=lane/` + +*Which refs of the `lane/` ref family still hold divergent authored work vs HEAD, with verdicts.* + +**wall time: 29.87s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [200 more display lines; full output is 27194 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: 2.32s** + +````` + + + + + + + + + + + + + + + + + + +````` + +## `./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: 122.29s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [33520 more display lines; full output is 4640491 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.12s** + +````` +(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: 91.49s** + +````` + + + + + + + + + + + + + + + +````` + +## `./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: 10.27s** + +````` + + + + + + + + + + + + + + + + + + + + +````` + +## `./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: 10.16s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [36 more display lines; full output is 13362 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.93s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [35 more display lines; full output is 14375 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: 18.14s** + +````` + + + + + + + +````` + +## `./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.29s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [400 more display lines; full output is 24541 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.07s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +````` + +## `./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.08s** + +````` +(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: 2.69s** + +````` +{"v":1,"verb":"plan-lanes","at":"c97f6f91c","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":3589,"symbols":31640,"edges":298 … [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":57,"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":1793,"ord":0,"overloads":1,"amb":0,"cx":4,"ccx":4,"churn":61,"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":453,"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":453,"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/mcpverbs.h","n":"unknownSubVerbRefusal","scope":"rw","key":"e336d39b0a6addea","id":"./src/mcpverbs.h::rw::unknownSubVerbRefusal","id_addressable":true,"id_collides_with":0,"l":5838,"ord":0,"overloads":1,"amb":0,"cx":3,"ccx":2,"churn":459,"tested":0}], +"files":[{"p":"./src/darkflags.h","symbols":1,"churn":32,"ccx":8,"hotspot_rank":55}, +{"p":"./src/docdrift.h","symbols":4,"churn":36,"ccx":14,"hotspot_rank":38}, +{"p":"./src/editcheck.h","symbols":1,"churn":57,"ccx":10,"hotspot_rank":41}, +{"p":"./src/main.cpp","symbols":2,"churn":453,"ccx":12,"hotspot_rank":5}, +{"p":"./src/mcpverbs.h","symbols":1,"churn":459,"ccx":2,"hotspot_rank":4}, +{"p":"./src/pageview.h","symbols":1,"churn":28,"ccx":2,"hotspot_rank":129}, +{"p":"./src/testmap.h","symbols":1,"churn":61,"ccx":4,"hotspot_rank":33}, +{"p":"./src/verbs_change.h","symbols":1,"churn":82,"ccx":9,"hotspot_rank":25}]},"blast_radius":{"reaches":47,"files_total":16,"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 … [line truncated: 182 more bytes on this line] +"tests_total":0,"tests_capped":false,"tests_granularity":"claimed-symbols","untested":47,"module_span":1,"notes":[], +"execution":{"policy":"codex-lane/v1","model":"gpt-5.6-terra","reasoning":"high","rule":"moderate-cross-module","basis":"structural-only","signals":{"claims":12,"files":8,"module_span":1,"max_ccx":11,"sum_ccx":61,"ambiguous_calls":6,"blast_reaches":47,"contract_touches":0,"conflicts":0,"untested":47 … [line truncated: 142 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/gitmine.h","n":"gitWindowBoundarySha","scope":"rw","key":"18627516699d7062","id":"./src/gitmine.h::rw::gitWindowBoundarySha","id_addressable":true,"id_collides_with":0,"l":1674 … [line truncated: 69 more bytes on this line] +{"p":"./src/cli.h","n":"kViewFlags","scope":"rw","key":"2a2f2487082cc6d8","id":"./src/cli.h::rw::kViewFlags","id_addressable":true,"id_collides_with":0,"l":3327,"ord":0,"overloads":1,"amb":0,"cx":0,"ccx":0,"churn":511,"tested":0}, +… [69 more display lines; full output is 20481 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: 3.28s** + +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":"c97f6f91c","root":".","task":null,"source":"brief","requested":3,"lane_count":3,"claim_key":"path+scope+name","on_conflict":"producing-lane-rebases","corpus":{"files":3589,"symbols":31640,"edges":29882,"ambiguous":2925,"unresolved":15128},"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":459,"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":3327,"ord":0,"overloads":1,"amb":0,"cx":0,"ccx":0,"churn":511,"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":82,"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":5838,"ord":0,"overloads":1,"amb":0,"cx":3,"ccx":2,"churn":459,"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":511,"ccx":0,"hotspot_rank":6}, +{"p":"./src/docdrift.h","symbols":6,"churn":36,"ccx":54,"hotspot_rank":38}, +{"p":"./src/ensemble.h","symbols":1,"churn":23,"ccx":0,"hotspot_rank":105}, +{"p":"./src/mcpverbs.h","symbols":2,"churn":459,"ccx":2,"hotspot_rank":4}, +{"p":"./src/recall.h","symbols":1,"churn":27,"ccx":4,"hotspot_rank":64}, +{"p":"./src/verbs_change.h","symbols":1,"churn":82,"ccx":4,"hotspot_rank":25}], +"symbols_total":3455,"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":6,"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":6,"max_ccx":38,"sum_ccx":64,"ambiguous_calls":12,"blast_reaches":13,"contract_touches":1,"conflicts":0,"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":133,"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":2948,"ord":0,"overloads":1,"amb":0,"cx":0,"ccx":0,"churn":511,"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":3027,"ord":0,"overloads":1,"amb":0,"cx":0,"ccx":0,"churn":511,"tested":0}, +{"p":"./docs/docs_commands_build.py","n":"main","scope":"","key":"4015853681ded3bc","id":null,"id_addressable":false,"id_collides_with":133,"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":49,"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":3092,"ord":0,"overloads":1,"amb":1,"cx":4,"ccx":3,"churn":511,"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}, +{"p":"./src/cli.h","n":"printUsageTier","scope":"rw","key":"82b7af67acdca479","id":"./src/cli.h::rw::printUsageTier","id_addressable":true,"id_collides_with":0,"l":3078,"ord":0,"overloads":1,"amb":0,"cx":2,"ccx":1,"churn":511,"tested":0}, +… [60 more display lines; full output is 18606 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.* + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +````` + +## `./build/ripwire . --layout=Lang` + +*The honest refusal (exit 1): Lang is an `enum class`, not a struct — no offsets are fabricated.* + +**exit code: 1** + +````` +(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: 1.28s** + +````` + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + +… [226 more display lines; full output is 23854 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: 1.23s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [253 more display lines; full output is 25109 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: 1.84s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +… [206 more display lines; full output is 23833 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:11751 + #1 0x102f3e884 in rw::rankGraph(Graph const&, float) src/graph.h:11792 + #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=.* + +````` + + + + +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] +inline std::string classNewGroupXml( const IngestResult& ing, std::span<const ClassNewRow> rows, std::string_view rootPrefix, EscapeFn ex ) + + + + + + + + + +… [67 more display lines; full output is 12092 bytes on 59 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.17s** + +````` + 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 +inline std::string powerShellSingleQuote( std::string_view s ) noexcept +inline std::string path_prepend_hint( std::string_view dir ) +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] +handle_t leftmost_leaf() const +inline void discloseUniformChurnFallback( bool hasChurnEvidence, bool stubbed, const char* verbLabel, const std::string& windowStamp ) +inline std::string doctorPathPrependLine( const std::string& selfPath ) +inline std::string doctorWhichRipwire( rw::os::stat_t& st ) +inline const char* rankByLegendFor( const char* label ) noexcept +std::uint32_t testIterationCeiling() noexcept +… [124 more display lines; full output is 19081 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.28s** + +````` +(empty) +````` + +Artifact written: + +````` + 556185 /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: 2.65s** + +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 ) +HashMap<const TSLanguage*, TSQuery*>& compiledQueryCache() +… [107 more display lines; full output is 51596 bytes on 1 raw line(s)] +````` + +stderr: + +````` +ripwire: cache : over its 2048 MiB budget — evicted 199 blob(s) of other roots or other ripwire builds (this build's blobs for this root are kept) +````` + + +--- + +# 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: 10.26s** + +````` + + + + + + + + + + +````` + + +--- + +# 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: 2.36s** + +````` + + + + + + + + + + + + + + + + + + + + + +````` + +## `./build/ripwire . --rank-by=churn-decay --since=HEAD~6 --exclude=test --exclude=docs --exclude=skills --exclude=changelog.d --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: 2.08s** + +````` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +````` + +## `./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).* + +````` + + +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,11790,12436,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:11793,src/graph.h:12487,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: 1.91s** + +````` + + + +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.* + +````` +{"of":"rankGraphTeleport","defs":1,"count":7,"root":".","hop_tested":0,"hop_untested":7,"graph_ambiguous":2925,"graph_unresolved":15128,"graph_unindexed":269,"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:11790","sites_at":["src/graph.h:11793"]}, +{"t":"fn","n":"anchoredLexicalRank","p":"src/graph.h:12436","sites_at":["src/graph.h:12487"]}, +{"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