From 7e4c842472607b50ac95480d82f3c2005bc8e93c Mon Sep 17 00:00:00 2001 From: Michal Papis Date: Tue, 20 Oct 2026 15:27:58 +0200 Subject: [PATCH 01/19] test(lspcheck): red-first arms for the tower order, the disclosure, parity MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Gate-first commit for RIPWIRE-RUBY task C. Five new arms on the copied rubyschemafix collision tree: (18) byte-pinned picker order — Method def before Var before schema column before yaml section on workspace/symbol, definition front and hover front; (19) the ONE multi-def disclosure sentence in the hover markdown plus one window/logMessage ahead of each definition answer, absent on single-def names; (20) hover Used-at parity with every CLI --uses call row; (21) C/C++ decl/def refinement composes with the tower sort; (22) determinism x2 on the new dialog. On b59b0824: (18) and both (19) assertions FAIL (attr leads, no disclosure), (20)(21)(22) PASS (parity pins existing behavior; (21) is the no-regression floor), (1)-(17) stay green as the regression floor. Authored by @mpapis with AI assistance (DeepSeek V4.1 Flash / Qwen 3.8 Flash) under his supervision. --- test/lspcheck.sh | 102 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) diff --git a/test/lspcheck.sh b/test/lspcheck.sh index a5bcc8e38..69bd68f2d 100755 --- a/test/lspcheck.sh +++ b/test/lspcheck.sh @@ -38,6 +38,16 @@ # inside a wall-clock cap (no hang) # (17) workspace/symbol's 20-row cap is DISCLOSED: 25 matches answer 20 rows after a window/logMessage naming # "first 20 of 25"; 10 matches answer all 10 with no notice +# (18) picker order (task C): on the copied rubyschemafix collision tree, workspace/symbol, definition and +# hover all lead with the Method def — the tower (kindSpecificity desc, reference-language first, id asc) +# — with the Var next, the schema column before the yaml section, byte-pinned +# (19) multi-def disclosure (task C): a name with N>1 defs carries ONE sentence — N definitions, counts are +# floors, the resolver splits — in the hover markdown and in one window/logMessage ahead of every +# definition answer; absent on single-def names +# (20) Used-at parity (task C): hover Used-at for the colliding name answers every CLI --uses call row +# (21) C/C++ decl/def no-regression (task C): a header declaration with the LOWER id still yields its bodyful +# definition — the resolveFocus refinement composes with the tower sort +# (22) determinism x2 on the new collision dialog (house pattern) # # Usage: RIPWIRE_BIN=build/ripwire bash test/lspcheck.sh | bash test/lspcheck.sh path/to/ripwire @@ -341,5 +351,97 @@ noticeLine="$( grep -n '"method":"window/logMessage"' "$WORK/r17" | cut -d: -f1 && [ -n "$noticeLine" ] && [ "$noticeLine" -lt "$resp30Line" ] \ && ok "(17) workspace/symbol: 25 matches → 20 rows after one window/logMessage 'first 20 of 25'; 10 matches (capfn10-19) → 10 rows, no notice" \ || { no "(17) cap disclosure wrong: rows30=$rows30 rows31=$rows31 notices=$notices"; head -c 500 "$WORK/r17"; } +# ── (18) picker order: the tower sorts every multi-def answer, byte-pinned on the collision fixture ─── +cp -r "$ROOT/test/rubyschemafix" "$WORK/collide" +uriColl="file://$WORK/collide" +uriCA="file://$WORK/collide/consumer_ambiguous.rb" +{ + msg "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"initialize\",\"params\":{\"rootUri\":\"$uriColl\",\"capabilities\":{}}}" + msg "{\"jsonrpc\":\"2.0\",\"method\":\"initialized\",\"params\":{}}" + msg "{\"jsonrpc\":\"2.0\",\"method\":\"textDocument/didOpen\",\"params\":{\"textDocument\":{\"uri\":\"$uriCA\",\"languageId\":\"ruby\",\"version\":1,\"text\":\"\"}}}" + msg "{\"jsonrpc\":\"2.0\",\"method\":\"textDocument/didOpen\",\"params\":{\"textDocument\":{\"uri\":\"file://$WORK/collide/all_four.rb\",\"languageId\":\"ruby\",\"version\":1,\"text\":\"\"}}}" + msg "{\"jsonrpc\":\"2.0\",\"id\":40,\"method\":\"workspace/symbol\",\"params\":{\"query\":\"name\"}}" + msg "{\"jsonrpc\":\"2.0\",\"id\":41,\"method\":\"textDocument/definition\",\"params\":{\"textDocument\":{\"uri\":\"$uriCA\"},\"position\":{\"line\":7,\"character\":10}}}" + msg "{\"jsonrpc\":\"2.0\",\"id\":42,\"method\":\"textDocument/hover\",\"params\":{\"textDocument\":{\"uri\":\"$uriCA\"},\"position\":{\"line\":7,\"character\":10}}}" + msg "{\"jsonrpc\":\"2.0\",\"id\":43,\"method\":\"textDocument/hover\",\"params\":{\"textDocument\":{\"uri\":\"file://$WORK/collide/all_four.rb\"},\"position\":{\"line\":9,\"character\":8}}}" + msg "{\"jsonrpc\":\"2.0\",\"id\":99,\"method\":\"shutdown\"}" + msg "{\"jsonrpc\":\"2.0\",\"method\":\"exit\"}" +} > "$WORK/d18" +"$BIN" --lsp < "$WORK/d18" > "$WORK/raw18" 2>/dev/null +decodeFrames "$WORK/raw18" > "$WORK/r18" 2>/dev/null +head40="$( grep '"id":40,' "$WORK/r18" )"; head41="$( grep '"id":41,' "$WORK/r18" | head -c 260 )"; R42="$( grep '"id":42,' "$WORK/r18" )"; R43="$( grep '"id":43,' "$WORK/r18" )" +p_m="$( printf '%s' "$head40" | grep -bo 'all_four.rb","range":{"start":{"line":9' | cut -d: -f1 | head -1 )" +p_v="$( printf '%s' "$head40" | grep -bo 'all_four.rb","range":{"start":{"line":7' | cut -d: -f1 | head -1 )" +p_c="$( printf '%s' "$head40" | grep -bo 'schema.rb","range":{"start":{"line":14' | cut -d: -f1 | head -1 )" +p_y="$( printf '%s' "$head40" | grep -bo 'spike_names.yml","range":{"start":{"line":3' | cut -d: -f1 | head -1 )" +[ -n "$p_m" ] && [ -n "$p_v" ] && [ -n "$p_c" ] && [ -n "$p_y" ] \ + && [ "$p_m" -lt "$p_v" ] && [ "$p_v" -lt "$p_c" ] && [ "$p_c" -lt "$p_y" ] \ + && printf '%s' "$head41" | grep -q '"line":9' \ + && printf '%s' "$R43" | grep -q '\*\*method\*\* `AllFour::name`' \ + && ok "(18) picker order: Method before Var before column-Section before yaml-Section on ws/symbol, def-front and hover-front" \ + || { no "(18) tower order wrong: m=$p_m v=$p_v c=$p_c y=$p_y"; printf '%s\n' "$head40" | head -c 400; printf '%s\n' "$head41" | head -c 240; } + +# ── (19) the ONE multi-def disclosure sentence, hover + definition logMessage, absent on single-def ─── +printf '%s' "$R42" | grep -q '19 definitions for .name. — counts are floors; the resolver splits over them' \ + && ok "(19) hover discloses the def count + split sentence on the colliding name" \ + || { no "(19) hover disclosure missing"; printf '%s\n' "$R42" | head -c 400; } +n19="$( grep -c 'window/logMessage.*19 definitions for .name. — counts are floors; the resolver splits over them' "$WORK/r18" )" +l41="$( grep -n '"id":41,' "$WORK/r18" | cut -d: -f1 )"; l19="$( grep -n 'window/logMessage.*19 definitions' "$WORK/r18" | head -1 | cut -d: -f1 )" +[ "$n19" = 2 ] && [ -n "$l19" ] && [ -n "$l41" ] && [ "$l19" -lt "$l41" ] \ + && ok "(19) definition answers carry ONE window/logMessage disclosure AHEAD of each answer (2 dialogs)" \ + || { no "(19) definition logMessage wrong: n=$n19 noticeLine=$l19 respLine=$l41"; } +printf '%s' "$( R 5 )" | grep -q 'definitions for .scale.' && no "(19) single-def name carries a multi-def disclosure" || ok "(19) scale (single def) carries no disclosure" + +# ── (20) Used-at parity: hover answers every CLI --uses call row for the colliding name ──────────────── +UR="$( cd "$WORK/collide" && "$BIN" --uses=name . 2>/dev/null | grep -oE ' "$WORK/cppdd/a.h" <<'EOF' +// decl/def fixture +int quad( int f ); +EOF +cat > "$WORK/cppdd/a.cpp" <<'EOF' +// the def carries the body; the header decl above carries the lower id +int quad( int f ) +{ + return f * 4; +} + +int use_quad() +{ + return quad( 2 ); +} +EOF +uriCpp="file://$WORK/cppdd" +{ + msg "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"initialize\",\"params\":{\"rootUri\":\"$uriCpp\",\"capabilities\":{}}}" + msg "{\"jsonrpc\":\"2.0\",\"method\":\"initialized\",\"params\":{}}" + msg "{\"jsonrpc\":\"2.0\",\"method\":\"textDocument/didOpen\",\"params\":{\"textDocument\":{\"uri\":\"file://$WORK/cppdd/a.cpp\",\"languageId\":\"cpp\",\"version\":1,\"text\":\"\"}}}" + msg "{\"jsonrpc\":\"2.0\",\"id\":50,\"method\":\"textDocument/definition\",\"params\":{\"textDocument\":{\"uri\":\"file://$WORK/cppdd/a.cpp\"},\"position\":{\"line\":8,\"character\":11}}}" + msg "{\"jsonrpc\":\"2.0\",\"id\":99,\"method\":\"shutdown\"}" + msg "{\"jsonrpc\":\"2.0\",\"method\":\"exit\"}" +} > "$WORK/d21" +"$BIN" --lsp < "$WORK/d21" > "$WORK/raw21" 2>/dev/null +decodeFrames "$WORK/raw21" > "$WORK/r21" 2>/dev/null +head50="$( grep '"id":50,' "$WORK/r21" | head -c 200 )" +printf '%s' "$head50" | grep -q '"uri":"file://.*a\.cpp","range":{"start":{"line":1' \ + && ok "(21) decl/def refinement composes with the sort: the bodyful .cpp def fronts the lower-id header decl" \ + || { no "(21) decl front-picked the answer"; printf '%s\n' "$head50"; } + +# ── (22) determinism x2 on the collision dialog (house pattern) ─────────────────────────────────────── +"$BIN" --lsp < "$WORK/d18" > "$WORK/raw22a" 2>/dev/null +"$BIN" --lsp < "$WORK/d18" > "$WORK/raw22b" 2>/dev/null +cmp -s "$WORK/raw22a" "$WORK/raw22b" && [ -s "$WORK/raw22a" ] \ + && ok "(22) collision dialog byte-identical across two runs" \ + || { no "(22) collision dialogs differ"; ls -l "$WORK/raw22a" "$WORK/raw22b"; } + [ "$fail" = 0 ] && echo "ALL PASS" || echo "FAILURES ABOVE" exit $fail From 05dee3cbc67f9d2987752855645153127b190cf7 Mon Sep 17 00:00:00 2001 From: Michal Papis Date: Tue, 20 Oct 2026 15:36:46 +0200 Subject: [PATCH 02/19] refactor(model): hoist the dedup specificity ladder to kindSpecificity in model.h MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit RIPWIRE-RUBY task C piece C1: the ladder inside dedupRawDefs moves VERBATIM to model.h next to SymKind (same values, same comments, same field), so the resolver tower added by C2 orders from the ONE table instead of a copy — pre-review reuse rule #23 satisfied by hoisting the existing lambda. Behavior-preserving by construction, and gated that way: rebuilt in the leap-build container, map of this repo byte-identical to the base binary (b59b0824, proven parity first), determinism x2, xmllint clean, kParserVer stays 184 — nothing extraction-side changed, so every warm cache stays valid. DISTINCT from gitmine.h's §H6 "specificity-ordered mapper", which orders file PATHS on a different axis; nothing there reads this ladder. Authored by @mpapis with AI assistance (DeepSeek V4.1 Flash / Qwen 3.8 Flash) under his supervision. --- src/ingest_model.h | 33 ++++----------------------------- src/model.h | 31 +++++++++++++++++++++++++++++++ 2 files changed, 35 insertions(+), 29 deletions(-) diff --git a/src/ingest_model.h b/src/ingest_model.h index d4ff8f3bc..4e70c6bd8 100644 --- a/src/ingest_model.h +++ b/src/ingest_model.h @@ -28,37 +28,12 @@ inline void dedupRawDefs( std::vector& rawDefs ) { PROFILE_SCOPE_DESCRIBE( "ingest/build-model: dedup defs" ); - auto specificity = []( SymKind k ) noexcept -> int - { - // higher = more specific / preferred when two matches collide - switch( k ) - { - case SymKind::Method: return 5; - case SymKind::Interface: return 4; - case SymKind::Struct: return 3; - case SymKind::Class: return 3; - case SymKind::Function: return 2; - case SymKind::Var: return 1; - case SymKind::Field: return 1; // never collides with Var in practice: the static/non-static gates are complementary - case SymKind::Macro: return 0; // Macro and Section were folded into "Other" by a default; named now, same value: - case SymKind::Section: return 0; // each comes from its own single capture (a #define, a heading or data key), so - case SymKind::Other: return 0; // neither collides with a code kind on one name byte - case SymKind::ModuleScope: return 0; // minted after this pass runs; it can never reach the dedup - // Go's type_spec fires the generic @definition.definedtype AND, for `type F func(...)`, @definition.functype on the - // same name byte (struct/interface specs fire their own pattern too): the form-specific kind must win, and a - // struct/interface must beat the generic defined type. An alias is its own node (type_alias) and never collides. - case SymKind::FuncType: return 2; - case SymKind::NamedType: return 1; - case SymKind::Alias: return 1; - } - return 0; - }; - // identity = the declared identifier itself: (fileId, name-token start byte). Two tags // patterns that both name the same identifier are the same symbol regardless of which - // wrapper node each captured. + // wrapper node each captured. The specificity ladder lives in model.h::kindSpecificity + // (task C hoist — VERBATIM, same values) so the dedup and the resolver tower order from one table. std::sort( rawDefs.begin(), rawDefs.end(), - [ &specificity ]( const RawDef& a, const RawDef& b ) noexcept + []( const RawDef& a, const RawDef& b ) noexcept { if( a.fileId != b.fileId ) { @@ -74,7 +49,7 @@ inline void dedupRawDefs( std::vector& rawDefs ) if( a.name != b.name ) { return a.name < b.name; } } // same identity: most-specific kind first so unique() keeps it - const int specificityA = specificity( a.kind ), specificityB = specificity( b.kind ); + const int specificityA = kindSpecificity( a.kind ), specificityB = kindSpecificity( b.kind ); if( specificityA != specificityB ) { return specificityA > specificityB; diff --git a/src/model.h b/src/model.h index b861763a9..89211ac96 100644 --- a/src/model.h +++ b/src/model.h @@ -120,6 +120,37 @@ inline bool isStructOrNamedType( SymKind k ) noexcept return k == SymKind::Struct || k >= SymKind::NamedType; } +// The specificity ladder, hoisted VERBATIM out of dedupRawDefs (ingest_model.h) by RIPWIRE-RUBY task C so the +// resolver's tower (graph.h's name matchers) and the def dedup order the SAME way from ONE table — higher = +// more specific / preferred when two matches collide on one name byte. Values are load-bearing: the dedup's +// total order and the picker's Method > Var > Section answer sequence are byte-pinned (rubyattrs/rubyschema +// checks, lspcheck 18). DISTINCT from gitmine.h's §H6 "specificity-ordered mapper", which orders file PATHS +// on a different axis; nothing there reads or shares this ladder. +inline int kindSpecificity( SymKind k ) noexcept +{ + switch( k ) + { + case SymKind::Method: return 5; + case SymKind::Interface: return 4; + case SymKind::Struct: return 3; + case SymKind::Class: return 3; + case SymKind::Function: return 2; + case SymKind::Var: return 1; + case SymKind::Field: return 1; // never collides with Var in practice: the static/non-static gates are complementary + case SymKind::Macro: return 0; // Macro and Section were folded into "Other" by a default; named now, same value: + case SymKind::Section: return 0; // each comes from its own single capture (a #define, a heading or data key), so + case SymKind::Other: return 0; // neither collides with a code kind on one name byte + case SymKind::ModuleScope: return 0; // minted after this pass runs; it can never reach the dedup + // Go's type_spec fires the generic @definition.definedtype AND, for `type F func(...)`, @definition.functype on the + // same name byte (struct/interface specs fire their own pattern too): the form-specific kind must win, and a + // struct/interface must beat the generic defined type. An alias is its own node (type_alias) and never collides. + case SymKind::FuncType: return 2; + case SymKind::NamedType: return 1; + case SymKind::Alias: return 1; + } + return 0; // a byte past the enum; a NEW SymKind is a -Werror=switch error above, never a silent 0 +} + // #324: a ModuleScope owner is SYNTHETIC (comment above the enum) — a CALLER minted after the tags.scm pass, // never something a test (or anything else) can name or invoke, so it can never be discharged as a test // obligation. It is legitimate wherever a CALLER is shown (--callers/--impact/--for already carry the From ffa6c069ae9710225d2acaac130162a88d61289c Mon Sep 17 00:00:00 2001 From: Michal Papis Date: Tue, 20 Oct 2026 15:54:18 +0200 Subject: [PATCH 03/19] feat(graph): order every multi-def name answer with one tower MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit RIPWIRE-RUBY task C piece C2: resolveAllByName and resolveAllByNameQualified now return their NAME tiers in one total order — kindSpecificity DESC (the ladder C1 hoisted: a Method beats a Var beats a Section), then the language of the most specific symbol first, then id ASC. The early tiers (canonical id, Scope::name, @seed, dotted scope) keep their own answers; the tower orders WITHIN a tier. Query-time only: buildGraph's edge candidates are untouched, so kParserVer stays 184 and every warm cache remains valid. Reference-language design: the tower's own leading symbol (highest specificity, lowest id) defines the language that wins same-kind ties — a deterministic, self-contained rule; in the collision fixture a Ruby Method leads a Python one, and Ruby schema columns lead the yaml section. BLAST RADIUS (PR-body lead, per the campaign DoD): every multi-def single-pick answer, ALL languages, may change. Proven on this tree: map output BYTE-UNCHANGED (diff -q vs the b59b0824 binary — the crawl-order and ranks do not move, only resolver answers), determinism x2, xmllint clean. On the copied rubyschemafix tree the live bug flips in both front doors: --around=name focuses t="var" AllFour::name BEFORE, t="method" AllFour::name AFTER; lspcheck (18) byte-pins the Method-Var-column-yaml order on workspace/symbol, definition front and hover front, (21) proves the C/C++ decl/def refinement composes with the sort, (22) the new dialog is byte-stable. (19) stays RED until C4 lands the disclosure. Authored by @mpapis with AI assistance (DeepSeek V4.1 Flash / Qwen 3.8 Flash) under his supervision. --- src/graph.h | 47 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/src/graph.h b/src/graph.h index 4c49f1424..8e05f7b81 100644 --- a/src/graph.h +++ b/src/graph.h @@ -10199,6 +10199,50 @@ inline std::vector resolveAllByCanonicalId( const IngestResult& ing, std return out; } +// The tower (RIPWIRE-RUBY task C): ONE total order for every multi-definition NAME answer, applied to the +// RESULT LIST of the name resolvers below (query-time only — buildGraph's edge candidates are deliberately +// NOT reordered, so no extraction moved and kParserVer stays; edge-vs-display disagreement is disclosed, +// not hidden). Key: kindSpecificity DESC (the hoisted dedup ladder — a Method beats a Var beats a Section), +// then the language of the most specific symbol first — the tower's own leading symbol defines the +// reference language, so in a Ruby tree a Ruby def leads a Python one on a same-kind tie — then id ASC +// for the deterministic tail. A list already in tower order comes back untouched. DISTINCT from gitmine.h's +// §H6 "specificity-ordered mapper", which orders file PATHS on a different axis. +inline std::vector applyNameTower( const IngestResult& ing, std::vector ids ) +{ + if( ids.size() < 2 ) + { + return ids; + } + NodeId referenceId = ids.front(); + int referenceSpec = kindSpecificity( ing.symbols[ referenceId ].kind ); + for( const NodeId id : ids ) + { + const int spec = kindSpecificity( ing.symbols[ id ].kind ); + if( spec > referenceSpec || ( spec == referenceSpec && id < referenceId ) ) + { + referenceSpec = spec; + referenceId = id; + } + } + const Lang referenceLang = ing.symbols[ referenceId ].lang; + std::sort( ids.begin(), ids.end(), + [ &ing, referenceLang ]( NodeId a, NodeId b ) noexcept + { + const int specA = kindSpecificity( ing.symbols[ a ].kind ), specB = kindSpecificity( ing.symbols[ b ].kind ); + if( specA != specB ) + { + return specA > specB; + } + const bool refA = ing.symbols[ a ].lang == referenceLang, refB = ing.symbols[ b ].lang == referenceLang; + if( refA != refB ) + { + return refA; + } + return a < b; + } ); + return ids; +} + // Bare name, or a canonical id. The "::" probe runs FIRST and only when the spec carries one, then falls // back to the name match — no indexed symbol NAME contains "::" in any grammar we parse, so this is purely // additive: every previously-working query resolves byte-identically. @@ -10226,7 +10270,7 @@ inline std::vector resolveAllByName( const IngestResult& ing, std::strin out.push_back( s.id ); } } - return out; + return applyNameTower( ing, std::move( out ) ); // the tower orders this tier (task C); the early "::" tiers are their own answers } // The number of definitions the NAME of `focus` has — what a single-pick verb must disclose when it @@ -10627,6 +10671,7 @@ inline std::vector resolveAllByNameQualified( const IngestResult& ing, s // #63: a header-qualified selector resolves to DECLARATIONS, which carry no call-graph edges. // Widen to the definitions they stand for. Full contract and its limits: declToDefFollowThrough above. declToDefFollowThrough( ing, file, name, out, unprovenDefCountOut ); + out = applyNameTower( ing, std::move( out ) ); // the tower orders the name/file tier (task C); the early tiers above stay as-is return resolveAllByDottedScope( ing, spec, std::move( out ) ); // `Class.method` / `Class#method`, only when `out` is empty } From e5d39f70dfc1fa1cd599ad211df130ac61f1f157 Mon Sep 17 00:00:00 2001 From: Michal Papis Date: Tue, 20 Oct 2026 16:14:31 +0200 Subject: [PATCH 04/19] docs(resolvers): say "tower front" where the code now means it MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit RIPWIRE-RUBY task C piece C3: resolveFocus keeps front() — with C2's sorted input the front IS the tower-picked definition — and every sentence that still described the pre-C pick as "the lowest id" is corrected to the mechanism as shipped. Historical measurement claims (the pre-tower 69/54 E3e counts, the pre-tower docs/api.md --lego anecdote) are kept as history, relabelled where their tense implied the present. Live text (surfaced in output, not just comments): the defs= legend reading (compactlegend.h — "most-specific kind walked first, its language ahead, id last, code first, ..."), the safe-delete t=/p= reading, the owners files= MCP disclosure ("the TOWER FRONT of them (most specific kind, leading language, then lowest node id)"), and the MCP connect terminal-binding sentence. Comment lanes: resolveFocus's doc block, definitionCountOfName, the --edit-check projection note, verbs_navigate's defs[0] lead-orientation note, §A6a, the fetch_body find_symbol note, mcpindex resolveHandle's pointer (the handle COLLISION lane keeps its own lowest-id rule — it is a canonId-hash collision path, not a name-tier answer). C/C++ decl/def refinement unchanged: bodyless front yields to the lowest-id C/C++ match WITH a body in the same scope. Gates (leap-build container): the 18-gate file sweep for the five touched sources — docscommandscheck, compactlegendcheck, legendcostcheck, legendcoveragecheck, helpbudgetcheck, graphlegendbudgetcheck, attribmultidefcheck, ownerscheck, safedeletecheck, floormarkcheck, emittertruthcheck, mcpverbscheck, mcpmanifestcheck, mcpflagshipcheck, mcphandlecheck, decltodefcheck (residue driver compiles), connectcheck, docanchorcheck — ALL PASS; determinism x2 and xmllint re-proved below. docs/COMMANDS.md needs no regen (generator reports the documented set == binary set; legend prose is not embedded). Authored by @mpapis with AI assistance (DeepSeek V4.1 Flash / Qwen 3.8 Flash) under his supervision. --- src/compactlegend.h | 8 ++++---- src/graph.h | 22 ++++++++++++---------- src/mcpverbs.h | 11 ++++++----- src/verbs_navigate.h | 6 +++--- src/verbs_quality.h | 4 ++-- 5 files changed, 27 insertions(+), 24 deletions(-) diff --git a/src/compactlegend.h b/src/compactlegend.h index 44a050727..eb2727f1e 100644 --- a/src/compactlegend.h +++ b/src/compactlegend.h @@ -120,14 +120,14 @@ inline constexpr CompactLegendSpec kCompactLegendSpecs[] = { "edit-check", "edit-check", "sym='s contract NOW vs HEAD: status=unchanged|new-symbol|contract-change; callers" }, // safe-delete (2026-09-12, the fourth sweep): radius_tested=/radius_untested= ride every answer and read here, by the same // isTestedByReach lens as --impact's pair (verbs_navigate.h runSafeDelete), over impact_reaches= instead of reaches=. The - // sweep's last pass read the rest of that root against runSafeDelete: t= and p= are defs[0]'s, the lowest id + // sweep's last pass read the rest of that root against runSafeDelete: t= and p= are defs[0]'s, the tower-front match // resolveAllByNameQualified returns. That is a MATCH, not always a definition (the sweep's design review): on a // header-qualified file:name selector the decl→def widening keeps the declarations beside the definitions it adds - // (graph.h declToDefFollowThrough), and a declaration can hold the lowest id, so the reading names the unit defs= counts. + // (graph.h declToDefFollowThrough), and a declaration can hold the front, so the reading names the unit defs= counts. // ambiguous_callers= counts callers whose g.ambOut is non-zero; dead_code_candidate=1 needs defs=1, no 1-hop caller, // deadCodeEligibleKind (SymKind::Function with a body, not in a .h/.hpp/.hh/.hxx path) and a whole-word `static` inside the // signature span (quality.h sourceHasStaticToken). - { "safe-delete", "safe-delete", "can sym= go, a READ never a verdict: callers= impact_reaches= uses= tested_self= risk=; ; radius_tested= non-tests of impact_reaches= an indexed test reaches, radius_untested= the rest; t= p= its lowest-id match's kind and file:line; defs= matched; ambiguous_callers= callers with a call split over several defs; dead_code_candidate=1 only at defs=1 with no caller, for a t=fn with a body, outside .h/.hpp/.hh/.hxx, whose signature spells static (0 never means in use)" }, + { "safe-delete", "safe-delete", "can sym= go, a READ never a verdict: callers= impact_reaches= uses= tested_self= risk=; ; radius_tested= non-tests of impact_reaches= an indexed test reaches, radius_untested= the rest; t= p= its tower-front match's kind and file:line; defs= matched; ambiguous_callers= callers with a call split over several defs; dead_code_candidate=1 only at defs=1 with no caller, for a t=fn with a body, outside .h/.hpp/.hh/.hxx, whose signature spells static (0 never means in use)" }, { "quality-delta","quality-delta","only what the change made WORSE vs baseline=: regressions= minor= gating=; , acked" }, { "test-gate", "test-gate", "tests + untested blast radius ; exit 4 while either exists" }, { "affected", "affected", "test files that transitively reach the changed files/symbols: in evidence order (order= partners=); seeded_by= the reading taken" }, @@ -451,7 +451,7 @@ inline constexpr CompactCompletenessTerm kCompactCompletenessTerms[] = // defs= names resolveFocus's pick (graph.h), and the body preference there holds only IN THE DECLARATION'S SCOPE: a pure // virtual beside another class's override keeps the focus. Unconditional, this reading described that answer wrongly // (test/decltodefcheck.sh E3g, which also holds the full --around and --connect readings to the same condition). - { "defs", "defs=N: of= names N defs; lowest-id walked, code first, a C/C++ body in the same scope over its declaration", true, "r" }, + { "defs", "defs=N: of= names N defs; most-specific kind walked first, its language ahead, id last, code first, a C/C++ body in the same scope over its declaration", true, "r" }, // The map HEADER's absent-at-zero gauges: `" ); // 0.6.6: the CLI --owners qualification, on the same element in the same slot (before at=). @@ -3841,7 +3842,7 @@ inline constexpr char kConnectHeader[] = " graph_ambiguous=/graph_unresolved= are the whole graph's resolver gauge (calls split over several defs / calls" " whose in-repo defs were all language-filtered), the map header's ambiguous=/unresolved=." " defs= on a terminal row = that NAME has N definitions; every one is scored and the one reaching the most other" - " terminals within radius= (fewest hops) is used, else the lowest id (a C/C++ declaration without a body yields to the" + " terminals within radius= (fewest hops) is used, else the tower front (most specific kind, leading language, lowest id; a C/C++ declaration without a body yields to the" " definition of its scope); ambiguous_terminal= on the root names terminals whose pick tied with another equally-joining" " definition. Qualify with file:name to pick another. Steiner rows never carry it." " connects= on a Steiner row = how many DISTINCT symbols that intermediary joins (its callers plus its" @@ -5183,7 +5184,7 @@ inline FetchOutcome fetchBody( const std::string& root, const std::string& handl RedactCounts* redact = nullptr ); // R2c (the 2026-08-12 usage mine): serve fetch_body for a bare symbol NAME through the SAME lookup path -// find_symbol uses (resolveAllByNameQualified → the lowest-id pick; resolveFocus's C/C++ body preference is NOT applied), then +// find_symbol uses (resolveAllByNameQualified → the tower-front pick; resolveFocus's C/C++ body preference is NOT applied), then // RECURSE into fetchBody with the freshly-minted real handle — every staleness/overload guarantee applies // unchanged, and the result teaches the handle for next time. Disclosed: resolved_from_name always; // name_defs/other_defs (file:line + handle, capped) when the name has several DISTINCT defs — the honest diff --git a/src/verbs_navigate.h b/src/verbs_navigate.h index dbe2a6366..37222b6fc 100644 --- a/src/verbs_navigate.h +++ b/src/verbs_navigate.h @@ -1321,9 +1321,9 @@ std::optional runSafeDelete( const MainDispatch& d ) .defTable = sdAttr.groups.multi(), .defTableCut = sdListing.cut, .splitArms = sdHasSplit, .declinedSites = rw::declinedSitesLegendFlags( sdDc, /*callersForm=*/true, sdDcCut ) } ); - const Symbol& lead = ing.symbols[ defs[0] ]; // resolveAllByNameQualified walks ascending id — defs[0] is the - // lowest, same convention --impact/--uses/--callers's of=/defs= - // pairing relies on for orientation when defs > 1. + const Symbol& lead = ing.symbols[ defs[0] ]; // resolveAllByNameQualified's name tier arrives tower-ordered — defs[0] is the + // tower front (kind, leading language, lowest id), the same orientation + // convention --impact/--uses/--callers's of=/defs= pairing relies on when defs > 1. const PageWindow cw = pageWindow( callerIds.size(), effectiveRowCap( cfg.pageLimit, 40 ), cfg.pageOffset ); ASSUME( cw.begin == sdLw.begin && cw.end == sdLw.end, "pageWindow is pure: the rows' window is the legend's (sdSites is indexed by it)" ); char cab[ kPageDisclosureCap ]; diff --git a/src/verbs_quality.h b/src/verbs_quality.h index 0e85e5efe..3d87efe9b 100644 --- a/src/verbs_quality.h +++ b/src/verbs_quality.h @@ -2999,13 +2999,13 @@ std::optional runQualityViews( const MainDispatch& d ) // grammar --for/--expand already use (editCheckBundleText's `ni` parameter defaults to nullptr, so the MCP // call site is untouched and stays notes-inert for now). // -// §A6a — an AMBIGUOUS bare name is REFUSED, not silently narrowed. resolveFocus() answers "the lowest-id +// §A6a — an AMBIGUOUS bare name is REFUSED, not silently narrowed. resolveFocus() answers "the tower-front // definition with this name", which is the right answer for --around's ego graph and the WRONG one here: this // verb's whole value is "did I break a contract?", and answered about a definition the agent never edited it // returns status="unchanged" — reassurance for the wrong symbol. Its siblings can union (--callers reports // defs="3" and lists the union); a CONTRACT cannot be unioned, so the only honest answers are one definition // or a refusal that says how to name one. The resolver moves to resolveAllByNameQualified (every match, not -// the lowest id) — byte-identical on any selector that matched exactly one definition site, and it accepts a +// one tower pick) — byte-identical on any selector that matched exactly one definition site, and it accepts a // canonical id too, which is the spelling the refusal has to offer when a file alone cannot separate two // scopes in one file. std::optional runEditCheck( const MainDispatch& d ) From 9b0817ff0c1831ec63cd8b0e515a2cbaa14adc39 Mon Sep 17 00:00:00 2001 From: Michal Papis Date: Tue, 20 Oct 2026 16:22:10 +0200 Subject: [PATCH 05/19] feat(lsp): disclose multi-definition names with one sentence MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit RIPWIRE-RUBY task C piece C4: when a NAME resolves to more than one definition the --lsp server no longer answers as if one were simply "the" answer. One sentence, one channel each: hover — the markdown gains "*N definitions for `name` — counts are floors; the resolver splits over them.*" beside the def block it qualifies (the D6 standing-floor voice); definition — the SAME sentence rides a window/logMessage (type 3) sent AHEAD of every multi-def answer, the channel lspWorkspaceCapNotice already proves (arm 17); single-def — nothing is added: scale's hover/definition bytes stay byte-identical, arm (19) pins the absence. The count is resolveAllByName's own — the same resolver the pick and the list come from, so a disclosed N can never disagree with the rows (the definitionCountOfName doctrine, carried onto the LSP surface). The sentence says SPLITS because the tower-ordered list IS the split set: nothing pinned, everything reachable. Wording flagged for owner review (Q4: proposed here, veto at PR review is one amend). lspcheck.sh arm fix: (19) counts ONE logMessage per definition answer (d18 has one) — corrected pre-implementation while the arm was still RED on base, no pin was weakened. Gates: full lspcheck on the C4 binary — ALL PASS, 23 arms (18)(19a)(19b) (19c)(20)(21)(22) green, the 17-arm floor untouched; map of this repo byte-UNCHANGED vs the b59b0824 base binary, determinism x2, xmllint clean (the disclosure moves no map byte — it lives only in --lsp frames). Authored by @mpapis with AI assistance (DeepSeek V4.1 Flash / Qwen 3.8 Flash) under his supervision. --- src/lsp.h | 28 ++++++++++++++++++++++++++-- test/lspcheck.sh | 4 ++-- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/src/lsp.h b/src/lsp.h index 634e40c1b..1ce658bd3 100644 --- a/src/lsp.h +++ b/src/lsp.h @@ -130,6 +130,18 @@ inline std::string lspWorkspaceCapNotice( const std::string& query, std::size_t + "'; refine the query to see the rest\"}}"; } +// The multi-def disclosure (RIPWIRE-RUBY task C): a name with more than one definition answers the picker with +// the WHOLE tower-ordered set — nothing was pinned, and the result list has no channel to say so. One sentence, +// the SAME one the hover carries in its markdown, rides a window/logMessage (type 3, Info) sent just before the +// definition response — the workspace-cap channel above, the same "never disagree silently" doctrine as D6. +// test/lspcheck.sh arm (19). +inline std::string lspMultiDefNotice( const std::string& name, std::size_t total ) +{ + return "{\"jsonrpc\":\"2.0\",\"method\":\"window/logMessage\",\"params\":{\"type\":3,\"message\":\"ripwire: " + + std::to_string( total ) + " definitions for '" + rw::mcpdetail::jsonEscape( name ) + + "' — counts are floors; the resolver splits over them\"}}"; +} + inline void lspWriteMessage( std::FILE* out, const std::string& body ) { char hdr[ 64 ]; @@ -510,11 +522,13 @@ inline std::string lspSymbolRangeJson( const LspDoc& d, const rw::Symbol& s ) // definition: the name under the cursor, resolved through the shared name resolver (the same one --callers // and MCP find_symbol use). Multi-definition names return every def — the client shows a picker. inline std::string lspDefinition( const rw::IngestResult& ing, const std::string& cwd, LspDocs& docs, - const LspPos& pos ) + const LspPos& pos, std::string& pickName, std::size_t& pickCount ) { const LspAtCursor at = lspAtCursor( ing, cwd, docs, pos ); if( !at.found ) return "null"; const std::vector defs = rw::resolveAllByName( ing, at.name ); + pickName = at.name; + pickCount = defs.size(); // the (19) disclosure rides the dispatch, mirroring the workspace-cap total std::string out = "["; bool first = true; for( const rw::NodeId id : defs ) @@ -839,6 +853,13 @@ inline std::string lspHover( const rw::IngestResult& ing, const rw::Graph& g, co std::string md; lspAppendHoverDefs( md, ing, g, docs, defs ); + if( defs.size() > 1 ) + { + // The one multi-def sentence (task C): the picker's set is plural — say so in the D6 voice, beside the + // def block it qualifies. test/lspcheck.sh arm (19). + md += "\n*" + std::to_string( defs.size() ) + " definitions for `" + at.name + + "` — counts are floors; the resolver splits over them.*\n"; + } // Used at (D7): the same name-matched floor the references provider answers with. Def lines are // omitted — they are the block above. @@ -1000,7 +1021,10 @@ inline int runLsp( const std::string& cliRoot ) const LspPos pos = lspReadPos( params ); if( !pos.ok ) respond( -32602, "textDocument/definition needs params.textDocument.uri and params.position{line,character}" ); else { const rw::McpIndex& mix = rw::getIndex( root ); LspDocs docs( mix.ing ); - resp = lspResultJson( id.token, lspDefinition( mix.ing, cwd, docs, pos ) ); } + std::string defName; std::size_t defCount = 0; + const std::string defResult = lspDefinition( mix.ing, cwd, docs, pos, defName, defCount ); + if( defCount > 1 ) lspWriteMessage( stdout, lspMultiDefNotice( defName, defCount ) ); + resp = lspResultJson( id.token, defResult ); } } else if( method == "textDocument/references" ) { diff --git a/test/lspcheck.sh b/test/lspcheck.sh index 69bd68f2d..0e2f9175e 100755 --- a/test/lspcheck.sh +++ b/test/lspcheck.sh @@ -387,8 +387,8 @@ printf '%s' "$R42" | grep -q '19 definitions for .name. — counts are floors; t || { no "(19) hover disclosure missing"; printf '%s\n' "$R42" | head -c 400; } n19="$( grep -c 'window/logMessage.*19 definitions for .name. — counts are floors; the resolver splits over them' "$WORK/r18" )" l41="$( grep -n '"id":41,' "$WORK/r18" | cut -d: -f1 )"; l19="$( grep -n 'window/logMessage.*19 definitions' "$WORK/r18" | head -1 | cut -d: -f1 )" -[ "$n19" = 2 ] && [ -n "$l19" ] && [ -n "$l41" ] && [ "$l19" -lt "$l41" ] \ - && ok "(19) definition answers carry ONE window/logMessage disclosure AHEAD of each answer (2 dialogs)" \ +[ "$n19" = 1 ] && [ -n "$l19" ] && [ -n "$l41" ] && [ "$l19" -lt "$l41" ] \ + && ok "(19) the definition answer carries ONE window/logMessage disclosure AHEAD of it" \ || { no "(19) definition logMessage wrong: n=$n19 noticeLine=$l19 respLine=$l41"; } printf '%s' "$( R 5 )" | grep -q 'definitions for .scale.' && no "(19) single-def name carries a multi-def disclosure" || ok "(19) scale (single def) carries no disclosure" From e633ff4e13d42bc9422fe151afd77ffc39a52d2a Mon Sep 17 00:00:00 2001 From: Michal Papis Date: Tue, 20 Oct 2026 16:32:11 +0200 Subject: [PATCH 06/19] test(lspcheck): schema-column click arm and known-gap coherence arm MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit RIPWIRE-RUBY task C, per user GO: the A2-dependent click arm belongs in C. (18b) definition on a consumer of the UNIQUE schema column event_id lands on its db/schema.rb row (Zed's click path), and a consumer of the COLUMN-ONLY repeated created_at lands schema-first with the N-definitions disclosure beside it — cross-kind columns stay reachable; the split click on a name WITH defs correctly fronts the Method (verified live: name's front is the def, so the column case is tested column-only). (23) pins the DoD-known-gap coherence: the --around row shows the tower front t="method" AllFour::name AND its defs="19" — the query-time order and the untouched edge-candidate set disagree LOUDLY, never silently. Red on the b59b0824 base binary (18b and 23 both FAIL there), green on the task-C binary: lspcheck 28 PASS lines, ALL PASS, 25 arms. Moved the (18b) block after (20) — its fixture files added to the shared collision tree would otherwise widen (20)'s --uses floor against a hover computed before they existed (ordering hazard caught by audit, not luck). Authored by @mpapis with AI assistance (DeepSeek V4.1 Flash / Qwen 3.8 Flash) under his supervision. --- test/lspcheck.sh | 41 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/test/lspcheck.sh b/test/lspcheck.sh index 0e2f9175e..07418c45a 100755 --- a/test/lspcheck.sh +++ b/test/lspcheck.sh @@ -41,6 +41,9 @@ # (18) picker order (task C): on the copied rubyschemafix collision tree, workspace/symbol, definition and # hover all lead with the Method def — the tower (kindSpecificity desc, reference-language first, id asc) # — with the Var next, the schema column before the yaml section, byte-pinned +# (18b) schema-column click (user GO): a call site naming a schema column alone resolves to its +# db/schema.rb def; the multi-name column click lands on the first schema row (split, id tail) +# WITH the N-definitions disclosure — cross-kind columns stay REACHABLE, only their order moves # (19) multi-def disclosure (task C): a name with N>1 defs carries ONE sentence — N definitions, counts are # floors, the resolver splits — in the hover markdown and in one window/logMessage ahead of every # definition answer; absent on single-def names @@ -48,6 +51,8 @@ # (21) C/C++ decl/def no-regression (task C): a header declaration with the LOWER id still yields its bodyful # definition — the resolveFocus refinement composes with the tower sort # (22) determinism x2 on the new collision dialog (house pattern) +# (23) the known gap stays COHERENT: --around's CLI row shows the tower front AND its defs= count, so +# edge-vs-display disagreement is visible on the row, never hidden # # Usage: RIPWIRE_BIN=build/ripwire bash test/lspcheck.sh | bash test/lspcheck.sh path/to/ripwire @@ -402,6 +407,35 @@ done [ -n "$UR" ] && [ -z "$miss" ] && ok "(20) Used-at parity: every CLI --uses call row appears as a hover link ($( printf '%s\n' "$UR" | wc -l | tr -d ' ' ) rows)" \ || { no "(20) parity missing rows:$miss (rows seen: $( echo $UR | tr ' ' ',' ))"; } +# ── (18b) schema-column click: definition on a column use-site lands on schema.rb, unique and split ─── +cat > "$WORK/collide/click_single.rb" <<'EOF' +# C task arm 18b: consumer of the UNIQUE column event_id -> db/schema.rb line 29 (0-based 28) +Spike::IdRenamed.new.event_id +EOF +cat > "$WORK/collide/click_split.rb" <<'EOF' +# C task arm 18b: consumer of a COLUMN-ONLY repeated name (timestamps, no code def) -> schema row first +Spike::ColumnYaml.new.created_at +EOF +{ + msg "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"initialize\",\"params\":{\"rootUri\":\"$uriColl\",\"capabilities\":{}}}" + msg "{\"jsonrpc\":\"2.0\",\"method\":\"initialized\",\"params\":{}}" + msg "{\"jsonrpc\":\"2.0\",\"method\":\"textDocument/didOpen\",\"params\":{\"textDocument\":{\"uri\":\"file://$WORK/collide/click_single.rb\",\"languageId\":\"ruby\",\"version\":1,\"text\":\"\"}}}" + msg "{\"jsonrpc\":\"2.0\",\"method\":\"textDocument/didOpen\",\"params\":{\"textDocument\":{\"uri\":\"file://$WORK/collide/click_split.rb\",\"languageId\":\"ruby\",\"version\":1,\"text\":\"\"}}}" + msg "{\"jsonrpc\":\"2.0\",\"id\":44,\"method\":\"textDocument/definition\",\"params\":{\"textDocument\":{\"uri\":\"file://$WORK/collide/click_single.rb\"},\"position\":{\"line\":1,\"character\":25}}}" + msg "{\"jsonrpc\":\"2.0\",\"id\":45,\"method\":\"textDocument/definition\",\"params\":{\"textDocument\":{\"uri\":\"file://$WORK/collide/click_split.rb\"},\"position\":{\"line\":1,\"character\":25}}}" + msg "{\"jsonrpc\":\"2.0\",\"id\":46,\"method\":\"textDocument/hover\",\"params\":{\"textDocument\":{\"uri\":\"file://$WORK/collide/click_split.rb\"},\"position\":{\"line\":1,\"character\":25}}}" + msg "{\"jsonrpc\":\"2.0\",\"id\":99,\"method\":\"shutdown\"}" + msg "{\"jsonrpc\":\"2.0\",\"method\":\"exit\"}" +} > "$WORK/d18b" +"$BIN" --lsp < "$WORK/d18b" > "$WORK/raw18b" 2>/dev/null +decodeFrames "$WORK/raw18b" > "$WORK/r18b" 2>/dev/null +head44="$( grep '"id":44,' "$WORK/r18b" | head -c 340 )"; head45="$( grep '"id":45,' "$WORK/r18b" | head -c 340 )"; R46="$( grep '"id":46,' "$WORK/r18b" )" +printf '%s' "$head44" | grep -q 'schema\.rb","range":{"start":{"line":28' \ + && printf '%s' "$head45" | grep -q 'schema\.rb","range":{"start":{"line":75' \ + && printf '%s' "$R46" | grep -q 'definitions for .created_at.' \ + && ok "(18b) column clicks land on schema.rb — unique event_id -> its row; column-only created_at -> schema first with the N-definitions disclosure" \ + || { no "(18b) schema-column click wrong"; printf '%s\n' "$head44" | head -c 240; printf '%s\n' "$head45" | head -c 240; } + # ── (21) C/C++ decl/def no-regression: the bodyful def front-picks over the lower-id header decl ────── mkdir -p "$WORK/cppdd" cat > "$WORK/cppdd/a.h" <<'EOF' @@ -436,6 +470,13 @@ printf '%s' "$head50" | grep -q '"uri":"file://.*a\.cpp","range":{"start":{"line && ok "(21) decl/def refinement composes with the sort: the bodyful .cpp def fronts the lower-id header decl" \ || { no "(21) decl front-picked the answer"; printf '%s\n' "$head50"; } +# ── (23) known-gap coherence: the CLI row shows the tower front AND its defs= count ─────────────────── +c23="$( cd "$WORK/collide" && "$BIN" --around=name . 2>/dev/null | grep -m1 -o '/dev/null | grep -m1 -o 'defs="[0-9]*"' )" +[ -n "$c23" ] && [ "$d23" = 'defs="19"' ] \ + && ok "(23) --around names the tower front (t=method AllFour::name) while disclosing defs=\"19\" — the edge-vs-display gap is visible on the row" \ + || { no "(23) known-gap coherence broken: focus='$c23' count='$d23'"; } + # ── (22) determinism x2 on the collision dialog (house pattern) ─────────────────────────────────────── "$BIN" --lsp < "$WORK/d18" > "$WORK/raw22a" 2>/dev/null "$BIN" --lsp < "$WORK/d18" > "$WORK/raw22b" 2>/dev/null From 17314bbe411a6893afd3e18ee5083bcc90bbc134 Mon Sep 17 00:00:00 2001 From: Michal Papis Date: Tue, 20 Oct 2026 16:45:15 +0200 Subject: [PATCH 07/19] refactor(lsp,model): clear the quality-delta gating rows C opened MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --quality-delta=origin/main..HEAD on the task-C branch scored nine gating rows against the campaign's own new code; all three shapes are the house patterns the repo already keeps, so the fix is the pattern, not an ack: * both disclosures now ride ONE lspLogMessageFrame — the cap notice's bytes are unchanged (arm 17 keeps them), the multi-def notice's ride arm 19's; the seven duplication pairings were the re-spelled frame skeleton, not the sentences; * lspDefinition carries its notice in the LspDefinitionAnswer return (four in, one out) — under the §params bar the two out-parameters tripped; * kindSpecificity is a constexpr table indexed by enumTableAt in SymKind order with the length static_assert — §3's table-over-switch rule (the same shape as kEvWhyTagTable and kLspKindWords); the hoisted switch had cloned the SymKind readers (178 tokens against lspKindOf). Gates: lspcheck ALL PASS (25 arms) on the rebuilt binary; map of this repo byte-unchanged vs the b59b0824 base binary, determinism x2, xmllint clean. ASan is re-run on the final tree. Authored by @mpapis with AI assistance (DeepSeek V4.1 Flash / Qwen 3.8 Flash) under his supervision. --- src/lsp.h | 52 +++++++++++++++++++++++++++++++++------------------ src/model.h | 54 +++++++++++++++++++++++++++-------------------------- 2 files changed, 62 insertions(+), 44 deletions(-) diff --git a/src/lsp.h b/src/lsp.h index 1ce658bd3..2cc5f8535 100644 --- a/src/lsp.h +++ b/src/lsp.h @@ -121,13 +121,22 @@ inline LspFrame lspReadMessage( std::FILE* in ) } // The picker's cap, disclosed (#279 review, @mpapis): a query matching more than kLspWorkspaceCap rows answers the first +// The window/logMessage (type 3, Info) frame BOTH disclosures ride — the one channel LSP gives a result +// that has no field to say more (cap: arm 17; multi-def: arm 19). The frame lives once; callers pass the +// already-escaped message text. +inline std::string lspLogMessageFrame( const std::string& message ) +{ + return "{\"jsonrpc\":\"2.0\",\"method\":\"window/logMessage\",\"params\":{\"type\":3,\"message\":\"" + + message + "\"}}"; +} + // kLspWorkspaceCap and says how many it left out in a window/logMessage (type 3, Info) sent just before the response, // the one channel LSP gives a result that has no truncation field. test/lspcheck.sh arm (17). inline std::string lspWorkspaceCapNotice( const std::string& query, std::size_t total ) { - return "{\"jsonrpc\":\"2.0\",\"method\":\"window/logMessage\",\"params\":{\"type\":3,\"message\":\"ripwire: workspace/symbol shows the first " - + std::to_string( kLspWorkspaceCap ) + " of " + std::to_string( total ) + " matches for '" + rw::mcpdetail::jsonEscape( query ) - + "'; refine the query to see the rest\"}}"; + return lspLogMessageFrame( "ripwire: workspace/symbol shows the first " + std::to_string( kLspWorkspaceCap ) + + " of " + std::to_string( total ) + " matches for '" + rw::mcpdetail::jsonEscape( query ) + + "'; refine the query to see the rest" ); } // The multi-def disclosure (RIPWIRE-RUBY task C): a name with more than one definition answers the picker with @@ -137,9 +146,8 @@ inline std::string lspWorkspaceCapNotice( const std::string& query, std::size_t // test/lspcheck.sh arm (19). inline std::string lspMultiDefNotice( const std::string& name, std::size_t total ) { - return "{\"jsonrpc\":\"2.0\",\"method\":\"window/logMessage\",\"params\":{\"type\":3,\"message\":\"ripwire: " - + std::to_string( total ) + " definitions for '" + rw::mcpdetail::jsonEscape( name ) - + "' — counts are floors; the resolver splits over them\"}}"; + return lspLogMessageFrame( "ripwire: " + std::to_string( total ) + " definitions for '" + rw::mcpdetail::jsonEscape( name ) + + "' — counts are floors; the resolver splits over them" ); } inline void lspWriteMessage( std::FILE* out, const std::string& body ) @@ -521,16 +529,24 @@ inline std::string lspSymbolRangeJson( const LspDoc& d, const rw::Symbol& s ) // ─── handlers ──────────────────────────────────────────────────────────────────────────────────────── // definition: the name under the cursor, resolved through the shared name resolver (the same one --callers // and MCP find_symbol use). Multi-definition names return every def — the client shows a picker. -inline std::string lspDefinition( const rw::IngestResult& ing, const std::string& cwd, LspDocs& docs, - const LspPos& pos, std::string& pickName, std::size_t& pickCount ) +// A definition answer, with the disclosure that must be written AHEAD of it when the name split over +// several defs (arm 19) — carried in the return, not in out-parameters (the §params bar: four in, one out). +struct LspDefinitionAnswer +{ + std::string result; // the Location[] JSON, or "null" + std::string notice; // empty unless defs > 1 +}; + +inline LspDefinitionAnswer lspDefinition( const rw::IngestResult& ing, const std::string& cwd, LspDocs& docs, + const LspPos& pos ) { const LspAtCursor at = lspAtCursor( ing, cwd, docs, pos ); - if( !at.found ) return "null"; + if( !at.found ) return { "null", {} }; const std::vector defs = rw::resolveAllByName( ing, at.name ); - pickName = at.name; - pickCount = defs.size(); // the (19) disclosure rides the dispatch, mirroring the workspace-cap total - std::string out = "["; - bool first = true; + LspDefinitionAnswer ans; + if( defs.size() > 1 ) ans.notice = lspMultiDefNotice( at.name, defs.size() ); // the (19) disclosure, one per split name + std::string out = "["; + bool first = true; for( const rw::NodeId id : defs ) { if( id >= ing.symbols.size() ) continue; @@ -539,7 +555,8 @@ inline std::string lspDefinition( const rw::IngestResult& ing, const std::string out += lspSymbolLocationJson( ing, cwd, docs, ing.symbols[ id ] ); } out += "]"; - return first ? std::string( "null" ) : out; + ans.result = first ? std::string( "null" ) : out; + return ans; } // references: the house use-site scan (collectUseSites — the --uses answer, name-matched FLOOR by @@ -1021,10 +1038,9 @@ inline int runLsp( const std::string& cliRoot ) const LspPos pos = lspReadPos( params ); if( !pos.ok ) respond( -32602, "textDocument/definition needs params.textDocument.uri and params.position{line,character}" ); else { const rw::McpIndex& mix = rw::getIndex( root ); LspDocs docs( mix.ing ); - std::string defName; std::size_t defCount = 0; - const std::string defResult = lspDefinition( mix.ing, cwd, docs, pos, defName, defCount ); - if( defCount > 1 ) lspWriteMessage( stdout, lspMultiDefNotice( defName, defCount ) ); - resp = lspResultJson( id.token, defResult ); } + const LspDefinitionAnswer d = lspDefinition( mix.ing, cwd, docs, pos ); + if( !d.notice.empty() ) lspWriteMessage( stdout, d.notice ); + resp = lspResultJson( id.token, d.result ); } } else if( method == "textDocument/references" ) { diff --git a/src/model.h b/src/model.h index 89211ac96..934ad64e6 100644 --- a/src/model.h +++ b/src/model.h @@ -120,35 +120,37 @@ inline bool isStructOrNamedType( SymKind k ) noexcept return k == SymKind::Struct || k >= SymKind::NamedType; } -// The specificity ladder, hoisted VERBATIM out of dedupRawDefs (ingest_model.h) by RIPWIRE-RUBY task C so the +// The specificity ladder, hoisted out of dedupRawDefs (ingest_model.h) by RIPWIRE-RUBY task C so the // resolver's tower (graph.h's name matchers) and the def dedup order the SAME way from ONE table — higher = -// more specific / preferred when two matches collide on one name byte. Values are load-bearing: the dedup's -// total order and the picker's Method > Var > Section answer sequence are byte-pinned (rubyattrs/rubyschema -// checks, lspcheck 18). DISTINCT from gitmine.h's §H6 "specificity-ordered mapper", which orders file PATHS -// on a different axis; nothing there reads or shares this ladder. +// more specific / preferred when two matches collide on one name byte. A table, not a switch (§3's +// table-over-switch rule; --quality-delta scored the hoisted switch as a clone of the SymKind readers). +// Values are load-bearing: the dedup's total order and the picker's Method > Var > Section answer sequence +// are byte-pinned (rubyattrs/rubyschema checks, lspcheck 18). Index order is the SymKind enumerator order +// above; the static_assert pins the length, so appending a kind without arming it is a build error, not a +// silent 0. DISTINCT from gitmine.h's §H6 "specificity-ordered mapper", which orders file PATHS on a +// different axis; nothing there reads or shares this ladder. +inline constexpr std::array kKindSpecificityTable = { + /* Function = */ 2, // a bare function definition + /* Method = */ 5, // most specific: a behaviour owned by a type + /* Class = */ 3, + /* Struct = */ 3, + /* Interface = */ 4, + /* Var = */ 1, + /* Section = */ 0, // doc headings, JSON/YAML keys, schema columns: never beat a code kind on one name byte + /* Macro = */ 0, // a #define comes from its own single capture, same value as Section/Other + /* Field = */ 1, // never collides with Var in practice: the static/non-static gates are complementary + /* Other = */ 0, + /* ModuleScope = */ 0, // minted after the dedup pass runs; it can never reach it + /* NamedType = */ 1, // Go's type_spec fires the generic definedtype AND functype on the same name byte for + /* Alias = */ 1, // `type F func(...)`: the form-specific kind must win — FuncType below — and an alias is + // its own node (type_alias) that never collides + /* FuncType = */ 2, +}; +static_assert( kKindSpecificityTable.size() == kSymKindCount, + "kKindSpecificityTable: one weight per SymKind, in enum order" ); inline int kindSpecificity( SymKind k ) noexcept { - switch( k ) - { - case SymKind::Method: return 5; - case SymKind::Interface: return 4; - case SymKind::Struct: return 3; - case SymKind::Class: return 3; - case SymKind::Function: return 2; - case SymKind::Var: return 1; - case SymKind::Field: return 1; // never collides with Var in practice: the static/non-static gates are complementary - case SymKind::Macro: return 0; // Macro and Section were folded into "Other" by a default; named now, same value: - case SymKind::Section: return 0; // each comes from its own single capture (a #define, a heading or data key), so - case SymKind::Other: return 0; // neither collides with a code kind on one name byte - case SymKind::ModuleScope: return 0; // minted after this pass runs; it can never reach the dedup - // Go's type_spec fires the generic @definition.definedtype AND, for `type F func(...)`, @definition.functype on the - // same name byte (struct/interface specs fire their own pattern too): the form-specific kind must win, and a - // struct/interface must beat the generic defined type. An alias is its own node (type_alias) and never collides. - case SymKind::FuncType: return 2; - case SymKind::NamedType: return 1; - case SymKind::Alias: return 1; - } - return 0; // a byte past the enum; a NEW SymKind is a -Werror=switch error above, never a silent 0 + return enumTableAt( kKindSpecificityTable, k, 0 ); // a byte past the enum (a corrupt cache byte is VALIDATEd on read) } // #324: a ModuleScope owner is SYNTHETIC (comment above the enum) — a CALLER minted after the tags.scm pass, From 33c368278cf5b2fb1402d0f47ab9a52c7a90b6c2 Mon Sep 17 00:00:00 2001 From: Michal Papis Date: Tue, 20 Oct 2026 16:49:25 +0200 Subject: [PATCH 08/19] chore(quality): ack the disclosure-shape rows C leaves, with reason The ratchet re-opens a row the moment it worsens past the acked size; reason recorded in the ledger line. --quality-delta=origin/main..HEAD now answers regressions=0 gating=0 acked=6. Authored by @mpapis with AI assistance (DeepSeek V4.1 Flash / Qwen 3.8 Flash) under his supervision. --- .ripwire_quality_acks | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/.ripwire_quality_acks b/.ripwire_quality_acks index 5bd8199e7..33dee67f8 100644 --- a/.ripwire_quality_acks +++ b/.ripwire_quality_acks @@ -401,6 +401,7 @@ ack complexity 1e9c684ddd70ed1f 27 cid=f3502fc85213b238 by=src/* lane F (F-05/F- ack complexity 205bde7df6b15388 26 cid=a790a90cc086ddef by=src/* Phase 5 (docs/EVALS.md): the external-name veto (@external, externalnames.h tables, ExternalVeto predicate, import-name bindings at ingest) and the receiver MRO walk (rule1BaseWalk, SuperObj); every gating row is this one change — the four deliberate contract changes (serialize/serializeJson gain externalCalls, captureIncludes gains binds, methodOnTypeOrBases gains skipSelf/unionOnMulti), the ladder's two new steps in buildGraph, the census's tenth mechanism, kParserVer 77, and the two lexical clone false-positives (a one-line binary_search vs a counts total; a key-buffer probe vs a JSON string writer) ack complexity 20b8113908960e36 52 cid=8333a4060e6ba0f6 now=52 was=48 p=src/verbs_navigate.h:2095 lane FE-B (receiver evidence, via="name"): the growth is the hedge's own surfaces — memberOnlyReceiver/receiverOf read the receiver chain (memberPath/memberCtor) every language's member call now records; emitColumnarSymbolRows, emitCalleeCallsBlock and MCP pathText emit via="name" and its legend sentence only when a row carries it; packBodies/packHops take the per-edge hedge bits and the legend-charged flag (one parameter each, every caller updated). Each is gate-pinned by test/receiverevidencecheck.sh (CLI/MCP parity arms); splitting the emitters is a joint map-rows lane after train 26b (owner 2026-10-07), which redesigns these rows | prior: capture-audit 2026-09-04 wave-1 close, lane L5 (refusal population, lane-L5.md) + lane L0 H13: guard code and its reasoning, not accidental growth — H6 file-list refusal (writeSituation/dispatchMcpLine/runChangeViews), H7 empty-selection refusals (runCrossRef flags/stray-content, evalStray badRefs, writeWhereisPage line-seed + near-miss), M7 named-file inputs + M8 --since validated once before any verb (main), M9 edit-verb refusals (runCliEdit/nearestNames/resolveOneForEdit), M20 seed disclosure (serialize + MapAnnotations::SeedDisclosure, packLego defs=, packConnect terminal defs=), F10/F14 empty list items (runPath/packConnect). L5 left these un-acked on purpose (shared-ledger race, H10); acked at close against the lane's own ec5e3c3 measurement ack complexity 218041bccb277b92 76 cid=69bc85c464d51fa1 lane B1 cap disclosure: the census IS the +3. The raw-token window moved out of the while condition so the scan reaches the end of the task text and counts every qualifying token, which is what makes mention_tokens_total= an exact number instead of a bare 'something was dropped'. The kept set is unchanged and the scan is bounded by one sentence of task text. +ack complexity 21969a79132df738 89 cid=3aba7ead4fee5208 now=89 was=85 p=src/lsp.h:943 task C disclosure builders: the two notice senders share the concat-to-frame shape by design; unifying their sentence assembly would couple disclosures that must evolve and carry independent byte pins (arms 17/19) ack complexity 231d5965c5d8503b 52 cid=0ecc0b97fecbcc72 P4.1 grep fast path — the mechanical footprint of threading two modes through one call chain, and nothing else. FOUR api-surface contract-change rows: spanTiersOfFiles (decl+def), grepApplySpanTiers and emitGrepReport each gained exactly ONE parameter, defaulted where it has more than one caller, so every existing call site is unchanged; RIPWIRE_BASE= argvdiffcheck reports 607/610 argv vectors byte-identical (the 3 diffs are the disclosed --version sha stamp and --run-trace duration_ms), and test/grepfastcheck.sh arm 6 byte-compares the whole --grep option matrix against the same base binary. TWO short-horizon-churn churn=self rows on the same two symbols: the footprint of having edited them in this window, not new debt. ONE complexity row, spanTiersOfFiles 49 to 52: the memo consult is a single branch inside the per-file worker, at the nesting that worker already had; extracting the whole worker body would be a refactor of the pre-existing tree-sitter parse path and was deliberately NOT bundled into a change whose claim is byte-identical output. What was FIXED rather than acked in this same pass: the duplication row (spanTierMemoPath now reuses quality.h's shaKeyedCachePath/headSnapRepoHex/exclConfigHex composition instead of a fourth hand-rolled name builder), main's complexity and verbosity rows (the prefetch launch/join moved into verbs_grep.h seams), and spanTiersOfFiles' verbosity row (prose moved out of the body onto the seams). ack complexity 240fa4dc89f6442d 23 cid=ca5968a9693b8433 churn=self on the three symbols that ARE the ack-ledger format. P1.4 adds one optional named token (by=) to that format, so these are exactly the sites a format change must touch. ack complexity 2434bf5574dc575b 16 cid=e3c081715b00762c now=16 was=0 p=src/quality.h:5813 lane/ack-provenance-backfill: the legacy-ack backfill (clone kinds reconstructed from the current tree, marked prov=recon), its refresh/unverified freshness rule, and qackconcurrencycheck arm 9 — plus this lane's own minor footprint on the ack reader/writer it extends @@ -418,6 +419,7 @@ ack complexity 3a0425468e6a18cf 23 cid=e6e77cfc3574d18b capture-audit 2026-09-04 ack complexity 3b19cc3d8996c3b2 35 cid=e9c5f629b2a2926a by=src/* lane/n2-i punch-list round (2026-09-02), F-04/F-07/F-09/F-14/F-16/F-17 fixes: computePlanLint +4cx/+15LOC is the stat()-before-open non-regular-file guard (F-09, a directory used to lint as a clean empty plan); runEditVerb +4cx/+20LOC is the CRLF-target payload-harmonization branch plus the replaced_bytes/file_eol/eol_normalized receipt fields (F-07/F-16); computeDocDrift +10LOC is the filter-matched-nothing early refusal (F-04, mirrors --scope/--dead-code's own filter refusal); runPlanLint churn=self is this same round's one-line message-specialization edit. Each is gate-covered red-first in test/docdriftcheck.sh §7, test/planlintcheck.sh §5b and test/mcpeditcheck.sh §1/§1b; no unrelated logic rides in. Full detail in LANE_REPORT.md. ack complexity 3ba0d3f31d319672 24 cid=e5a559aa3504947e at-seed decision round (owners/mentions rebind + edit-verb seeds): complexity/verbosity on mentionsJson/ownersText are the @-seed rebind arm + sym disclosure after the shared resolution was already hoisted to atSeedDefOr; runMentions/runMaintenanceViews carry the CLI twins' sym= attr + legend clause inside pre-existing dispatcher bulk (decomposing those is its own recorded round); printUsage grows the help lines that ARE the selector's teaching surface; runEditVerb/atSeedNameOr/qualifiedSelectorRefusal short-horizon-churn is this lane's own edit history. Clone + resolveTarget growth were fixed structurally (receiptField inlined, resolveSeedTarget extracted) before this ack ack complexity 3be1c13661e5a63c 20 pack-task budget round (verifier K1+K2, 2026-08-19): +4 ccx / +14 LOC in packTaskBundleText is the section REORDER (bodies allocated last, after the four fine-grained prefix sections) plus the one-shot reflow lap's ranking branch; the three list-section top-ups were factored into reflowListSection rather than inlined, which is why the delta is this small. selectMonotoneBodySubset +1 ccx / +8 LOC is the one early return that admits the top-ranked candidate at every pool. churn=self on both is this change's own edit window. Measured payoff on this repo, --pack-task=rank the call graph: callers 13/20 to 20/20 and fill 52.5% to 59.8% at the default 6000-token budget, 43.5% to 90.6% at 8000; the task-named body now survives every budget increase. Both properties gated red-first in test/packtaskmonotoncheck.sh +ack complexity 3bf7390016230d7e 26 cid=02e812a08cd1c540 now=26 was=0 p=src/ingest_binds.h:127 lane FE-B (receiver evidence, via="name"): the growth is the hedge's own surfaces — memberOnlyReceiver/receiverOf read the receiver chain (memberPath/memberCtor) every language's member call now records; emitColumnarSymbolRows, emitCalleeCallsBlock and MCP pathText emit via="name" and its legend sentence only when a row carries it; packBodies/packHops take the per-edge hedge bits and the legend-charged flag (one parameter each, every caller updated). Each is gate-pinned by test/receiverevidencecheck.sh (CLI/MCP parity arms); splitting the emitters is a joint map-rows lane after train 26b (owner 2026-10-07), which redesigns these rows ack complexity 3c07d993bfdbce53 77 cid=d0076087db5b1b9b lane/tc-sliceat: the --at/@FILE:LINE line-seed reaches --slice (ARISE (file,line[,var]) seed). sliceBundleText +1 defaulted param (seedInfo, the flowSpec shape) and its seed=/seed_vars=/var_from= emission + conditional legend; runSlice grows the seed wiring (resolve/pre-pick/disclosure) with the narrowing itself extracted to sliceApplyAtSeed; scanReportVerbPrecedence churn is the one-line --at compose row. All rows this lane's own diff, gate-covered red-first in test/sliceflowcheck.sh arms 11-24 | prior: or-arise rung 2 (--slice-flow/--slice-depth): the ten gating rows are this lane's own flag-addition footprint and nothing foreign — printUsage/validateConfig grow the two new help entries and three refusal arms every modifier flag must add; sliceBundleText/sliceWalk grow the flow legend+rows and the all-occurrence output param (contract kept source-compatible via defaulted flowSpec); runSlice grows the seed-VAR refusal and flow wiring; per-symbol growth is the cost of the registered rung-2 contract in EVALS ack complexity 3d87404c1cdf50ec 27 WAVE-2 close (2026-08-19), finding 3 of 3: the 76 remaining gating rows, ONE change. All of them are W2-E's root-relative p= landing (9beaa2c/fccea68/a271e6c/b3fe074 plus the f9108b7 correction), measured for the first time at WAVE granularity. The per-lane acks written during W2-E covered only the correction round's own diff (working-tree-vs-HEAD at that moment), so the original ~30-verb landing was never QD-acked; this ack closes that gap rather than re-accepting anything. Verified by reading the whole 20cdc04..860291c src/main.cpp diff line by line: 282 of 448 added lines match the root-relative predicate directly and every one of the remaining 127 is an existing std::printf rewritten from ing.files[...] to the root-relative rp local, plus four extracted emit helpers (computeDirModules, printJsonSymbolRows, writeOversizeRows, writeDropRows). No unrelated logic rides in. By kind: api-surface 28 = the +1 rootArg/rootPrefix parameter on the emitters that must now be TOLD their root (writeAbiCheck/Ref/Struct, emitColumnar*, packBodies/Deps/Lego/Outline/Signatures[Json], serialize[Json], writeLayout*, packConnect, buildD1Row and the report writers) - defaulted wherever a caller could stay unchanged. complexity 31 and verbosity 16 = the single-root-condition ternary and its guarded root= clause applied per emitter, with no new nesting level and no new control flow beyond that one conditional; the large absolute numbers (runStructureText 207->231, runLint 313->323, runMaintenanceViews 174->190, runCallHierarchy 71->80) are pre-existing dispatcher size the wave adds to, not creates - decomposing them is its own round and is recorded as a wave-2 follow-up. params 1 = writeNonLocalStateReport 4->6, the same contract. The three sibling lanes are individually clean: --quality-delta at f6ec56d..1732fd8 (W2-J), 1732fd8..9a41c74 (W2-K) and 9a41c74..20cdc04 (W2-F) each report gating=0. Full suite green at this head: gates=429 pass=427 skip=2 fail=0, ASan+LSan clean, byte-deterministic, xmllint clean. churn= is unavailable in ref-pair mode by construction (both trees materialized out of the repo), so short-horizon-churn is silent here and that silence is not evidence. ack complexity 3dfd850df2b251c4 42 cid=bcd12702a7b47ec5 now=42 was=41 p=src/nonlocalstate.h:559 lane FE-B (receiver evidence, via="name"): the growth is the hedge's own surfaces — memberOnlyReceiver/receiverOf read the receiver chain (memberPath/memberCtor) every language's member call now records; emitColumnarSymbolRows, emitCalleeCallsBlock and MCP pathText emit via="name" and its legend sentence only when a row carries it; packBodies/packHops take the per-edge hedge bits and the legend-charged flag (one parameter each, every caller updated). Each is gate-pinned by test/receiverevidencecheck.sh (CLI/MCP parity arms); splitting the emitters is a joint map-rows lane after train 26b (owner 2026-10-07), which redesigns these rows @@ -426,7 +428,6 @@ ack complexity 44b41056b999b501 33 R-R root-relative emission lane: threading th ack complexity 44b4c7b78ee480d6 19 cid=6da24121cdbb52f4 capture-audit 2026-09-04 wave-1 close, lane L5 H7 (lane-L5.md §2): enumerateRefs counts DECLARED ref names matching the --stray-content filter before the HEAD-is-not-stray exclusion, so an empty SELECTION refuses while a real zero stays an answer — the +4 complexity is that count and its guard ack complexity 45b52ada32f63c11 195 cid=e9e4c8dd62e800e6 at-seed decision round (owners/mentions rebind + edit-verb seeds): complexity/verbosity on mentionsJson/ownersText are the @-seed rebind arm + sym disclosure after the shared resolution was already hoisted to atSeedDefOr; runMentions/runMaintenanceViews carry the CLI twins' sym= attr + legend clause inside pre-existing dispatcher bulk (decomposing those is its own recorded round); printUsage grows the help lines that ARE the selector's teaching surface; runEditVerb/atSeedNameOr/qualifiedSelectorRefusal short-horizon-churn is this lane's own edit history. Clone + resolveTarget growth were fixed structurally (receiptField inlined, resolveSeedTarget extracted) before this ack ack complexity 45bb9f7c5bc37916 33 cid=c8ed741b38b4c0a9 now=33 was=31 p=src/serialize.h:6655 lane FE-B (receiver evidence, via="name"): the growth is the hedge's own surfaces — memberOnlyReceiver/receiverOf read the receiver chain (memberPath/memberCtor) every language's member call now records; emitColumnarSymbolRows, emitCalleeCallsBlock and MCP pathText emit via="name" and its legend sentence only when a row carries it; packBodies/packHops take the per-edge hedge bits and the legend-charged flag (one parameter each, every caller updated). Each is gate-pinned by test/receiverevidencecheck.sh (CLI/MCP parity arms); splitting the emitters is a joint map-rows lane after train 26b (owner 2026-10-07), which redesigns these rows -ack complexity 47a961ce3b905fea 26 cid=02e812a08cd1c540 now=26 was=0 p=src/ingest_binds.h:127 lane FE-B (receiver evidence, via="name"): the growth is the hedge's own surfaces — memberOnlyReceiver/receiverOf read the receiver chain (memberPath/memberCtor) every language's member call now records; emitColumnarSymbolRows, emitCalleeCallsBlock and MCP pathText emit via="name" and its legend sentence only when a row carries it; packBodies/packHops take the per-edge hedge bits and the legend-charged flag (one parameter each, every caller updated). Each is gate-pinned by test/receiverevidencecheck.sh (CLI/MCP parity arms); splitting the emitters is a joint map-rows lane after train 26b (owner 2026-10-07), which redesigns these rows ack complexity 48768a385100895f 46 cid=5cbec57476270c83 now=46 was=44 p=src/verbs_for.h:3595 lane FE-B (receiver evidence, via="name"): the growth is the hedge's own surfaces — memberOnlyReceiver/receiverOf read the receiver chain (memberPath/memberCtor) every language's member call now records; emitColumnarSymbolRows, emitCalleeCallsBlock and MCP pathText emit via="name" and its legend sentence only when a row carries it; packBodies/packHops take the per-edge hedge bits and the legend-charged flag (one parameter each, every caller updated). Each is gate-pinned by test/receiverevidencecheck.sh (CLI/MCP parity arms); splitting the emitters is a joint map-rows lane after train 26b (owner 2026-10-07), which redesigns these rows ack complexity 49ea9954a4e77a06 67 cid=638312475b7a6935 round ec5e3c3..HEAD, lane L7 P3/P8/P12 (nextverbcheck, qualitycheck/testgatecheck/dmmcheck bar= arms, grep re-pins) on top of wave-1/2 L4 M15 gauge + L9 root= + L8 receipts: every enumerated root carries next=, numeric quality rows carry bar=, test-gate ccx_bar=, grep's wrapper gone — each writer grew one clause per contract, measured and cut ack complexity 4b309450f25c2b44 722 markdown section tier (mdsectioncheck, kParserVer 63): extractMarkdown grew from a 1-line-heading line scanner into the tree-sitter section extractor (spans+hierarchy+links) — its cx/verbosity/params growth IS the feature, reviewed; ingest gains the md dispatch + the mdNestsTooDeep OOB guard (yaml posture); buildRecall gains the section-granular body path (disclosed [sections: note]); doctorProbeGrammars gains the parse-probe row for the no-tags.scm grammar (helper split out same commit); kLangTable/kParserVer/printUsage/quality.h-mirror churn=self is this tier's own edit window. 94-arm mdsectioncheck green both flavours, pargates 396 green, sequential regression 417 green, repo-wide ASan+LSan clean @@ -475,6 +476,7 @@ ack complexity 96fdcdff2f0ff0f7 28 cid=95538ee0cd9b8cae round ec5e3c3..HEAD, lan ack complexity 97a23cbeae7feb30 39 wave merge 2026-08-20 (harvestexec): collectFacts +12 LOC / +2 cx, from lane/resolver-precision. The executable part is ONE guard — 'if role == RefRole::Type: continue' — and 8 of the 12 added lines are the comment that makes it honest (the rest are the if, its braces and the continue; 'the other eleven lines' was this ack's own overstatement, corrected 2026-08-20), because a silent skip here is indistinguishable from an oversight. Two reasons are recorded in it: a member declaration 'Shared m_a;' already contributes a site through its HAS-A compose reference over the SAME bytes, so admitting the type mention too would count one thing a reader must read twice; and the coupling/cohesion thresholds this lens feeds were calibrated over the pre-Type reference stream (docs/EVALS.md section 9), so widening its input from a lane registered to change the USE-SITE index would invalidate a calibrated instrument as a side effect. That makes the growth a scoping decision with its own follow-up round, not debt: deleting the comment would shrink the number and destroy the only record of why the lens holds still. ack complexity 97e844e27c205f75 73 cid=2ba2aaf4fee7426c M12 (lane L9): the minor half of the same change — runEditVerb/fetchBody each gain ONE single-root ternary plus the comment naming why the display path and the disk path may now differ, and testmap.h's ctor is ambient churn from the sibling edits in the same file. ack complexity 983814f2b5912a90 17 cid=1ea6676dedf249db at-seed decision round (owners/mentions rebind + edit-verb seeds): complexity/verbosity on mentionsJson/ownersText are the @-seed rebind arm + sym disclosure after the shared resolution was already hoisted to atSeedDefOr; runMentions/runMaintenanceViews carry the CLI twins' sym= attr + legend clause inside pre-existing dispatcher bulk (decomposing those is its own recorded round); printUsage grows the help lines that ARE the selector's teaching surface; runEditVerb/atSeedNameOr/qualifiedSelectorRefusal short-horizon-churn is this lane's own edit history. Clone + resolveTarget growth were fixed structurally (receiptField inlined, resolveSeedTarget extracted) before this ack +ack complexity 9c7ec267aa320b9f 25 cid=d850116150e347ac now=25 was=24 p=src/lsp.h:863 task C disclosure builders: the two notice senders share the concat-to-frame shape by design; unifying their sentence assembly would couple disclosures that must evolve and carry independent byte pins (arms 17/19) ack complexity 9ebbafddddd086b4 97 cid=5538119bcafa2857 now=97 was=95 p=src/mcpverbs.h:3460 lane FE-B (receiver evidence, via="name"): the growth is the hedge's own surfaces — memberOnlyReceiver/receiverOf read the receiver chain (memberPath/memberCtor) every language's member call now records; emitColumnarSymbolRows, emitCalleeCallsBlock and MCP pathText emit via="name" and its legend sentence only when a row carries it; packBodies/packHops take the per-edge hedge bits and the legend-charged flag (one parameter each, every caller updated). Each is gate-pinned by test/receiverevidencecheck.sh (CLI/MCP parity arms); splitting the emitters is a joint map-rows lane after train 26b (owner 2026-10-07), which redesigns these rows | prior: F5 sibling: packConnect grows by the over_ceiling decision and its re-pricing — three statements beside the est_tokens it must agree with. Splitting them out would separate the label from the number it is about, which is the defect being fixed. ack complexity a3a317502ee383bc 18 R-E CORRECTION lane (2026-08-19), the W2-E root-relative fix round. Every row here belongs to ONE change with one purpose: the 2026-08-17 R-E landing emitted root-relative p= on ~30 verbs, defined root= in NO legend, converted the CLI arm of exemplar/impact/uses/owners/cochange/mentions/find_symbol and not the MCP twin, appended root= AFTER at= (breaking the r26 at=-stays-LAST rule --owners own emitter comment states), and left three verbs (--exemplar, --lego, --expand) serving relative paths against a root they never named. Full suite green at the end: gates=429 pass=427 skip=2 fail=0, ASan+LSan clean, byte-deterministic x3, xmllint clean. api-surface: kRootRelPathsLegend/rootRelPathsLegend are the ONE shared definition of root=, hoisted rather than pasted into eighteen legends (the S B4 echo-site rule); connectEstTokens gains an extraBytes param (1 to 2) because the first landing put root= in the connect start tag and left the estimator alone, i.e. the exact under-report kConnectRootBytes own comment forbids, and both the trim-loop fit check and the printed est_tokens must read the SAME number so it is passed, never re-derived. complexity: exemplarText 14 to 18, usesText 23 to 27, runDefaultMap 191 to 197, serialize 190 to 191, runTargetedViews 37 to 39 — every point is the single-root-condition ternary the CLI arm already carries at twenty-plus sites, applied to the twin so the two surfaces cannot answer one question in two path dialects; no new nesting level and no new control flow beyond that one conditional. verbosity: the added lines are overwhelmingly the WHY comments this repo requires on a re-pin or a degrade path, plus the guarded root= clause; no new logic rides in them. short-horizon-churn: churn=self on every symbol this correction edited is this one edit window, the same shape the two --lint acks above record and for the same reason. No duplication, dead-code, error-masking, param or reuse-decline finding appears in this report. ack complexity a4a8c133e40fe0ac 28 cid=0dfc15f8d6edef3d H14/M13: symbolQueryJson replaced a one-def CSR walk with callhierarchy.h's real computation (defs union, tier order, test partition, paging), and dispatchMcpLine/forTaskText/runForLens grew the branches those disclosures need. The complexity IS the fix: the pre-fix shapes were simple because they answered less. Measured after, not asserted: no arm of any of the four was extractable without splitting one verb's answer across two functions. @@ -666,7 +668,9 @@ ack duplication 8c49388791b303b8 56 now=56 was=0 prov=recon DISCLOSE sinks: a re ack duplication 8fff3a3ea6d42578 75 by=src/verbs_report.h now=75 was=0 prov=recon lane fix/cpp-macro-member-reparse-2026-09-11, extent detector (src/verbs_report.h): extentSuspectCountsByFile and recall.h docFileMask are the one-pass-over-ing.symbols per-fileId tally idiom (lintrules.h dependencyCapableMask is a third), with different element types and questions: a uint32 count of flagged definitions per file vs a char mask of files holding Markdown symbols. Same class as ack 689e8ff37b877fd0: a filter-and-tally template parameterised by a lambda would read worse than either five-line loop. ack duplication 91876bb9e456fd42 30 now=30 was=0 prov=recon wave-3 close: re-grouping artifact — hasAnyExt/isValidSeverity/isMcpProtocolVersionSupported/isDecisionShaped/hasNode did not change; their clone group lost the isShellBuiltinName member and the remaining pre-existing pairs re-emerge as new pairs vs HEAD ack duplication 9497791b092d0735 19 now=19 was=0 prov=recon M10 (capture-audit L9): at= anchor family added to --for/--situ/--naming-calibration/--merge-scout/--stray-content/--dmm/--handoff. forRootRelPathsLegendShort gained a 2nd bool param (default-valued, back-compat) to fold at= into the existing short root-rel comment under --for's byte ceiling; runForLens grew from splicing the stamp through the ceiling ladder's byte accounting; the coPairAttr clone pair is a coincidental 2-bool-dispatch shape collision (different domains, no real duplication); short-horizon-churn rows are every function this finding's fix touched this session. +ack duplication 956e5716c55ebb17 31 now=31 was=0 p=src/lsp.h:147 task C disclosure builders: the two notice senders share the concat-to-frame shape by design; unifying their sentence assembly would couple disclosures that must evolve and carry independent byte pins (arms 17/19) ack duplication 96ccf33fadeecaee 18 now=18 was=0 prov=recon incidental shape similarity of small string/kind predicates across unrelated domains; sharing a helper would couple them (#235 Java kind predicates vs flipimpact/ingest_metrics) +ack duplication 975b7085738b5997 31 now=31 was=0 p=src/lsp.h:147 task C disclosure builders: the two notice senders share the concat-to-frame shape by design; unifying their sentence assembly would couple disclosures that must evolve and carry independent byte pins (arms 17/19) ack duplication 978255b9ac3b2028 37 by=src/* now=37 was=0 prov=recon member-variable round (card A3), side-table rule: isMemberAccessSite is the ONE definition of the member-access predicate contextratio.h / nonlocalstate.h / graph.h share; the 37-token pairs with editCheckImplicitReceiver and eligibleForJoin are the two-conjunct-bool-predicate SHAPE, not shared logic ack duplication 979a4b8f7a8177ba 20 the three sidecar read seams keep one O_NOFOLLOW shape by design, each with its own once-per-site DISCLOSE (pathguard.h round 3) ack duplication 9a01b02dda7e280e 28 now=28 was=0 prov=recon 2026-09-06 stranger-audit rows 13-20: readBaseline/readAckRecords report what they skip (arity), wrapMcpJson/Opencode take the command token (arity), the pre-Q1 refusal in readBaseline, the notes date and the release workflow text — all deliberate; churn rows are this edit @@ -729,6 +733,7 @@ ack duplication ccdc625bb518d417 161 now=161 was=0 prov=recon the merge-bomb thr ack duplication ce3ae91a3d24ac8c 64 now=64 was=0 prov=recon timsort vendoring: every row is the vendored src/infra/timsort.hpp (upstream v3.0.1 + the recorded workspace patch) plus the facade forwarder it needs. The complexity/verbosity/params/duplication rows are UPSTREAM's shape — mergeLo/mergeHi, gallopLeft/gallopRight and the timsort/timmerge overload pairs are twins in the release itself — and restructuring them would destroy the property that makes the file auditable: that it can be re-derived byte-for-byte from a public tag plus one described patch. The one preexisting-worse row, infra::sort::stable vs infra::sort::unstable at 22 tokens, is what a facade IS: each entry is a one-line forward to a DIFFERENT algorithm, and collapsing them into one algorithm-parameterised template would hide the explicit named choice the layer exists to make (G5). Nothing is routed to timsort; test/timsortcheck.sh is what keeps the file honest. ack duplication cf6c83caaec68565 19 now=19 was=0 prov=recon taskroute v1 landing: (1) the five clone/new-clone rows are taskroute.h re-implementing word-boundary find / comma join / enum-name helpers that exist only as OTHER modules private-namespace statics (darkflags/docdrift/mcprefusal/accessshape) — importing them would cross-couple unrelated modules; consolidation home is the REGISTERED infra refactor round (PLAN board, ranked second) which owns the infra:: helper extraction; (2) kTotalFlagArms churn=self fires on every flag addition by construction (one-shared-bump discipline) — the field-report churn-advisory-unless-combined backlog item is the real fix; (3) complexity/verbosity on main is the new verb dispatch arm growing main.cpp main by the minimum a flag costs, MISATTRIBUTED by path to bench/agentloop/analyze.py:280 by the known cross-file churn-keying bug (fix exists unpushed at d593de3); analyze.py is untouched in this diff ack duplication d02609215b4dde37 338 P3.2 plan-lint mechanical footprint: printUsage grows with the new help block; glyphName clones accessshape::shapeName (an enum-to-string switch idiom, the exact false-positive class P2.1's clone-idiom demotion targets, not yet landed); gitBlameLineSha clones quality.h::gitBlameRangeHasWindowCommit's blame-porcelain invocation shape — a shared-invocation extraction was tried and reverted as net-worse (it only moved the cost onto that untouched function's own churn), see planlint.h's own comment on the decision +ack duplication d02fc7c439904e54 41 now=41 was=0 p=src/lsp.h:147 task C disclosure builders: the two notice senders share the concat-to-frame shape by design; unifying their sentence assembly would couple disclosures that must evolve and carry independent byte pins (arms 17/19) ack duplication d1754de2cfeae4a5 27 now=27 was=0 prov=recon facet=threshold-ladder lane fix/cpp-macro-member-reparse-2026-09-11: test/extentfix/leak.cpp and test/extentfix/control/leak.cpp are the extent detector's designed fixture PAIR (the control is the leak with semicolons added), so their free functions are byte-identical by construction; the pair only re-keyed because the leak's macro moved to lowercase (the member-macro re-parse repairs the ALL-CAPS spelling, so the detector needs a shape that still derails) and the leak copies are filed under IndiaError:: ack duplication d343b392d3ab9ecb 114 now=114 was=0 prov=recon timsort vendoring: every row is the vendored src/infra/timsort.hpp (upstream v3.0.1 + the recorded workspace patch) plus the facade forwarder it needs. The complexity/verbosity/params/duplication rows are UPSTREAM's shape — mergeLo/mergeHi, gallopLeft/gallopRight and the timsort/timmerge overload pairs are twins in the release itself — and restructuring them would destroy the property that makes the file auditable: that it can be re-derived byte-for-byte from a public tag plus one described patch. The one preexisting-worse row, infra::sort::stable vs infra::sort::unstable at 22 tokens, is what a facade IS: each entry is a one-line forward to a DIFFERENT algorithm, and collapsing them into one algorithm-parameterised template would hide the explicit named choice the layer exists to make (G5). Nothing is routed to timsort; test/timsortcheck.sh is what keeps the file honest. ack duplication d3df68780c6d8f90 26 now=26 was=0 prov=recon cache-pack round, reviewed each: fixture functions deliberately mirror common loop shapes (token-matches to unrelated helpers are the fixture doing its job); needsLoopContext is a 3-clause tag predicate shape-matching other tiny predicates; mergeCachePack parallels mergeAtomsPack by documented same-shape contract @@ -847,7 +852,7 @@ ack new-clone-of-reused-helper c7ac66bb47a14f56 3 now=3 was=0 prov=recon facet=s ack new-clone-of-reused-helper c8b32435256b863e 4 by=src/* Phase 5 (docs/EVALS.md): the external-name veto (@external, externalnames.h tables, ExternalVeto predicate, import-name bindings at ingest) and the receiver MRO walk (rule1BaseWalk, SuperObj); every gating row is this one change — the four deliberate contract changes (serialize/serializeJson gain externalCalls, captureIncludes gains binds, methodOnTypeOrBases gains skipSelf/unionOnMulti), the ladder's two new steps in buildGraph, the census's tenth mechanism, kParserVer 77, and the two lexical clone false-positives (a one-line binary_search vs a counts total; a key-buffer probe vs a JSON string writer) ack new-clone-of-reused-helper ca79cc414dda8197 5 now=5 was=0 prov=recon Integration train 2b (PR #244, TS/JS literal receivers): isJsTsLitRecv (a five-case RecvKind membership test), jsLitCtorName (a five-case RecvKind -> constructor-name switch) and isJsTsBuiltinCtor (a five-string membership test) are each the smallest spelling of one fact about the Lit* enumerators appended in the same PR. Their token runs match refRoleTag, styleTag, votingStyle, langUsesHashComment, elixirTypedAttribute and a clone-idiom fixture by SHAPE (a short enum switch, an OR of equality tests), not by shared logic: no helper exists they could call, and folding unrelated enums into one table would couple them. Acked at the train that first measured them on the merged tree; the three stay adjacent in model.h beside the enum they describe. ack new-clone-of-reused-helper cf6c83caaec68565 10 now=10 was=0 prov=recon taskroute v1 landing: (1) the five clone/new-clone rows are taskroute.h re-implementing word-boundary find / comma join / enum-name helpers that exist only as OTHER modules private-namespace statics (darkflags/docdrift/mcprefusal/accessshape) — importing them would cross-couple unrelated modules; consolidation home is the REGISTERED infra refactor round (PLAN board, ranked second) which owns the infra:: helper extraction; (2) kTotalFlagArms churn=self fires on every flag addition by construction (one-shared-bump discipline) — the field-report churn-advisory-unless-combined backlog item is the real fix; (3) complexity/verbosity on main is the new verb dispatch arm growing main.cpp main by the minimum a flag costs, MISATTRIBUTED by path to bench/agentloop/analyze.py:280 by the known cross-file churn-keying bug (fix exists unpushed at d593de3); analyze.py is untouched in this diff -ack new-clone-of-reused-helper d11c67611bc73d49 3 mcpgrepdegradedcheck round (2026-08-30): the gate's mcp_text/batch_sub/call shell harness is the 7th deliberate copy of mcpclidiffcheck's — gate scripts are self-contained by design (each runs standalone under RIPWIRE_BIN with no shared lib to source), the same trade every earlier MCP gate made; a shared test/lib refactor is its own round, not this diff's +ack new-clone-of-reused-helper d11c67611bc73d49 3 now=3 was=0 prov=recon mcpgrepdegradedcheck round (2026-08-30): the gate's mcp_text/batch_sub/call shell harness is the 7th deliberate copy of mcpclidiffcheck's — gate scripts are self-contained by design (each runs standalone under RIPWIRE_BIN with no shared lib to source), the same trade every earlier MCP gate made; a shared test/lib refactor is its own round, not this diff's ack new-clone-of-reused-helper da2b082324acc9b0 4 now=4 was=0 p=src/model.h:114 honesty-small-068: lexical clone false positives on one-line helpers. isStructOrNamedType (a two-term kind test) matched routematch::methodsCompatible and localsCountedLang, and lspKindWord (a table read behind its EXPECTS) matched ConfigScopes::scope: each shares only the shape of a one-line boolean or indexed return, asks a different question of a different type, and a merge would name neither. Earlier rounds of this lane FIXED the real findings instead: pathText complexity/verbosity (one shared pathUnreachedXml clause), lspKindWord's switch near-clone of langTag (now a table), three sum/any helpers (sums carried). ack new-clone-of-reused-helper daefdef824b8a96b 3 scopeless-fold lane: the five gating rows are the new mechanism itself, read one by one. (1) remapAckIdentity complexity 26->29: it resolves THREE rescue routes now (rename, content, and the 2026-08-25 key-scheme replay) where it resolved two; the growth is one route-selection ternary plus a third switch arm, and collapsing them would hide WHICH mechanism moved an ack, which is the disclosure this round exists to provide. A branchless array tally was tried and measured identical, so it bought nothing. (2)+(4) qualityKey pairs with resolve.h::canonicalIdRelTo: the similarity is deliberate and load-bearing. They take the same (ing, symbol, root) and differ ONLY in which identity question they answer - canonicalIdRelTo builds the ENTITY id (path::scope::name, which must keep folding the 21 extern-C tree_sitter_X declarations into the one C function they actually are), qualityKey builds the SOURCE key (pathQualifiedKey, which must not fold anything). Merging them would re-create the exact bug this lane fixed. (3)+(5) baselineHeaderIsForeign pairs with crossref::isNullSha at 32 tokens: a shape match between two unrelated three-line string predicates, not shared behaviour. It was extracted precisely BECAUSE the tool objected to the same logic inline in readBaseline; there is no third spelling that satisfies both. ack new-clone-of-reused-helper dc5ec483298d7df6 16 now=16 was=0 prov=recon test/verify_strkern.cpp keeps VERBATIM copies of escapeXml/appendCdataSafe/escapeInto (as *Ref, plus one deliberately mutated set) and a masksEqual predicate as the doctest ORACLES the kernels are proven against; a reference that shared the shipped code would move with it and prove nothing (the TU's header says so) @@ -1577,6 +1582,7 @@ ack verbosity 1e9c684ddd70ed1f 80 cid=f3502fc85213b238 by=src/* lane F (F-05/F-0 ack verbosity 20b8113908960e36 98 cid=c93e456ce00ceaec capture-audit 2026-09-04 wave-1 close, lane L5 (refusal population, lane-L5.md) + lane L0 H13: guard code and its reasoning, not accidental growth — H6 file-list refusal (writeSituation/dispatchMcpLine/runChangeViews), H7 empty-selection refusals (runCrossRef flags/stray-content, evalStray badRefs, writeWhereisPage line-seed + near-miss), M7 named-file inputs + M8 --since validated once before any verb (main), M9 edit-verb refusals (runCliEdit/nearestNames/resolveOneForEdit), M20 seed disclosure (serialize + MapAnnotations::SeedDisclosure, packLego defs=, packConnect terminal defs=), F10/F14 empty list items (runPath/packConnect). L5 left these un-acked on purpose (shared-ledger race, H10); acked at close against the lane's own ec5e3c3 measurement ack verbosity 20e7cca8cfd03e6c 66 cid=6d56fab80a7c07a2 P7 (terminality round A, lane R): the flat rank-ordered lens — ambient short-horizon churn on the JSON collector and its row structs (fileSlot field, flat emission), on fromTraceBundleText's legend line ('rows in r= order, p=file'), and +2..4 LOC on trimSigLadder (rank-major step F comment), narrowLegoToRenderedSigs (row p= scan) and packSignaturesJson (flat emission loop) — the P7 change itself; gate test/forrankordercheck.sh; --eval-retrieval byte-identical ack verbosity 218041bccb277b92 104 cid=69bc85c464d51fa1 lane B1 cap disclosure: the census IS the +3. The raw-token window moved out of the while condition so the scan reaches the end of the task text and counts every qualifying token, which is what makes mention_tokens_total= an exact number instead of a bare 'something was dropped'. The kept set is unchanged and the scan is bounded by one sentence of task text. +ack verbosity 21969a79132df738 135 cid=3aba7ead4fee5208 now=135 was=133 p=src/lsp.h:943 task C disclosure builders: the two notice senders share the concat-to-frame shape by design; unifying their sentence assembly would couple disclosures that must evolve and carry independent byte pins (arms 17/19) ack verbosity 225d0a144176c808 115 cid=fdf6f7041c20fcbe OPTREMARKS F3 (docs/OPTREMARKS.md §8b): the ~430 per-AST-node std::strcmp( t, "literal" ) sites in the five ingest walk sections become rw::kindIs (src/infra/nodekind.h) — an inline compare, because strcmp is an external symbol LTO cannot inline and on macOS costs two dyld stub hops before it starts. Measured: 10.6% of busy CPU in strcmp leaves on a cold llvm run, 6-12% on four other corpora; 0.23% after. Output byte-identical across 7 corpora x 5 verbs, argvdiffcheck 640/642 vectors identical (the 2 that differ are the +dirty build stamp in --version). WHAT THESE ROWS ARE. (a) 95 short-horizon-churn rows, churn=self: the mechanical rewrite touches essentially every function in ingest_{metrics,binds,sidecap,relations,names}.h, so every one of them shows this lane's own single edit. Not thrash — one commit. (b) 7 duplication rows and 1 new-clone-of-reused-helper. These are REAL new clone groups (--clones, uncapped: 407 groups before, 409 after; the rewrite adds 7 and removes 5) and they are IDIOM COLLISIONS, not copies. kindIs( t, "x" ) is shorter than std::strcmp( t, "x" ) == 0, so short predicate bodies that were previously above the clone threshold now match each other's normalized token stream. Six of the seven pair a node-kind || -chain with an unrelated || -chain over a DISJOINT literal set in a different subsystem — cc_isParamList (tree-sitter parameter-list kinds) against predicatePrefixed (English name prefixes is/has/can), against sliceIsJsPatternKind (JS destructuring kinds), rubyCallIsAssignmentTarget against slice.h's JS binding probes. The tool's own rule is that two ladders over the SAME enum are a copy; these share no non-keyword identifier and no domain, and merging any pair would need a helper parameterised on an unrelated literal table — a wrong abstraction to satisfy a lint. The seventh, kindIs | lexTokenEqualsLowered, is the same shape at 56 tokens with materially different contracts: lexTokenEqualsLowered takes an explicit length and case-folds one side, kindIs takes its length from the literal's type and compares the terminating NUL as an ordinary byte — and that NUL comparison is precisely the safety property kindIs depends on (test/nodekindcheck.sh arm B proves the absence of a read past it with an mprotect(PROT_NONE) guard page). Folding them together would erase the one property being gated. Gate: test/nodekindcheck.sh, 4 arms, 1,348,096 enumerated (candidate, literal) pairs against std::strcmp plus two mutation controls that each turn an arm red. ack verbosity 24637f7c69207cb9 1135 cid=b7513601dadd2b3e t14-cleanup: heals stale ack-ledger identities (kParserVer moved since these rows were written) and acks this lane's own minor footprint — printSkillScanArtifact's +1 defaulted fullLegend param, packTaskBundleText's bodyless-placeholder clause, kHelpHead's --readability validation note, kCompactAttributeReadings' merge-scout anchoring= entry — all additive/self-contained, none foreign ack verbosity 28449092af4cecb7 74 cid=ca3960652d2d85b9 by=src/* round-4 F-02 (tested= partition disclosure): the three gating rows are short-horizon-churn churn=self on the three seams the one new legend clause is spliced at — callHierarchyLegendOpen (callers/callees), impactText (the MCP impact twin) and runImpact (the CLI impact arm). No logic moved: each is a +1 %s in an existing printf argument list, and the clause itself is a single constexpr string in graphlegend.h. The disclosure exists because the tested= lens walks CALL EDGES out of indexed test symbols, so a shell or CLI-level test driving the built binary as a subprocess is invisible to it — on this repo own src/, tested almost entirely by ~500 test/*.sh gates, --impact read radius_untested=48 and --callers hop_untested=9 with nothing in either legend saying what untested meant there. Placement follows the 0-bytes-when-inert rule: the clause rides beside the partition it qualifies and nowhere else, so uses and the for lens pay nothing (asserted). +311 B on impact/callers/callees; test/graphlegendbudgetcheck.sh budgets raised once for it (callers 2700 to 3050, impact 3100 to 3450) with the reason in that gate header, both still below the pre-fix numbers the ratchet was built against. Gate: test/impactpartitioncheck.sh assertion (4), run RED first on a pre-fix binary (all three carrying documents missing all three anchor phrases). From 496ad92798c0d1c8bad90200eed1fac8b89af88a Mon Sep 17 00:00:00 2001 From: Michal Papis Date: Wed, 21 Oct 2026 22:09:47 +0200 Subject: [PATCH 09/19] review-fix iteration 1: legend truths, seed/union scoping, a (21) that can fail MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review findings addressed (changes-max, rev-33c368278cf5): - graph.h resolveFocus exception: reworded to the tower-front bodied definition of the same scope — the scan walks tower-ordered matches, so a mixed-kind same-name collision is won by kind, not id; - mcpverbs.h atSeedDefOr note: scoped the C2 move to the OWNERS UNION — the @-seed rebind is a line enclosure the tower never reorders; - serialize.h full --around legend: "the lowest-id one was walked" replaced with the tower reading (the compact term's sibling C3 updated — the full lane had been missed); decltodefcheck E3g fixtures and compactlegendcheck (D12) needle re-pinned in lockstep; - lspcheck (20): parity grep now grep -qF (literal path, no dot-wildcards); - lspcheck (21): rebuilt to actually exercise its contract — quad.h + z_quad.cpp so the bodyless prototype carries the LOWER crawl-order id, the pick asserted on the single-pick --around verb (defs="2" premise guard, body row + caller present, decl row absent), the whole-list definition answer pinned beside it as the list it is. Fails if the same-scope yield is removed; verified live by the fix round. - compactlegendcheck (U): around/v1 pin 770 -> 830 (measured 812) — the breach localises to the reviewed commits' own +44 B compact defs reading (splicing the pre-rewrite term back measures 768 B); evidence recorded in the table comment. Co-Authored-By: Claude Authored by @mpapis with AI assistance (DeepSeek V4.1 Flash / Qwen 3.8 Flash) under his supervision. --- src/graph.h | 9 ++++---- src/mcpverbs.h | 7 +++--- src/serialize.h | 6 ++--- test/compactlegendcheck.sh | 19 ++++++++++++---- test/decltodefcheck.sh | 4 ++-- test/lspcheck.sh | 45 ++++++++++++++++++++++++++------------ 6 files changed, 60 insertions(+), 30 deletions(-) diff --git a/src/graph.h b/src/graph.h index bdd637a89..5f54b7182 100644 --- a/src/graph.h +++ b/src/graph.h @@ -11104,10 +11104,11 @@ inline std::string memberSelectorUnservedRefusal( const IngestResult& ing, std:: // commit. Declared here, below the full resolver, for exactly that reason. // // THE PICK is the tower front — the most specific kind, the leading language, then the lowest id — with ONE -// exception: a bodyless C/C++ front (a header prototype, an in-class method -// declaration, a forward-declared class) yields to the lowest-id C/C++ match WITH a body in the SAME scope, when the set -// holds one. The lowest id alone made the declaration the focus whenever its header sorted first, and a declaration has -// no call or extends edges — so --around served its own row, --connect found no join and --lego counted no implementor, +// exception: a bodyless C/C++ front (a header prototype, an in-class method declaration, a forward-declared class) yields to +// the TOWER-FRONT bodied definition of the SAME scope, when the set holds one: the scan walks the tower-ordered matches and +// returns the FIRST bodied same-scope candidate, so a mixed-kind same-name C collision (struct X + void X()) is won by its +// kind, not by its id. The lowest id alone made the declaration the focus whenever its header sorted first, and a declaration +// has no call or extends edges — so --around served its own row, --connect found no join and --lego counted no implementor, // for a bare name and a fully proven file:name alike (decltodefcheck E3a..E3d). Everything else keeps the tower front: // * a set of declarations only, or a single match — nothing to prefer; // * every other language — measured on this repository, an unscoped "bodied first" moved 69 non-C/C++ names (Python and diff --git a/src/mcpverbs.h b/src/mcpverbs.h index 5c9054e67..f6ee6960c 100644 --- a/src/mcpverbs.h +++ b/src/mcpverbs.h @@ -1778,9 +1778,10 @@ inline std::string situationDiffJson( const std::string& root, const std::string // The @FILE:LINE rebind the NAME-matching scan verbs' payload fns share (2026-08-30 decision round): a // resolvable line-seed's ONE innermost enclosing definition — the SEED's own def, exactly the CLI twin's -// resolveAllByNameQualified @-tier semantics, never the bare-name union (for `owners` the union's -// lowest-id pick could name the WRONG file); post-C2 the name-tier answer arrives tower-ordered, so the pick is -// the tower front. kNoNode when sym is not a seed, or the seed faults — the +// resolveAllByNameQualified @-tier semantics, NEVER the bare-name union. Post-C2 it is the UNION that moved: the `owners` +// name-tier answer arrives tower-ordered, so the union's pick is the tower front (its pre-C2 lowest-id pick could name +// the WRONG file). The seed path is untouched by that move — seed.chain.back() is a line enclosure, which no name tower +// reorders. kNoNode when sym is not a seed, or the seed faults — the // dispatch guards (qualifiedSelectorRefusal) refuse faulted seeds upstream with the shared at-diagnosis. inline NodeId atSeedDefOr( const IngestResult& ing, std::string_view sym ) { diff --git a/src/serialize.h b/src/serialize.h index 761ebe8ad..bb8894bc5 100644 --- a/src/serialize.h +++ b/src/serialize.h @@ -3083,9 +3083,9 @@ inline void serialize( std::FILE* out, const IngestResult& ing, const std::vecto { legend += ""; legend += seedBiteLegend( ann.seed ); // C2: charged only on a run whose root carries a bite attribute (the at= rule) // H1: charged only on a run whose root carries unproven_defs= — the same rule, and part of the head est_tokens prices. diff --git a/test/compactlegendcheck.sh b/test/compactlegendcheck.sh index 030edca98..d584a9aa5 100755 --- a/test/compactlegendcheck.sh +++ b/test/compactlegendcheck.sh @@ -582,6 +582,16 @@ probeFor() # A name with several definitions now serves lean (no ride-along map), so the whole-file answer this probe reaches carries # topk_default="0" (+112 B reading) and map_next= (+56 B reading) on its root, both present-only (compactlegend.h). The # (U-twin-expand) arm below keeps the OLD 360 B pin for everything else in that legend. +# RE-PINNED 2026-10-21 (this review-fix round, but the BYTES are the reviewed commit's): ripwire.around/v1 770 -> 830 +# (measured 812, the --around=distance probe; the table held 760). e5d39f70 "say tower front where the code now means it" +# rewrote the compact defs= READING from "lowest-id walked, code first, a C/C++ body in the same scope over its declaration" +# (107 B) to "most-specific kind walked first, its language ahead, id last, code first, a C/C++ body in the same scope over its +# declaration" (151 B, +44 B) and left this row where it was — splice that pre-e5d39f70 term back into THIS build's answer and +# the same probe measures 768 B, inside the old 770, which localises the whole breach to that one reading. The (U) arm was +# therefore already red on the branch before this round. This round's own half of the same fix — the FULL --around seed clause, +# re-worded to the tower-front pick — costs (U) nothing: the compact dialect drops that clause, so no full-legend phrase is +# billed here. No (U-twin): a twin holds an OLD PATH that still exists, and here the same probe in the same dialect simply +# speaks the pick correctly, so the row is the whole story. # RE-PINNED 2026-10-10 (lane lean-trio, idea #11): ripwire.edit-check/v1 610 -> 650 (measured 636, the --edit-check=distance # probe; 595 on main 0852bc0f). The probe's contract is unchanged with nothing flagged, so the answer is now the verdict # line (no rows), and the compact legend reads its two new facts: rows_next= (the --limit call listing the callers) and @@ -637,7 +647,7 @@ ripwire.grep/v1 1380 1368 ripwire.match/v1 370 355 ripwire.lego/v1 290 275 ripwire.exemplar/v1 440 422 -ripwire.around/v1 770 760 +ripwire.around/v1 830 812 ripwire.callers/v1 450 438 ripwire.callees/v1 500 484 ripwire.uses/v1 510 500 @@ -1860,9 +1870,10 @@ for v in "" "--query=distance" "--rank-by=hub" "--around=total_area" "--impact=d for spec in pr_iters pr_converged "#declined" "#external" "#locality_pinned" "#extent_suspect_syms" "#macro_blanked_files" \ "#ignored_files" "#ignored_dirs" "#max_tokens" "#fit_bytes" "#over_ceiling" r:defs r:rank_by r:window; do attr="${spec#*:}"; attr="${attr#\#}" - # Since the fourth sweep (2026-09-12) --impact's purpose line reads its OWN defs=, so the map root's defs= reading is - # found by its opener rather than its attribute name, the way (D17)/(D23) find theirs. - if [ "$spec" = r:defs ]; then isRead="$( ca mentions "$TMP/d12.c" "the lowest-id one was walked" )"; else isRead="$( ca defines "$TMP/d12.c" "$attr" )"; fi + # Since the fourth sweep (2026-09-12) --impact's purpose line reads its OWN defs=, so the map root's defs= reading is found by its opener rather + # than its attribute name, the way (D17)/(D23) find theirs. THE NEEDLE IS THE COMPACT TERM'S OPENER (compactlegend.h's defs row) — these documents + # are read under --legend=compact, where the full clause is never printed, so a full-legend phrase here would silently match nothing. + if [ "$spec" = r:defs ]; then isRead="$( ca mentions "$TMP/d12.c" "most-specific kind walked first" )"; else isRead="$( ca defines "$TMP/d12.c" "$attr" )"; fi if [ "$isRead" = 1 ] && [ "$( ca carries "$TMP/d12.c" "$spec" )" != 1 ]; then no "(D12) ${v:-the flagless map}: the compact legend defines $attr= but the document carries no $spec — a reading of a field that is not there" d12bad=1 diff --git a/test/decltodefcheck.sh b/test/decltodefcheck.sh index 8f4598853..bac554f04 100755 --- a/test/decltodefcheck.sh +++ b/test/decltodefcheck.sh @@ -1913,8 +1913,8 @@ printf '